-
Notifications
You must be signed in to change notification settings - Fork 30.4k
Improve error message for type mismatch in Navigator.pop and maybePop. #186571
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
base: master
Are you sure you want to change the base?
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change | ||||
|---|---|---|---|---|---|---|
|
|
@@ -641,6 +641,28 @@ abstract class Route<T> extends _RoutePlaceholder { | |||||
| return _navigator?._firstRouteEntryWhereOrNull(_RouteEntry.isRoutePredicate(this))?.isPresent ?? | ||||||
| false; | ||||||
| } | ||||||
|
|
||||||
| /// Asserts that the given result is of a type that can be consumed by this route. | ||||||
| /// | ||||||
| /// This is used by [Navigator] to provide a clear error message when route is popped with mismatched result type. | ||||||
| bool _debugCheckCanConsumeResult(dynamic result, {required String methodName}) { | ||||||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
Suggested change
|
||||||
| if (result is! T?) { | ||||||
| throw FlutterError.fromParts(<DiagnosticsNode>[ | ||||||
| ErrorSummary( | ||||||
| 'A request was made to pop a route with a result of type ${result.runtimeType}, but the route expected a value of type $T.', | ||||||
| ), | ||||||
| ErrorDescription( | ||||||
| 'This usually happens when the type provided to Navigator.$methodName() ' | ||||||
| 'is not a subtype of the type expected by the Route (e.g. DialogRoute<Null>), ' | ||||||
| 'or when a generic type is explicitly provided to a route creation method ' | ||||||
| '(such as showDialog<T>()) but the popped value does not match this type.', | ||||||
| ), | ||||||
| DiagnosticsProperty<Route<dynamic>>('The route was', this), | ||||||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
Suggested change
|
||||||
| DiagnosticsProperty<Object?>('The provided result was', result), | ||||||
| ]); | ||||||
| } | ||||||
| return true; | ||||||
| } | ||||||
| } | ||||||
|
|
||||||
| /// Data that might be useful in constructing a [Route]. | ||||||
|
|
@@ -5565,7 +5587,7 @@ class NavigatorState extends State<Navigator> with TickerProviderStateMixin, Res | |||||
| return false; | ||||||
| } | ||||||
| assert(lastEntry.route._isInstalledIn(this)); | ||||||
|
|
||||||
| assert(lastEntry.route._debugCheckCanConsumeResult(result, methodName: 'maybePop')); | ||||||
| // TODO(justinmc): When the deprecated willPop method is removed, delete | ||||||
| // this code and use only popDisposition, below. | ||||||
| if (await lastEntry.route.willPop() == RoutePopDisposition.doNotPop) { | ||||||
|
|
@@ -5621,6 +5643,10 @@ class NavigatorState extends State<Navigator> with TickerProviderStateMixin, Res | |||||
| @optionalTypeArgs | ||||||
| void pop<T extends Object?>([T? result]) { | ||||||
| assert(!_debugLocked); | ||||||
| assert(() { | ||||||
| final _RouteEntry entry = _history.lastWhere(_RouteEntry.isPresentPredicate); | ||||||
| return entry.route._debugCheckCanConsumeResult(result, methodName: 'pop'); | ||||||
| }()); | ||||||
|
Comment on lines
+5646
to
+5649
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This assertion block performs a redundant search of the navigation history using assert(() {
final _RouteEntry? entry = _lastRouteEntryWhereOrNull(_RouteEntry.isPresentPredicate);
return entry?.route._debugCheckCanConsumeResult(result, methodName: 'pop') ?? true;
}());
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I think I agree with Gemini here. |
||||||
| assert(() { | ||||||
| _debugLocked = true; | ||||||
| return true; | ||||||
|
|
||||||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -6342,6 +6342,105 @@ void main() { | |
| }, | ||
| variant: TargetPlatformVariant.only(TargetPlatform.iOS), | ||
| ); | ||
|
|
||
| testWidgets( | ||
| 'Navigator.pop and maybePop throw Flutter error when popped with mismatched type via showDialog', | ||
| experimentalLeakTesting: LeakTesting.settings.withIgnoredAll(), | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Why does this need a leak tracking override? |
||
| (WidgetTester tester) async { | ||
| Object? popException; | ||
| Object? maybePopException; | ||
|
|
||
| await tester.pumpWidget( | ||
| MaterialApp( | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Don't use Material impoorts in test/widgets You can use TestWidgetsApp, TestButton and modal routes directly for this test I think. |
||
| home: Scaffold( | ||
| body: Builder( | ||
| builder: (BuildContext context) { | ||
| return ElevatedButton( | ||
| onPressed: () { | ||
| showDialog<bool>( | ||
| context: context, | ||
| builder: (BuildContext context) { | ||
| return AlertDialog( | ||
| actions: <Widget>[ | ||
| TextButton( | ||
| onPressed: () { | ||
| try { | ||
| Navigator.pop(context, 'NO'); | ||
| } catch (e) { | ||
| popException = e; | ||
| } | ||
| }, | ||
| child: const Text('NO'), | ||
| ), | ||
| TextButton( | ||
| onPressed: () { | ||
| Navigator.maybePop(context, 'YES').catchError((Object e) { | ||
| maybePopException = e; | ||
| return false; | ||
| }); | ||
| }, | ||
| child: const Text('YES'), | ||
| ), | ||
| ], | ||
| ); | ||
| }, | ||
| ); | ||
| }, | ||
| child: const Text('Open Dialog'), | ||
| ); | ||
| }, | ||
| ), | ||
| ), | ||
| ), | ||
| ); | ||
| await tester.tap(find.text('Open Dialog')); | ||
| await tester.pumpAndSettle(); | ||
| await tester.tap(find.text('NO')); | ||
| expect(popException, isFlutterError); | ||
| final popError = popException! as FlutterError; | ||
| expect( | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Should this be split into a test for pop and maybePop separately? |
||
| popError.toStringDeep(), | ||
| equalsIgnoringHashCodes( | ||
| 'FlutterError\n' | ||
| ' A request was made to pop a route with a result of type String,\n' | ||
| ' but the route expected a value of type bool.\n' | ||
| ' This usually happens when the type provided to Navigator.pop() is\n' | ||
| ' not a subtype of the type expected by the Route (e.g.\n' | ||
| ' DialogRoute<Null>), or when a generic type is explicitly provided\n' | ||
| ' to a route creation method (such as showDialog<T>()) but the\n' | ||
| ' popped value does not match this type.\n' | ||
| ' The route was: DialogRoute<bool>(RouteSettings(none, null),\n' | ||
| ' animation: AnimationController#00000(⏭ 1.000; paused; for\n' | ||
| ' DialogRoute<bool>))\n' | ||
| ' The provided result was: NO\n' | ||
| '', | ||
| ), | ||
| ); | ||
|
|
||
| await tester.tap(find.text('YES')); | ||
| await tester.pumpAndSettle(); | ||
| expect(maybePopException, isFlutterError); | ||
| final maybePopError = maybePopException! as FlutterError; | ||
| expect( | ||
| maybePopError.toStringDeep(), | ||
| equalsIgnoringHashCodes( | ||
| 'FlutterError\n' | ||
| ' A request was made to pop a route with a result of type String,\n' | ||
| ' but the route expected a value of type bool.\n' | ||
| ' This usually happens when the type provided to\n' | ||
| ' Navigator.maybePop() is not a subtype of the type expected by the\n' | ||
| ' Route (e.g. DialogRoute<Null>), or when a generic type is\n' | ||
| ' explicitly provided to a route creation method (such as\n' | ||
| ' showDialog<T>()) but the popped value does not match this type.\n' | ||
| ' The route was: DialogRoute<bool>(RouteSettings(none, null),\n' | ||
| ' animation: AnimationController#00000(⏭ 1.000; paused; for\n' | ||
| ' DialogRoute<bool>))\n' | ||
| ' The provided result was: YES\n' | ||
| '', | ||
| ), | ||
| ); | ||
| }, | ||
| ); | ||
| } | ||
|
|
||
| typedef AnnouncementCallBack = void Function(Route<dynamic>?); | ||
|
|
||
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.
Good call putting all of this in a _debug* method.