Add "navigator" option to "showDialog" and "showGeneralDialog" (#42842)
diff --git a/packages/flutter/lib/src/cupertino/route.dart b/packages/flutter/lib/src/cupertino/route.dart
index b7038f0..3f12c12 100644
--- a/packages/flutter/lib/src/cupertino/route.dart
+++ b/packages/flutter/lib/src/cupertino/route.dart
@@ -864,6 +864,10 @@
/// It is only used when the method is called. Its corresponding widget can be
/// safely removed from the tree before the popup is closed.
///
+/// The `useRootNavigator` argument is used to determine whether to push the
+/// popup to the [Navigator] furthest from or nearest to the given `context`. It
+/// is `false` by default.
+///
/// The `builder` argument typically builds a [CupertinoActionSheet] widget.
/// Content below the widget is dimmed with a [ModalBarrier]. The widget built
/// by the `builder` does not share a context with the location that
@@ -882,8 +886,10 @@
Future<T> showCupertinoModalPopup<T>({
@required BuildContext context,
@required WidgetBuilder builder,
+ bool useRootNavigator = true,
}) {
- return Navigator.of(context, rootNavigator: true).push(
+ assert(useRootNavigator != null);
+ return Navigator.of(context, rootNavigator: useRootNavigator).push(
_CupertinoModalPopupRoute<T>(
builder: builder,
barrierLabel: 'Dismiss',
@@ -933,14 +939,18 @@
/// It is only used when the method is called. Its corresponding widget can
/// be safely removed from the tree before the dialog is closed.
///
-/// Returns a [Future] that resolves to the value (if any) that was passed to
-/// [Navigator.pop] when the dialog was closed.
+/// The `useRootNavigator` argument is used to determine whether to push the
+/// dialog to the [Navigator] furthest from or nearest to the given `context`.
+/// By default, `useRootNavigator` is `true` and the dialog route created by
+/// this method is pushed to the root navigator.
///
-/// The dialog route created by this method is pushed to the root navigator.
/// If the application has multiple [Navigator] objects, it may be necessary to
/// call `Navigator.of(context, rootNavigator: true).pop(result)` to close the
/// dialog rather than just `Navigator.pop(context, result)`.
///
+/// Returns a [Future] that resolves to the value (if any) that was passed to
+/// [Navigator.pop] when the dialog was closed.
+///
/// See also:
///
/// * [CupertinoDialog], an iOS-style dialog.
@@ -951,8 +961,10 @@
Future<T> showCupertinoDialog<T>({
@required BuildContext context,
@required WidgetBuilder builder,
+ bool useRootNavigator = true,
}) {
assert(builder != null);
+ assert(useRootNavigator != null);
return showGeneralDialog(
context: context,
barrierDismissible: false,
@@ -963,5 +975,6 @@
return builder(context);
},
transitionBuilder: _buildCupertinoDialogTransitions,
+ useRootNavigator: useRootNavigator,
);
}
diff --git a/packages/flutter/lib/src/material/about.dart b/packages/flutter/lib/src/material/about.dart
index 74b612b..4091177 100644
--- a/packages/flutter/lib/src/material/about.dart
+++ b/packages/flutter/lib/src/material/about.dart
@@ -223,8 +223,8 @@
/// The licenses shown on the [LicensePage] are those returned by the
/// [LicenseRegistry] API, which can be used to add more licenses to the list.
///
-/// The `context` argument is passed to [showDialog], the documentation for
-/// which discusses how it is used.
+/// The [context] and [useRootNavigator] arguments are passed to [showDialog],
+/// the documentation for which discusses how it is used.
void showAboutDialog({
@required BuildContext context,
String applicationName,
@@ -232,10 +232,13 @@
Widget applicationIcon,
String applicationLegalese,
List<Widget> children,
+ bool useRootNavigator = true,
}) {
assert(context != null);
+ assert(useRootNavigator != null);
showDialog<void>(
context: context,
+ useRootNavigator: useRootNavigator,
builder: (BuildContext context) {
return AboutDialog(
applicationName: applicationName,
@@ -251,7 +254,13 @@
/// Displays a [LicensePage], which shows licenses for software used by the
/// application.
///
-/// The arguments correspond to the properties on [LicensePage].
+/// The application arguments correspond to the properties on [LicensePage].
+///
+/// The `context` argument is used to look up the [Navigator] for the page.
+///
+/// The `useRootNavigator` argument is used to determine whether to push the
+/// page to the [Navigator] furthest from or nearest to the given `context`. It
+/// is `false` by default.
///
/// If the application has a [Drawer], consider using [AboutListTile] instead
/// of calling this directly.
@@ -267,9 +276,11 @@
String applicationVersion,
Widget applicationIcon,
String applicationLegalese,
+ bool useRootNavigator = false,
}) {
assert(context != null);
- Navigator.push(context, MaterialPageRoute<void>(
+ assert(useRootNavigator != null);
+ Navigator.of(context, rootNavigator: useRootNavigator).push(MaterialPageRoute<void>(
builder: (BuildContext context) => LicensePage(
applicationName: applicationName,
applicationVersion: applicationVersion,
diff --git a/packages/flutter/lib/src/material/date_picker.dart b/packages/flutter/lib/src/material/date_picker.dart
index 2633f09..32e45e6 100644
--- a/packages/flutter/lib/src/material/date_picker.dart
+++ b/packages/flutter/lib/src/material/date_picker.dart
@@ -1085,8 +1085,8 @@
/// provided by [Directionality]. If both [locale] and [textDirection] are not
/// null, [textDirection] overrides the direction chosen for the [locale].
///
-/// The [context] argument is passed to [showDialog], the documentation for
-/// which discusses how it is used.
+/// The [context] and [useRootNavigator] arguments are passed to [showDialog],
+/// the documentation for which discusses how it is used.
///
/// The [builder] parameter can be used to wrap the dialog widget
/// to add inherited widgets like [Theme].
@@ -1133,10 +1133,12 @@
Locale locale,
TextDirection textDirection,
TransitionBuilder builder,
+ bool useRootNavigator = true,
}) async {
assert(initialDate != null);
assert(firstDate != null);
assert(lastDate != null);
+ assert(useRootNavigator != null);
assert(!initialDate.isBefore(firstDate), 'initialDate must be on or after firstDate');
assert(!initialDate.isAfter(lastDate), 'initialDate must be on or before lastDate');
assert(!firstDate.isAfter(lastDate), 'lastDate must be on or after firstDate');
@@ -1173,6 +1175,7 @@
return await showDialog<DateTime>(
context: context,
+ useRootNavigator: useRootNavigator,
builder: (BuildContext context) {
return builder == null ? child : builder(context, child);
},
diff --git a/packages/flutter/lib/src/material/dialog.dart b/packages/flutter/lib/src/material/dialog.dart
index 1314cd0..de0c52a 100644
--- a/packages/flutter/lib/src/material/dialog.dart
+++ b/packages/flutter/lib/src/material/dialog.dart
@@ -655,20 +655,24 @@
/// `showDialog` is originally called from. Use a [StatefulBuilder] or a
/// custom [StatefulWidget] if the dialog needs to update dynamically.
///
+/// The `child` argument is deprecated, and should be replaced with `builder`.
+///
/// The `context` argument is used to look up the [Navigator] and [Theme] for
/// the dialog. It is only used when the method is called. Its corresponding
/// widget can be safely removed from the tree before the dialog is closed.
///
-/// The `child` argument is deprecated, and should be replaced with `builder`.
+/// The `useRootNavigator` argument is used to determine whether to push the
+/// dialog to the [Navigator] furthest from or nearest to the given `context`.
+/// By default, `useRootNavigator` is `true` and the dialog route created by
+/// this method is pushed to the root navigator.
///
-/// Returns a [Future] that resolves to the value (if any) that was passed to
-/// [Navigator.pop] when the dialog was closed.
-///
-/// The dialog route created by this method is pushed to the root navigator.
/// If the application has multiple [Navigator] objects, it may be necessary to
/// call `Navigator.of(context, rootNavigator: true).pop(result)` to close the
/// dialog rather than just `Navigator.pop(context, result)`.
///
+/// Returns a [Future] that resolves to the value (if any) that was passed to
+/// [Navigator.pop] when the dialog was closed.
+///
/// See also:
///
/// * [AlertDialog], for dialogs that have a row of buttons below a body.
@@ -687,8 +691,10 @@
'is appropriate for widgets built in the dialog.'
) Widget child,
WidgetBuilder builder,
+ bool useRootNavigator = true,
}) {
assert(child == null || builder == null);
+ assert(useRootNavigator != null);
assert(debugCheckHasMaterialLocalizations(context));
final ThemeData theme = Theme.of(context, shadowThemeOnly: true);
@@ -711,5 +717,6 @@
barrierColor: Colors.black54,
transitionDuration: const Duration(milliseconds: 150),
transitionBuilder: _buildMaterialDialogTransitions,
+ useRootNavigator: useRootNavigator,
);
}
diff --git a/packages/flutter/lib/src/material/popup_menu.dart b/packages/flutter/lib/src/material/popup_menu.dart
index d54195b..79107c4 100644
--- a/packages/flutter/lib/src/material/popup_menu.dart
+++ b/packages/flutter/lib/src/material/popup_menu.dart
@@ -790,6 +790,10 @@
/// the menu. It is only used when the method is called. Its corresponding
/// widget can be safely removed from the tree before the popup menu is closed.
///
+/// The `useRootNavigator` argument is used to determine whether to push the
+/// menu to the [Navigator] furthest from or nearest to the given `context`. It
+/// is `false` by default.
+///
/// The `semanticLabel` argument is used by accessibility frameworks to
/// announce screen transitions when the menu is opened and closed. If this
/// label is not provided, it will default to
@@ -814,9 +818,11 @@
ShapeBorder shape,
Color color,
bool captureInheritedThemes = true,
+ bool useRootNavigator = false,
}) {
assert(context != null);
assert(position != null);
+ assert(useRootNavigator != null);
assert(items != null && items.isNotEmpty);
assert(captureInheritedThemes != null);
assert(debugCheckHasMaterialLocalizations(context));
@@ -831,7 +837,7 @@
label = semanticLabel ?? MaterialLocalizations.of(context)?.popupMenuLabel;
}
- return Navigator.push(context, _PopupMenuRoute<T>(
+ return Navigator.of(context, rootNavigator: useRootNavigator).push(_PopupMenuRoute<T>(
position: position,
items: items,
initialValue: initialValue,
diff --git a/packages/flutter/lib/src/material/time_picker.dart b/packages/flutter/lib/src/material/time_picker.dart
index 5bad424..6ee0ef9 100644
--- a/packages/flutter/lib/src/material/time_picker.dart
+++ b/packages/flutter/lib/src/material/time_picker.dart
@@ -1731,8 +1731,8 @@
/// ```
/// {@end-tool}
///
-/// The [context] argument is passed to [showDialog], the documentation for
-/// which discusses how it is used.
+/// The [context] and [useRootNavigator] arguments are passed to [showDialog],
+/// the documentation for which discusses how it is used.
///
/// The [builder] parameter can be used to wrap the dialog widget
/// to add inherited widgets like [Localizations.override],
@@ -1780,14 +1780,17 @@
@required BuildContext context,
@required TimeOfDay initialTime,
TransitionBuilder builder,
+ bool useRootNavigator = true,
}) async {
assert(context != null);
assert(initialTime != null);
+ assert(useRootNavigator != null);
assert(debugCheckHasMaterialLocalizations(context));
final Widget dialog = _TimePickerDialog(initialTime: initialTime);
return await showDialog<TimeOfDay>(
context: context,
+ useRootNavigator: useRootNavigator,
builder: (BuildContext context) {
return builder == null ? dialog : builder(context, dialog);
},
diff --git a/packages/flutter/lib/src/widgets/routes.dart b/packages/flutter/lib/src/widgets/routes.dart
index 2fbf975..1d2f04b 100644
--- a/packages/flutter/lib/src/widgets/routes.dart
+++ b/packages/flutter/lib/src/widgets/routes.dart
@@ -1560,9 +1560,18 @@
/// [StatefulWidget] if the dialog needs to update dynamically. The
/// `pageBuilder` argument can not be null.
///
-/// The `context` argument is used to look up the [Navigator] for the dialog.
-/// It is only used when the method is called. Its corresponding widget can
-/// be safely removed from the tree before the dialog is closed.
+/// The `context` argument is used to look up the [Navigator] for the
+/// dialog. It is only used when the method is called. Its corresponding widget
+/// can be safely removed from the tree before the dialog is closed.
+///
+/// The `useRootNavigator` argument is used to determine whether to push the
+/// dialog to the [Navigator] furthest from or nearest to the given `context`.
+/// By default, `useRootNavigator` is `true` and the dialog route created by
+/// this method is pushed to the root navigator.
+///
+/// If the application has multiple [Navigator] objects, it may be necessary to
+/// call `Navigator.of(context, rootNavigator: true).pop(result)` to close the
+/// dialog rather than just `Navigator.pop(context, result)`.
///
/// The `barrierDismissible` argument is used to determine whether this route
/// can be dismissed by tapping the modal barrier. This argument defaults
@@ -1586,11 +1595,6 @@
/// Returns a [Future] that resolves to the value (if any) that was passed to
/// [Navigator.pop] when the dialog was closed.
///
-/// The dialog route created by this method is pushed to the root navigator.
-/// If the application has multiple [Navigator] objects, it may be necessary to
-/// call `Navigator.of(context, rootNavigator: true).pop(result)` to close the
-/// dialog rather than just `Navigator.pop(context, result)`.
-///
/// See also:
///
/// * [showDialog], which displays a Material-style dialog.
@@ -1603,10 +1607,12 @@
Color barrierColor,
Duration transitionDuration,
RouteTransitionsBuilder transitionBuilder,
+ bool useRootNavigator = true,
}) {
assert(pageBuilder != null);
+ assert(useRootNavigator != null);
assert(!barrierDismissible || barrierLabel != null);
- return Navigator.of(context, rootNavigator: true).push<T>(_DialogRoute<T>(
+ return Navigator.of(context, rootNavigator: useRootNavigator).push<T>(_DialogRoute<T>(
pageBuilder: pageBuilder,
barrierDismissible: barrierDismissible,
barrierLabel: barrierLabel,
diff --git a/packages/flutter/test/cupertino/route_test.dart b/packages/flutter/test/cupertino/route_test.dart
index cb02e9b..f33ef83 100644
--- a/packages/flutter/test/cupertino/route_test.dart
+++ b/packages/flutter/test/cupertino/route_test.dart
@@ -918,6 +918,164 @@
expect(homeTapCount, 1);
expect(pageTapCount, 1);
});
+
+ testWidgets('showCupertinoModalPopup uses root navigator by default', (WidgetTester tester) async {
+ final PopupObserver rootObserver = PopupObserver();
+ final PopupObserver nestedObserver = PopupObserver();
+
+ await tester.pumpWidget(CupertinoApp(
+ navigatorObservers: <NavigatorObserver>[rootObserver],
+ home: Navigator(
+ observers: <NavigatorObserver>[nestedObserver],
+ onGenerateRoute: (RouteSettings settings) {
+ return PageRouteBuilder<dynamic>(
+ pageBuilder: (BuildContext context, Animation<double> _, Animation<double> __) {
+ return GestureDetector(
+ onTap: () async {
+ await showCupertinoModalPopup<void>(
+ context: context,
+ builder: (BuildContext context) => const SizedBox(),
+ );
+ },
+ child: const Text('tap'),
+ );
+ },
+ );
+ },
+ ),
+ ));
+
+ // Open the dialog.
+ await tester.tap(find.text('tap'));
+
+ expect(rootObserver.popupCount, 1);
+ expect(nestedObserver.popupCount, 0);
+ });
+
+ testWidgets('showCupertinoModalPopup uses nested navigator if useRootNavigator is false', (WidgetTester tester) async {
+ final PopupObserver rootObserver = PopupObserver();
+ final PopupObserver nestedObserver = PopupObserver();
+
+ await tester.pumpWidget(CupertinoApp(
+ navigatorObservers: <NavigatorObserver>[rootObserver],
+ home: Navigator(
+ observers: <NavigatorObserver>[nestedObserver],
+ onGenerateRoute: (RouteSettings settings) {
+ return PageRouteBuilder<dynamic>(
+ pageBuilder: (BuildContext context, Animation<double> _, Animation<double> __) {
+ return GestureDetector(
+ onTap: () async {
+ await showCupertinoModalPopup<void>(
+ context: context,
+ useRootNavigator: false,
+ builder: (BuildContext context) => const SizedBox(),
+ );
+ },
+ child: const Text('tap'),
+ );
+ },
+ );
+ },
+ ),
+ ));
+
+ // Open the dialog.
+ await tester.tap(find.text('tap'));
+
+ expect(rootObserver.popupCount, 0);
+ expect(nestedObserver.popupCount, 1);
+ });
+
+ testWidgets('showCupertinoDialog uses root navigator by default', (WidgetTester tester) async {
+ final DialogObserver rootObserver = DialogObserver();
+ final DialogObserver nestedObserver = DialogObserver();
+
+ await tester.pumpWidget(CupertinoApp(
+ navigatorObservers: <NavigatorObserver>[rootObserver],
+ home: Navigator(
+ observers: <NavigatorObserver>[nestedObserver],
+ onGenerateRoute: (RouteSettings settings) {
+ return PageRouteBuilder<dynamic>(
+ pageBuilder: (BuildContext context, Animation<double> _, Animation<double> __) {
+ return GestureDetector(
+ onTap: () async {
+ await showCupertinoDialog<void>(
+ context: context,
+ builder: (BuildContext context) => const SizedBox(),
+ );
+ },
+ child: const Text('tap'),
+ );
+ },
+ );
+ },
+ ),
+ ));
+
+ // Open the dialog.
+ await tester.tap(find.text('tap'));
+
+ expect(rootObserver.dialogCount, 1);
+ expect(nestedObserver.dialogCount, 0);
+ });
+
+ testWidgets('showCupertinoDialog uses nested navigator if useRootNavigator is false', (WidgetTester tester) async {
+ final DialogObserver rootObserver = DialogObserver();
+ final DialogObserver nestedObserver = DialogObserver();
+
+ await tester.pumpWidget(CupertinoApp(
+ navigatorObservers: <NavigatorObserver>[rootObserver],
+ home: Navigator(
+ observers: <NavigatorObserver>[nestedObserver],
+ onGenerateRoute: (RouteSettings settings) {
+ return PageRouteBuilder<dynamic>(
+ pageBuilder: (BuildContext context, Animation<double> _, Animation<double> __) {
+ return GestureDetector(
+ onTap: () async {
+ await showCupertinoDialog<void>(
+ context: context,
+ useRootNavigator: false,
+ builder: (BuildContext context) => const SizedBox(),
+ );
+ },
+ child: const Text('tap'),
+ );
+ },
+ );
+ },
+ ),
+ ));
+
+ // Open the dialog.
+ await tester.tap(find.text('tap'));
+
+ expect(rootObserver.dialogCount, 0);
+ expect(nestedObserver.dialogCount, 1);
+ });
}
class MockNavigatorObserver extends Mock implements NavigatorObserver {}
+
+class PopupObserver extends NavigatorObserver {
+ int popupCount = 0;
+
+ @override
+ void didPush(Route<dynamic> route, Route<dynamic> previousRoute) {
+ if (route.toString().contains('_CupertinoModalPopupRoute')) {
+ popupCount++;
+ }
+ super.didPush(route, previousRoute);
+ }
+}
+
+class DialogObserver extends NavigatorObserver {
+ int dialogCount = 0;
+
+ @override
+ void didPush(Route<dynamic> route, Route<dynamic> previousRoute) {
+ if (route.toString().contains('_DialogRoute')) {
+ dialogCount++;
+ }
+ super.didPush(route, previousRoute);
+ }
+}
diff --git a/packages/flutter/test/material/about_test.dart b/packages/flutter/test/material/about_test.dart
index 5c716ea..976c2c4 100644
--- a/packages/flutter/test/material/about_test.dart
+++ b/packages/flutter/test/material/about_test.dart
@@ -305,6 +305,150 @@
tileRect = tester.getRect(find.byType(AboutListTile));
expect(tileRect.height, 48.0);
});
+
+ testWidgets('showLicensePage uses nested navigator by default', (WidgetTester tester) async {
+ final LicensePageObserver rootObserver = LicensePageObserver();
+ final LicensePageObserver nestedObserver = LicensePageObserver();
+
+ await tester.pumpWidget(MaterialApp(
+ navigatorObservers: <NavigatorObserver>[rootObserver],
+ initialRoute: '/',
+ onGenerateRoute: (_) {
+ return PageRouteBuilder<dynamic>(
+ pageBuilder: (_, __, ___) => Navigator(
+ observers: <NavigatorObserver>[nestedObserver],
+ onGenerateRoute: (RouteSettings settings) {
+ return PageRouteBuilder<dynamic>(
+ pageBuilder: (BuildContext context, _, __) {
+ return RaisedButton(
+ onPressed: () {
+ showLicensePage(
+ context: context,
+ applicationName: 'A',
+ );
+ },
+ child: const Text('Show License Page'),
+ );
+ },
+ );
+ },
+ ),
+ );
+ },
+ ));
+
+ // Open the dialog.
+ await tester.tap(find.byType(RaisedButton));
+
+ expect(rootObserver.licensePageCount, 0);
+ expect(nestedObserver.licensePageCount, 1);
+ });
+
+ testWidgets('showLicensePage uses root navigator if useRootNavigator is true', (WidgetTester tester) async {
+ final LicensePageObserver rootObserver = LicensePageObserver();
+ final LicensePageObserver nestedObserver = LicensePageObserver();
+
+ await tester.pumpWidget(MaterialApp(
+ navigatorObservers: <NavigatorObserver>[rootObserver],
+ initialRoute: '/',
+ onGenerateRoute: (_) {
+ return PageRouteBuilder<dynamic>(
+ pageBuilder: (_, __, ___) => Navigator(
+ observers: <NavigatorObserver>[nestedObserver],
+ onGenerateRoute: (RouteSettings settings) {
+ return PageRouteBuilder<dynamic>(
+ pageBuilder: (BuildContext context, _, __) {
+ return RaisedButton(
+ onPressed: () {
+ showLicensePage(
+ context: context,
+ useRootNavigator: true,
+ applicationName: 'A',
+ );
+ },
+ child: const Text('Show License Page'),
+ );
+ },
+ );
+ },
+ ),
+ );
+ },
+ ));
+
+ // Open the dialog.
+ await tester.tap(find.byType(RaisedButton));
+
+ expect(rootObserver.licensePageCount, 1);
+ expect(nestedObserver.licensePageCount, 0);
+ });
+
+ testWidgets('showAboutDialog uses root navigator by default', (WidgetTester tester) async {
+ final AboutDialogObserver rootObserver = AboutDialogObserver();
+ final AboutDialogObserver nestedObserver = AboutDialogObserver();
+
+ await tester.pumpWidget(MaterialApp(
+ navigatorObservers: <NavigatorObserver>[rootObserver],
+ home: Navigator(
+ observers: <NavigatorObserver>[nestedObserver],
+ onGenerateRoute: (RouteSettings settings) {
+ return MaterialPageRoute<dynamic>(
+ builder: (BuildContext context) {
+ return RaisedButton(
+ onPressed: () {
+ showAboutDialog(
+ context: context,
+ applicationName: 'A',
+ );
+ },
+ child: const Text('Show About Dialog'),
+ );
+ },
+ );
+ },
+ ),
+ ));
+
+ // Open the dialog.
+ await tester.tap(find.byType(RaisedButton));
+
+ expect(rootObserver.dialogCount, 1);
+ expect(nestedObserver.dialogCount, 0);
+ });
+
+ testWidgets('showAboutDialog uses nested navigator if useRootNavigator is false', (WidgetTester tester) async {
+ final AboutDialogObserver rootObserver = AboutDialogObserver();
+ final AboutDialogObserver nestedObserver = AboutDialogObserver();
+
+ await tester.pumpWidget(MaterialApp(
+ navigatorObservers: <NavigatorObserver>[rootObserver],
+ home: Navigator(
+ observers: <NavigatorObserver>[nestedObserver],
+ onGenerateRoute: (RouteSettings settings) {
+ return MaterialPageRoute<dynamic>(
+ builder: (BuildContext context) {
+ return RaisedButton(
+ onPressed: () {
+ showAboutDialog(
+ context: context,
+ useRootNavigator: false,
+ applicationName: 'A',
+ );
+ },
+ child: const Text('Show About Dialog'),
+ );
+ },
+ );
+ },
+ ),
+ ));
+
+ // Open the dialog.
+ await tester.tap(find.byType(RaisedButton));
+
+ expect(rootObserver.dialogCount, 0);
+ expect(nestedObserver.dialogCount, 1);
+ });
}
class FakeLicenseEntry extends LicenseEntry {
@@ -322,3 +466,27 @@
return <LicenseParagraph>[];
}
}
+
+class LicensePageObserver extends NavigatorObserver {
+ int licensePageCount = 0;
+
+ @override
+ void didPush(Route<dynamic> route, Route<dynamic> previousRoute) {
+ if (route is MaterialPageRoute<dynamic>) {
+ licensePageCount++;
+ }
+ super.didPush(route, previousRoute);
+ }
+}
+
+class AboutDialogObserver extends NavigatorObserver {
+ int dialogCount = 0;
+
+ @override
+ void didPush(Route<dynamic> route, Route<dynamic> previousRoute) {
+ if (route.toString().contains('_DialogRoute')) {
+ dialogCount++;
+ }
+ super.didPush(route, previousRoute);
+ }
+}
diff --git a/packages/flutter/test/material/date_picker_test.dart b/packages/flutter/test/material/date_picker_test.dart
index cf05624..a1303a4 100644
--- a/packages/flutter/test/material/date_picker_test.dart
+++ b/packages/flutter/test/material/date_picker_test.dart
@@ -897,4 +897,90 @@
});
});
+ testWidgets('uses root navigator by default', (WidgetTester tester) async {
+ final DatePickerObserver rootObserver = DatePickerObserver();
+ final DatePickerObserver nestedObserver = DatePickerObserver();
+
+ await tester.pumpWidget(MaterialApp(
+ navigatorObservers: <NavigatorObserver>[rootObserver],
+ home: Navigator(
+ observers: <NavigatorObserver>[nestedObserver],
+ onGenerateRoute: (RouteSettings settings) {
+ return MaterialPageRoute<dynamic>(
+ builder: (BuildContext context) {
+ return RaisedButton(
+ onPressed: () {
+ showDatePicker(
+ context: context,
+ initialDate: DateTime.now(),
+ firstDate: DateTime(2018),
+ lastDate: DateTime(2030),
+ builder: (BuildContext context, Widget child) {
+ return const SizedBox();
+ },
+ );
+ },
+ child: const Text('Show Date Picker'),
+ );
+ },
+ );
+ },
+ ),
+ ));
+
+ // Open the dialog.
+ await tester.tap(find.byType(RaisedButton));
+
+ expect(rootObserver.datePickerCount, 1);
+ expect(nestedObserver.datePickerCount, 0);
+ });
+
+ testWidgets('uses nested navigator if useRootNavigator is false', (WidgetTester tester) async {
+ final DatePickerObserver rootObserver = DatePickerObserver();
+ final DatePickerObserver nestedObserver = DatePickerObserver();
+
+ await tester.pumpWidget(MaterialApp(
+ navigatorObservers: <NavigatorObserver>[rootObserver],
+ home: Navigator(
+ observers: <NavigatorObserver>[nestedObserver],
+ onGenerateRoute: (RouteSettings settings) {
+ return MaterialPageRoute<dynamic>(
+ builder: (BuildContext context) {
+ return RaisedButton(
+ onPressed: () {
+ showDatePicker(
+ context: context,
+ useRootNavigator: false,
+ initialDate: DateTime.now(),
+ firstDate: DateTime(2018),
+ lastDate: DateTime(2030),
+ builder: (BuildContext context, Widget child) => const SizedBox(),
+ );
+ },
+ child: const Text('Show Date Picker'),
+ );
+ },
+ );
+ },
+ ),
+ ));
+
+ // Open the dialog.
+ await tester.tap(find.byType(RaisedButton));
+
+ expect(rootObserver.datePickerCount, 0);
+ expect(nestedObserver.datePickerCount, 1);
+ });
+}
+
+class DatePickerObserver extends NavigatorObserver {
+ int datePickerCount = 0;
+
+ @override
+ void didPush(Route<dynamic> route, Route<dynamic> previousRoute) {
+ if (route.toString().contains('_DialogRoute')) {
+ datePickerCount++;
+ }
+ super.didPush(route, previousRoute);
+ }
}
diff --git a/packages/flutter/test/material/dialog_test.dart b/packages/flutter/test/material/dialog_test.dart
index 0bceb81..0143eaa 100644
--- a/packages/flutter/test/material/dialog_test.dart
+++ b/packages/flutter/test/material/dialog_test.dart
@@ -646,6 +646,77 @@
await tester.pump();
});
+ testWidgets('showDialog uses root navigator by default', (WidgetTester tester) async {
+ final DialogObserver rootObserver = DialogObserver();
+ final DialogObserver nestedObserver = DialogObserver();
+
+ await tester.pumpWidget(MaterialApp(
+ navigatorObservers: <NavigatorObserver>[rootObserver],
+ home: Navigator(
+ observers: <NavigatorObserver>[nestedObserver],
+ onGenerateRoute: (RouteSettings settings) {
+ return MaterialPageRoute<dynamic>(
+ builder: (BuildContext context) {
+ return RaisedButton(
+ onPressed: () {
+ showDialog<void>(
+ context: context,
+ builder: (BuildContext innerContext) {
+ return const AlertDialog(title: Text('Title'));
+ },
+ );
+ },
+ child: const Text('Show Dialog'),
+ );
+ },
+ );
+ },
+ ),
+ ));
+
+ // Open the dialog.
+ await tester.tap(find.byType(RaisedButton));
+
+ expect(rootObserver.dialogCount, 1);
+ expect(nestedObserver.dialogCount, 0);
+ });
+
+ testWidgets('showDialog uses nested navigator if useRootNavigator is false', (WidgetTester tester) async {
+ final DialogObserver rootObserver = DialogObserver();
+ final DialogObserver nestedObserver = DialogObserver();
+
+ await tester.pumpWidget(MaterialApp(
+ navigatorObservers: <NavigatorObserver>[rootObserver],
+ home: Navigator(
+ observers: <NavigatorObserver>[nestedObserver],
+ onGenerateRoute: (RouteSettings settings) {
+ return MaterialPageRoute<dynamic>(
+ builder: (BuildContext context) {
+ return RaisedButton(
+ onPressed: () {
+ showDialog<void>(
+ context: context,
+ useRootNavigator: false,
+ builder: (BuildContext innerContext) {
+ return const AlertDialog(title: Text('Title'));
+ },
+ );
+ },
+ child: const Text('Show Dialog'),
+ );
+ },
+ );
+ },
+ ),
+ ));
+
+ // Open the dialog.
+ await tester.tap(find.byType(RaisedButton));
+
+ expect(rootObserver.dialogCount, 0);
+ expect(nestedObserver.dialogCount, 1);
+ });
+
group('Scrollable title and content', () {
testWidgets('Title is scrollable', (WidgetTester tester) async {
final Key titleKey = UniqueKey();
@@ -723,3 +794,15 @@
});
});
}
+
+class DialogObserver extends NavigatorObserver {
+ int dialogCount = 0;
+
+ @override
+ void didPush(Route<dynamic> route, Route<dynamic> previousRoute) {
+ if (route.toString().contains('_DialogRoute')) {
+ dialogCount++;
+ }
+ super.didPush(route, previousRoute);
+ }
+}
diff --git a/packages/flutter/test/material/popup_menu_test.dart b/packages/flutter/test/material/popup_menu_test.dart
index 791cb52..44ca6ea 100644
--- a/packages/flutter/test/material/popup_menu_test.dart
+++ b/packages/flutter/test/material/popup_menu_test.dart
@@ -1114,6 +1114,83 @@
expect(find.text('PopupMenuButton icon'), findsOneWidget);
});
+
+ testWidgets('showMenu uses nested navigator by default', (WidgetTester tester) async {
+ final MenuObserver rootObserver = MenuObserver();
+ final MenuObserver nestedObserver = MenuObserver();
+
+ await tester.pumpWidget(MaterialApp(
+ navigatorObservers: <NavigatorObserver>[rootObserver],
+ home: Navigator(
+ observers: <NavigatorObserver>[nestedObserver],
+ onGenerateRoute: (RouteSettings settings) {
+ return MaterialPageRoute<dynamic>(
+ builder: (BuildContext context) {
+ return RaisedButton(
+ onPressed: () {
+ showMenu<int>(
+ context: context,
+ position: const RelativeRect.fromLTRB(0, 0, 0, 0),
+ items: <PopupMenuItem<int>>[
+ const PopupMenuItem<int>(
+ value: 1, child: Text('1'),
+ ),
+ ],
+ );
+ },
+ child: const Text('Show Menu'),
+ );
+ },
+ );
+ },
+ ),
+ ));
+
+ // Open the dialog.
+ await tester.tap(find.byType(RaisedButton));
+
+ expect(rootObserver.menuCount, 0);
+ expect(nestedObserver.menuCount, 1);
+ });
+
+ testWidgets('showMenu uses root navigator if useRootNavigator is true', (WidgetTester tester) async {
+ final MenuObserver rootObserver = MenuObserver();
+ final MenuObserver nestedObserver = MenuObserver();
+
+ await tester.pumpWidget(MaterialApp(
+ navigatorObservers: <NavigatorObserver>[rootObserver],
+ home: Navigator(
+ observers: <NavigatorObserver>[nestedObserver],
+ onGenerateRoute: (RouteSettings settings) {
+ return MaterialPageRoute<dynamic>(
+ builder: (BuildContext context) {
+ return RaisedButton(
+ onPressed: () {
+ showMenu<int>(
+ context: context,
+ useRootNavigator: true,
+ position: const RelativeRect.fromLTRB(0, 0, 0, 0),
+ items: <PopupMenuItem<int>>[
+ const PopupMenuItem<int>(
+ value: 1, child: Text('1'),
+ ),
+ ],
+ );
+ },
+ child: const Text('Show Menu'),
+ );
+ },
+ );
+ },
+ ),
+ ));
+
+ // Open the dialog.
+ await tester.tap(find.byType(RaisedButton));
+
+ expect(rootObserver.menuCount, 1);
+ expect(nestedObserver.menuCount, 0);
+ });
}
class TestApp extends StatefulWidget {
@@ -1153,3 +1230,15 @@
);
}
}
+
+class MenuObserver extends NavigatorObserver {
+ int menuCount = 0;
+
+ @override
+ void didPush(Route<dynamic> route, Route<dynamic> previousRoute) {
+ if (route.toString().contains('_PopupMenuRoute')) {
+ menuCount++;
+ }
+ super.didPush(route, previousRoute);
+ }
+}
diff --git a/packages/flutter/test/material/time_picker_test.dart b/packages/flutter/test/material/time_picker_test.dart
index 8cacc1e..134ebf7 100644
--- a/packages/flutter/test/material/time_picker_test.dart
+++ b/packages/flutter/test/material/time_picker_test.dart
@@ -622,6 +622,73 @@
// button and the right edge of the 800 wide window.
expect(tester.getBottomLeft(find.text('OK')).dx, 800 - ltrOkRight);
});
+
+ testWidgets('uses root navigator by default', (WidgetTester tester) async {
+ final PickerObserver rootObserver = PickerObserver();
+ final PickerObserver nestedObserver = PickerObserver();
+
+ await tester.pumpWidget(MaterialApp(
+ navigatorObservers: <NavigatorObserver>[rootObserver],
+ home: Navigator(
+ observers: <NavigatorObserver>[nestedObserver],
+ onGenerateRoute: (RouteSettings settings) {
+ return MaterialPageRoute<dynamic>(
+ builder: (BuildContext context) {
+ return RaisedButton(
+ onPressed: () {
+ showTimePicker(
+ context: context,
+ initialTime: const TimeOfDay(hour: 7, minute: 0),
+ );
+ },
+ child: const Text('Show Picker'),
+ );
+ },
+ );
+ },
+ ),
+ ));
+
+ // Open the dialog.
+ await tester.tap(find.byType(RaisedButton));
+
+ expect(rootObserver.pickerCount, 1);
+ expect(nestedObserver.pickerCount, 0);
+ });
+
+ testWidgets('uses nested navigator if useRootNavigator is false', (WidgetTester tester) async {
+ final PickerObserver rootObserver = PickerObserver();
+ final PickerObserver nestedObserver = PickerObserver();
+
+ await tester.pumpWidget(MaterialApp(
+ navigatorObservers: <NavigatorObserver>[rootObserver],
+ home: Navigator(
+ observers: <NavigatorObserver>[nestedObserver],
+ onGenerateRoute: (RouteSettings settings) {
+ return MaterialPageRoute<dynamic>(
+ builder: (BuildContext context) {
+ return RaisedButton(
+ onPressed: () {
+ showTimePicker(
+ context: context,
+ useRootNavigator: false,
+ initialTime: const TimeOfDay(hour: 7, minute: 0),
+ );
+ },
+ child: const Text('Show Picker'),
+ );
+ },
+ );
+ },
+ ),
+ ));
+
+ // Open the dialog.
+ await tester.tap(find.byType(RaisedButton));
+
+ expect(rootObserver.pickerCount, 0);
+ expect(nestedObserver.pickerCount, 1);
+ });
}
final Finder findDialPaint = find.descendant(
@@ -695,3 +762,15 @@
expect(tester.renderObject(findDialPaint), expectedLabels);
}
}
+
+class PickerObserver extends NavigatorObserver {
+ int pickerCount = 0;
+
+ @override
+ void didPush(Route<dynamic> route, Route<dynamic> previousRoute) {
+ if (route.toString().contains('_DialogRoute')) {
+ pickerCount++;
+ }
+ super.didPush(route, previousRoute);
+ }
+}
diff --git a/packages/flutter/test/widgets/routes_test.dart b/packages/flutter/test/widgets/routes_test.dart
index 2b0decf..26b6542 100644
--- a/packages/flutter/test/widgets/routes_test.dart
+++ b/packages/flutter/test/widgets/routes_test.dart
@@ -788,6 +788,81 @@
expect(secondaryAnimationPageOne.parent, kAlwaysDismissedAnimation);
expect(trainHopper2.currentTrain, isNull); // Has been disposed.
});
+
+ testWidgets('showGeneralDialog uses root navigator by default', (WidgetTester tester) async {
+ final DialogObserver rootObserver = DialogObserver();
+ final DialogObserver nestedObserver = DialogObserver();
+
+ await tester.pumpWidget(MaterialApp(
+ navigatorObservers: <NavigatorObserver>[rootObserver],
+ home: Navigator(
+ observers: <NavigatorObserver>[nestedObserver],
+ onGenerateRoute: (RouteSettings settings) {
+ return MaterialPageRoute<dynamic>(
+ builder: (BuildContext context) {
+ return RaisedButton(
+ onPressed: () {
+ showGeneralDialog<void>(
+ context: context,
+ barrierDismissible: false,
+ transitionDuration: Duration.zero,
+ pageBuilder: (BuildContext innerContext, _, __) {
+ return const SizedBox();
+ },
+ );
+ },
+ child: const Text('Show Dialog'),
+ );
+ },
+ );
+ },
+ ),
+ ));
+
+ // Open the dialog.
+ await tester.tap(find.byType(RaisedButton));
+
+ expect(rootObserver.dialogCount, 1);
+ expect(nestedObserver.dialogCount, 0);
+ });
+
+ testWidgets('showGeneralDialog uses nested navigator if useRootNavigator is false', (WidgetTester tester) async {
+ final DialogObserver rootObserver = DialogObserver();
+ final DialogObserver nestedObserver = DialogObserver();
+
+ await tester.pumpWidget(MaterialApp(
+ navigatorObservers: <NavigatorObserver>[rootObserver],
+ home: Navigator(
+ observers: <NavigatorObserver>[nestedObserver],
+ onGenerateRoute: (RouteSettings settings) {
+ return MaterialPageRoute<dynamic>(
+ builder: (BuildContext context) {
+ return RaisedButton(
+ onPressed: () {
+ showGeneralDialog<void>(
+ useRootNavigator: false,
+ context: context,
+ barrierDismissible: false,
+ transitionDuration: Duration.zero,
+ pageBuilder: (BuildContext innerContext, _, __) {
+ return const SizedBox();
+ },
+ );
+ },
+ child: const Text('Show Dialog'),
+ );
+ },
+ );
+ },
+ ),
+ ));
+
+ // Open the dialog.
+ await tester.tap(find.byType(RaisedButton));
+
+ expect(rootObserver.dialogCount, 0);
+ expect(nestedObserver.dialogCount, 1);
+ });
});
}
@@ -805,3 +880,15 @@
return CurvedAnimation(parent: super.createAnimation(), curve: Curves.easeOutExpo);
}
}
+
+class DialogObserver extends NavigatorObserver {
+ int dialogCount = 0;
+
+ @override
+ void didPush(Route<dynamic> route, Route<dynamic> previousRoute) {
+ if (route.toString().contains('_DialogRoute')) {
+ dialogCount++;
+ }
+ super.didPush(route, previousRoute);
+ }
+}