-
Notifications
You must be signed in to change notification settings - Fork 276
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
dialog: Use Cupertino-flavored alert dialogs on iOS #1017
base: main
Are you sure you want to change the base?
Conversation
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Thanks! Comments below.
Also, please tidy up the branch's commit history for clear and coherent commits. Each commit should be clean and pass all tests (you can run our tests with tools/check
).
lib/widgets/dialog.dart
Outdated
/// Sets the dialog action to be platform appropriate | ||
/// by displaying a [CupertinoDialogAction] for IOS platforms | ||
/// and a regular [TextButton] otherwise. | ||
Widget _adaptiveAction( | ||
{required BuildContext context, | ||
required VoidCallback onPressed, | ||
required Widget child}) { | ||
final ThemeData theme = Theme.of(context); | ||
switch (theme.platform) { | ||
case TargetPlatform.android: | ||
return TextButton(onPressed: onPressed, child: child); | ||
case TargetPlatform.fuchsia: | ||
return TextButton(onPressed: onPressed, child: child); | ||
case TargetPlatform.linux: | ||
return TextButton(onPressed: onPressed, child: child); | ||
case TargetPlatform.windows: | ||
return TextButton(onPressed: onPressed, child: child); | ||
case TargetPlatform.iOS: | ||
return CupertinoDialogAction(onPressed: onPressed, child: child); | ||
case TargetPlatform.macOS: | ||
return CupertinoDialogAction(onPressed: onPressed, child: child); | ||
} | ||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This can be tightened up in a few ways:
-
Use empty
case
s to fall through. From the doc on Dartswitch
statements: -
Use
defaultTargetPlatform
instead of passing throughcontext
. When the app is run on a device,defaultTargetPlatform
will match the platform (iOS or Android). When we need to simulate a specific platform in tests, we setdefaultTargetPlatform
; search fordebugDefaultTargetPlatformOverride
for how we do this. -
More concise dartdoc
Also, because the logic in _dialogActionText
fits and is recommended for the Material-style dialog, let's only apply it on Android. This helper is a fine place for that conditional logic; how about changing its interface so it takes a String instead of a Widget, and applies _dialogActionText
in the Android branch. In my proposal below, I've also given _dialogActionText
an appropriately specific name, _materialDialogActionText
. That helper could even be inlined, perhaps in a followup NFC commit.
So, putting all that together:
/// Sets the dialog action to be platform appropriate | |
/// by displaying a [CupertinoDialogAction] for IOS platforms | |
/// and a regular [TextButton] otherwise. | |
Widget _adaptiveAction( | |
{required BuildContext context, | |
required VoidCallback onPressed, | |
required Widget child}) { | |
final ThemeData theme = Theme.of(context); | |
switch (theme.platform) { | |
case TargetPlatform.android: | |
return TextButton(onPressed: onPressed, child: child); | |
case TargetPlatform.fuchsia: | |
return TextButton(onPressed: onPressed, child: child); | |
case TargetPlatform.linux: | |
return TextButton(onPressed: onPressed, child: child); | |
case TargetPlatform.windows: | |
return TextButton(onPressed: onPressed, child: child); | |
case TargetPlatform.iOS: | |
return CupertinoDialogAction(onPressed: onPressed, child: child); | |
case TargetPlatform.macOS: | |
return CupertinoDialogAction(onPressed: onPressed, child: child); | |
} | |
} | |
/// A platform-appropriate action for [AlertDialog.adaptive]'s [actions] param. | |
Widget _adaptiveAction({required VoidCallback onPressed, required String text}) { | |
switch (defaultTargetPlatform) { | |
case TargetPlatform.android: | |
case TargetPlatform.fuchsia: | |
case TargetPlatform.linux: | |
case TargetPlatform.windows: | |
return TextButton(onPressed: onPressed, child: _materialDialogActionText(text)); | |
case TargetPlatform.iOS: | |
case TargetPlatform.macOS: | |
return CupertinoDialogAction(onPressed: onPressed, child: Text(text)); | |
} | |
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Is this the idea for inlining the _materialDialogActionText
?
return TextButton(onPressed: onPressed, child: Text(text, textAlign: TextAlign.end));
will update the commit message of the latest commit as well as per the guidelines
test/widgets/dialog_checks.dart
Outdated
@@ -6,21 +8,41 @@ import 'package:flutter_test/flutter_test.dart'; | |||
/// Checks for an error dialog matching an expected title | |||
/// and, optionally, matching an expected message. Fails if none is found. | |||
/// | |||
/// On success, returns the widget's "OK" button. | |||
/// On success, returns the widget's "OK" button | |||
/// (which is a [CupertinoDialogAction] for OS platforms). |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
We can leave this dartdoc unchanged. It doesn't matter what specific widget the button is, as long as it responds to taps.
test/widgets/dialog_checks.dart
Outdated
final dialog = tester.widget<AlertDialog>(find.byType(AlertDialog)); | ||
tester.widget(find.descendant(matchRoot: true, | ||
of: find.byWidget(dialog.title!), matching: find.text(expectedTitle))); | ||
if (expectedMessage != null) { | ||
if (defaultTargetPlatform == TargetPlatform.iOS | ||
|| defaultTargetPlatform == TargetPlatform.macOS) { | ||
|
||
final dialog = tester.widget<CupertinoAlertDialog>(find.byType(CupertinoAlertDialog)); | ||
|
||
tester.widget(find.descendant(matchRoot: true, | ||
of: find.byWidget(dialog.content!), matching: find.text(expectedMessage))); | ||
} | ||
of: find.byWidget(dialog.title!), matching: find.text(expectedTitle))); | ||
|
||
return tester.widget( | ||
find.descendant(of: find.byWidget(dialog), | ||
matching: find.widgetWithText(TextButton, 'OK'))); | ||
if (expectedMessage != null) { | ||
tester.widget(find.descendant(matchRoot: true, | ||
of: find.byWidget(dialog.content!), matching: find.text(expectedMessage))); | ||
} | ||
|
||
return tester.widget( | ||
find.descendant(of: find.byWidget(dialog), | ||
matching: find.widgetWithText(CupertinoDialogAction, 'OK'))); | ||
|
||
} | ||
else { | ||
final dialog = tester.widget<Dialog>(find.byType(Dialog)); | ||
tester.widget(find.widgetWithText(Dialog, expectedTitle)); | ||
if (expectedMessage != null) { | ||
tester.widget(find.widgetWithText(Dialog, expectedMessage)); | ||
} | ||
return tester.widget( | ||
find.descendant(of: find.byWidget(dialog), | ||
matching: find.widgetWithText(TextButton, 'OK'))); | ||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Let's use an exhaustive switch
on defaultTargetPlatform
, like we do elsewhere. Also, there are several formatting nits that makes this code harder to read than it needs to be.
Proposal:
switch (defaultTargetPlatform) {
case TargetPlatform.android:
case TargetPlatform.fuchsia:
case TargetPlatform.linux:
case TargetPlatform.windows: {
final dialog = tester.widget<Dialog>(find.byType(Dialog));
tester.widget(find.widgetWithText(Dialog, expectedTitle));
if (expectedMessage != null) {
tester.widget(find.widgetWithText(Dialog, expectedMessage));
}
return tester.widget(
find.descendant(of: find.byWidget(dialog),
matching: find.widgetWithText(TextButton, 'OK')));
}
case TargetPlatform.iOS:
case TargetPlatform.macOS: {
final dialog = tester.widget<CupertinoAlertDialog>(
find.byType(CupertinoAlertDialog));
tester.widget(find.descendant(matchRoot: true,
of: find.byWidget(dialog.title!), matching: find.text(expectedTitle)));
if (expectedMessage != null) {
tester.widget(find.descendant(matchRoot: true,
of: find.byWidget(dialog.content!), matching: find.text(expectedMessage)));
}
return tester.widget(find.descendant(of: find.byWidget(dialog),
matching: find.widgetWithText(CupertinoDialogAction, 'OK')));
}
}
pubspec.lock
Outdated
@@ -315,10 +315,10 @@ packages: | |||
dependency: transitive | |||
description: | |||
name: file | |||
sha256: "5fc22d7c25582e38ad9a8515372cd9a93834027aacf1801cf01164dac0ffa08c" | |||
sha256: a3b4f84adafef897088c160faf7dfffb7696046cb13ae90b508c2cbc95d3b8d4 |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The changes to this file don't look related; please remove them.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Ok thankyou for the feedback, will be sure to onboard these points when working on issues in the future. I'll fix these things up in a new commit.
(Misclick, sorry) |
a255919
to
db740f9
Compare
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Thanks. You'll need to tidy up your branch, as I mentioned above, before we can review this again. If you need help, please ask in #git help
in the development community.
yep my bad, will do so. Thanks for your patients with me on this. |
ed8abb7
to
9d2c4df
Compare
As was suggested in a comment of the pull request zulip#1017 (comment).
9e6fc98
to
373cd75
Compare
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Thanks, this is much closer! Small comments below.
Also, a few commit message nits:
dialog: display adaptive dialogs and action buttons based on the target platform
AlertDialog was changed to AlertDialog.adaptive to the effect described in #996.
_adaptiveAction was implemented to display a platform appropriate action for
AlertDialog.adaptive's actions param, as was also discussed in #996.
tests in dialog_test were updated to perform platform appropriate tests.
- This commit fixes an issue (🎉), so let's put a
Fixes: #996
line at the end of it. - Also, I think the paragraph ("AlertDialog was changed…") doesn't add anything that's not already obvious from reading the code changes and the linked issue, so let's just delete it.
dialog [nfc]: inline _materialDialogActionTest in _adaptiveAction
As was suggested in a comment of the pull request https://github.com/zulip/zulip-flutter/pull/1017#discussion_r1813819656.
-
The URL should be on a new line; we try to wrap to 68 columns except where doing so would make things more confusing. How about:
As suggested at: https://github.com/zulip/zulip-flutter/pull/1017#discussion_r1813819656
Then for both commit messages, use initial caps for the part after the prefix, so:
dialog: Display adaptive dialogs and action buttons based on the target platform
dialog [nfc]: inline _materialDialogActionTest in _adaptiveAction
For examples of commit messages in the project's style, see the project's Git history; I recommend Greg's excellent tip about how to do that.
As was suggested in a comment of the pull request zulip#1017 (comment).
373cd75
to
52d762d
Compare
52d762d
to
6166a0a
Compare
6166a0a
to
f91a8c5
Compare
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Thanks!
Some new nits below, and also some more commit-message nits that I should have caught last time 🙂:
dialog: Display adaptive dialogs and action buttons based on the target platform
This summary line is too long, at 80 characters. How about:
dialog: Use Cupertino-flavored alert dialogs on iOS
This also has a bit more information: the fact that iOS was the platform getting the wrong-style dialog. (The fact that the dialog's buttons match the rest of the dialog isn't surprising enough to need a mention. 🙂)
For what the length limit actually is, see discussion, which I've just started 🙂. This was a helpful opportunity for me to spot a change in the zulip/zulip documentation that I'd missed!
dialog [nfc]: Inline _materialDialogActionTest in _adaptiveAction
The function's name is _materialDialogActionText
, not _materialDialogActionTest
.
f91a8c5
to
0a44828
Compare
all sorted :) |
2f8cd8b
to
8f95cff
Compare
8f95cff
to
b57c97e
Compare
b57c97e
to
2341baf
Compare
2341baf
to
a9c1daf
Compare
a9c1daf
to
45ed11e
Compare
45ed11e
to
ad45ea5
Compare
40f7646
to
b361f70
Compare
b361f70
to
c253497
Compare
c253497
to
343870b
Compare
343870b
to
2ef257b
Compare
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Thanks for the revision! Comments below.
return tester.widget(find.descendant(of: find.byWidget(dialog), | ||
matching: find.widgetWithText(TextButton, 'OK'))); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
nit: two-space indents (like in the existing code)
return tester.widget(find.descendant(of: find.byWidget(dialog), | |
matching: find.widgetWithText(TextButton, 'OK'))); | |
return tester.widget(find.descendant(of: find.byWidget(dialog), | |
matching: find.widgetWithText(TextButton, 'OK'))); |
test/widgets/dialog_checks.dart
Outdated
} | ||
|
||
return tester.widget(find.descendant(of: find.byWidget(dialog), |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
nit: avoid trailing whitespace
} | |
return tester.widget(find.descendant(of: find.byWidget(dialog), | |
} | |
return tester.widget(find.descendant(of: find.byWidget(dialog), |
If you use git log -p
to read through your changes for self-review, Git should highlight this sort of whitespace error for you. Here's how it looks in my terminal:
lib/widgets/dialog.dart
Outdated
import 'package:flutter/material.dart'; | ||
import 'package:flutter/cupertino.dart'; | ||
import 'package:flutter/foundation.dart'; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
nit: keep imports sorted
lib/widgets/dialog.dart
Outdated
onPressed: () { | ||
onActionButtonPress(); | ||
Navigator.pop(context); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Why does the Navigator.pop
get removed here? That seems like a substantive change unrelated to switching to Cupertino style.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
It looks like this was probably an error in resolving rebase conflicts. Mistakes there happen, but that's one of the reasons it's important to reread your changes when sending them for review:
https://zulip.readthedocs.io/en/latest/contributing/code-reviewing.html#reviewing-your-own-code
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
There are also some important tips that can make it a lot easier to resolve rebase/merge conflicts correctly, including a Git config setting (merge.conflictstyle diff3
) that is tragically not the default but you should definitely set. Details here:
https://chat.zulip.org/#narrow/channel/243-mobile-team/topic/rebase.20conflicts/near/1136429
https://chat.zulip.org/#narrow/stream/2-general/topic/navigating.20code.20review.3A.20Rebasing.20work/near/1737371
Also in the latter thread are tips about self-review specifically for the result of rebasing, starting at this message:
https://chat.zulip.org/#narrow/channel/2-general/topic/navigating.20code.20review.3A.20Rebasing.20work/near/1737381
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Yep that's my mistake, thanks for the materials - I'll make an effort to be more thorough about it
test/widgets/dialog_checks.dart
Outdated
case TargetPlatform.fuchsia: | ||
case TargetPlatform.linux: | ||
case TargetPlatform.windows: | ||
check(find.byType(Dialog)).findsNothing(); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Does this work correctly?
It looks like it would just always pass. Remember what we saw about find.byType
here: #1017 (comment)
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Hmm, right, AlertDialog
builds a Dialog
child rather than subclassing it. And there are no subclasses of Dialog
.
find.byType
is fine, then.
Still, we can make this check stronger:
-
No need for the switch — just unconditionally check there's no Dialog, and also no CupertinoAlertDialog. After all, if we did somehow end up with a Dialog on iOS or a CupertinoAlertDialog on Android, that would probably be a bug; and it definitely wouldn't be a case of there being "no dialog", so this
checkNoDialog
function should fail. -
Reading this code and comparing to
checkErrorDialog
andcheckSuggestedActionDialog
still makes me nervous that they don't seem to line up — it sure looks as if there might be cases where one of those passes and yet this also passes. (The only reason that's impossible is a fact about the details of howAlertDialog
works, which is that it usesDialog
as a child.)To fix that, this should use have a
find.bySubtype<AlertDialog>
check too. Thefind.byType(Dialog)
is a good additional check to make sure there isn't some unexpected other type of dialog, but it doesn't substitute for directly checking that the scenario in the other checks can't be happening.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
sure thing that all makes sense.
Should I also add in a find.byType(AlertDialog)
check for the sake of thoroughness as well? To ensure that no dialog has been created using the AlertDialog
constructor directly.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The find.bySubtype<AlertDialog>
check should cover that, right?
test/widgets/dialog_checks.dart
Outdated
final actionButton = tester.widget(find.descendant(of: find.byWidget(dialog), | ||
matching: find.widgetWithText(CupertinoDialogAction, expectedActionButtonText ?? 'Continue'))); | ||
|
||
final cancelButton = tester.widget(find.descendant(of: find.byWidget(dialog), | ||
matching: find.widgetWithText(CupertinoDialogAction, 'Cancel'))); | ||
|
||
return (actionButton, cancelButton); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
nit: group these as one stanza:
final actionButton = tester.widget(find.descendant(of: find.byWidget(dialog), | |
matching: find.widgetWithText(CupertinoDialogAction, expectedActionButtonText ?? 'Continue'))); | |
final cancelButton = tester.widget(find.descendant(of: find.byWidget(dialog), | |
matching: find.widgetWithText(CupertinoDialogAction, 'Cancel'))); | |
return (actionButton, cancelButton); | |
final actionButton = tester.widget(find.descendant(of: find.byWidget(dialog), | |
matching: find.widgetWithText(CupertinoDialogAction, expectedActionButtonText ?? 'Continue'))); | |
final cancelButton = tester.widget(find.descendant(of: find.byWidget(dialog), | |
matching: find.widgetWithText(CupertinoDialogAction, 'Cancel'))); | |
return (actionButton, cancelButton); |
That helps in seeing the overall organization, since there's now this extra layer of structure around these that is the switch
statement.
test/widgets/dialog_test.dart
Outdated
String title = "Dialog Title"; | ||
String message = "Dialog message."; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
nit: where possible, declare final (or in fact const)
group('showErrorDialog', () { | ||
testWidgets('show error dialog', (tester) async { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
nit: the test name says the same thing as the group name — no need for the group
import 'dialog_checks.dart'; | ||
import 'test_app.dart'; | ||
|
||
void main() { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Cool, these tests look broadly right.
Let's have them check one other important aspect: each of the buttons in the dialog does the intended thing if you tap it.
2ef257b
to
5761c46
Compare
5761c46
to
f2739dd
Compare
This pull request closes #996.
In
dialog.dart
:Switched
alertDialog(
toalertDialog.adaptive(
, as per #996.Defined new private widget
_adaptiveAction
which displays aCupertinoDialogAction
for IOS and aTextButton
otherwise._adaptiveAction(
is used in place ofTextButton(
when stating dialog actions.The new result for IOS:

dialog-checks.dart
has been updated to check the text content of the respective dialog types depending on the platform