From aba0379dccefdd93cc69dee328440f739c01a038 Mon Sep 17 00:00:00 2001 From: Ian Hickson Date: Thu, 22 Mar 2018 13:21:07 -0700 Subject: [PATCH] Clean up the existing Navigator API. (#15718) This is not a grand refactor yet, it's just cleaning up what we have already, so that people who keep using this API (e.g. dialogs) have something coherent to deal with. The major changes are that Navigator and NavigatorState have the same API now, that most of the examples use `` instead of ``, that the navigator observer can see replaces, and that the `settings` is moved from ModalRoute to Route. I also cleaned up some of the API documentation. --- dev/manual_tests/lib/text.dart | 12 +- .../lib/demo/animation/home.dart | 2 +- .../demo/cupertino/cupertino_dialog_demo.dart | 2 +- .../cupertino/cupertino_navigation_demo.dart | 4 +- .../demo/cupertino/cupertino_picker_demo.dart | 2 +- .../lib/demo/material/dialog_demo.dart | 2 +- .../lib/demo/material/drawer_demo.dart | 6 +- .../lib/demo/material/grid_list_demo.dart | 2 +- .../material/modal_bottom_sheet_demo.dart | 2 +- .../persistent_bottom_sheet_demo.dart | 2 +- .../flutter_gallery/lib/demo/pesto_demo.dart | 4 +- .../lib/demo/shrine/shrine_order.dart | 4 +- .../lib/demo/shrine/shrine_page.dart | 2 +- .../flutter_gallery/lib/demo/shrine_demo.dart | 2 +- .../flutter_gallery/lib/demo/video_demo.dart | 4 +- examples/flutter_gallery/lib/gallery/app.dart | 2 +- .../flutter_gallery/lib/gallery/updates.dart | 29 +- examples/stocks/lib/main.dart | 4 +- examples/stocks/lib/stock_home.dart | 4 +- .../flutter/lib/src/cupertino/nav_bar.dart | 2 +- packages/flutter/lib/src/cupertino/route.dart | 35 +- packages/flutter/lib/src/material/about.dart | 8 +- packages/flutter/lib/src/material/app.dart | 5 +- .../flutter/lib/src/material/back_button.dart | 4 +- .../lib/src/material/bottom_sheet.dart | 9 +- packages/flutter/lib/src/material/dialog.dart | 4 +- .../flutter/lib/src/material/dropdown.dart | 2 +- packages/flutter/lib/src/material/page.dart | 28 +- .../flutter/lib/src/material/popup_menu.dart | 43 +- .../flutter/lib/src/semantics/semantics.dart | 3 + .../flutter/lib/src/widgets/navigator.dart | 1412 +++++++++++------ packages/flutter/lib/src/widgets/pages.dart | 4 +- packages/flutter/lib/src/widgets/routes.dart | 319 ++-- .../flutter/test/cupertino/dialog_test.dart | 4 +- .../flutter/test/cupertino/nav_bar_test.dart | 28 +- .../flutter/test/cupertino/page_test.dart | 12 +- .../flutter/test/cupertino/scaffold_test.dart | 14 +- .../test/cupertino/tab_scaffold_test.dart | 8 +- packages/flutter/test/cupertino/tab_test.dart | 12 +- packages/flutter/test/material/app_test.dart | 76 +- .../flutter/test/material/dialog_test.dart | 16 +- .../flutter/test/material/dropdown_test.dart | 4 +- .../material/floating_action_button_test.dart | 6 +- .../material/modal_bottom_sheet_test.dart | 8 +- packages/flutter/test/material/page_test.dart | 4 +- .../test/material/popup_menu_test.dart | 2 +- .../flutter/test/material/scaffold_test.dart | 2 +- .../test/material/text_field_focus_test.dart | 2 +- .../flutter/test/material/theme_test.dart | 6 +- .../test/material/time_picker_test.dart | 2 +- .../flutter/test/material/tooltip_test.dart | 2 +- .../flutter/test/material/will_pop_test.dart | 10 +- .../test/widgets/app_navigator_key_test.dart | 2 +- .../flutter/test/widgets/app_title_test.dart | 2 +- .../flutter/test/widgets/heroes_test.dart | 28 +- .../flutter/test/widgets/navigator_test.dart | 143 +- .../page_forward_transitions_test.dart | 2 +- .../test/widgets/page_transitions_test.dart | 5 +- .../remember_scroll_position_test.dart | 4 +- .../flutter/test/widgets/routes_test.dart | 24 +- .../test/override_test.dart | 2 +- .../test/time_picker_test.dart | 2 +- .../test/widgets_test.dart | 2 +- 63 files changed, 1614 insertions(+), 789 deletions(-) diff --git a/dev/manual_tests/lib/text.dart b/dev/manual_tests/lib/text.dart index 92212d84ed3..9b2567d0c6d 100644 --- a/dev/manual_tests/lib/text.dart +++ b/dev/manual_tests/lib/text.dart @@ -47,37 +47,37 @@ class _HomeState extends State { child: const Text('Test Underlines'), color: Colors.red.shade800, textColor: Colors.white, - onPressed: () { Navigator.of(context).pushNamed('underlines'); }, + onPressed: () { Navigator.pushNamed(context, 'underlines'); }, ), new FlatButton( child: const Text('Test Font Fallback'), color: Colors.orange.shade700, textColor: Colors.white, - onPressed: () { Navigator.of(context).pushNamed('fallback'); }, + onPressed: () { Navigator.pushNamed(context, 'fallback'); }, ), new FlatButton( child: const Text('Test Bidi Formatting'), color: Colors.yellow.shade700, textColor: Colors.black, - onPressed: () { Navigator.of(context).pushNamed('bidi'); }, + onPressed: () { Navigator.pushNamed(context, 'bidi'); }, ), new FlatButton( child: const Text('TextSpan Fuzzer'), color: Colors.green.shade400, textColor: Colors.black, - onPressed: () { Navigator.of(context).pushNamed('fuzzer'); }, + onPressed: () { Navigator.pushNamed(context, 'fuzzer'); }, ), new FlatButton( child: const Text('Diacritics Fuzzer'), color: Colors.blue.shade400, textColor: Colors.white, - onPressed: () { Navigator.of(context).pushNamed('zalgo'); }, + onPressed: () { Navigator.pushNamed(context, 'zalgo'); }, ), new FlatButton( child: const Text('Painting Fuzzer'), color: Colors.purple.shade200, textColor: Colors.black, - onPressed: () { Navigator.of(context).pushNamed('painting'); }, + onPressed: () { Navigator.pushNamed(context, 'painting'); }, ), ], ), diff --git a/examples/flutter_gallery/lib/demo/animation/home.dart b/examples/flutter_gallery/lib/demo/animation/home.dart index 5a476e3d8fe..adf1e56f53c 100644 --- a/examples/flutter_gallery/lib/demo/animation/home.dart +++ b/examples/flutter_gallery/lib/demo/animation/home.dart @@ -451,7 +451,7 @@ class _AnimationDemoHomeState extends State { if (_scrollController.offset >= midScrollOffset) _scrollController.animateTo(0.0, curve: _kScrollCurve, duration: _kScrollDuration); else - Navigator.of(context).maybePop(); + Navigator.maybePop(context); } // Only enable paging for the heading when the user has scrolled to midScrollOffset. diff --git a/examples/flutter_gallery/lib/demo/cupertino/cupertino_dialog_demo.dart b/examples/flutter_gallery/lib/demo/cupertino/cupertino_dialog_demo.dart index 4d23789e3e8..ad0cd3ae9a8 100644 --- a/examples/flutter_gallery/lib/demo/cupertino/cupertino_dialog_demo.dart +++ b/examples/flutter_gallery/lib/demo/cupertino/cupertino_dialog_demo.dart @@ -21,7 +21,7 @@ class _CupertinoDialogDemoState extends State { barrierDismissible: false, builder: (BuildContext context) => child, ) - .then((T value) { // The value passed to Navigator.pop() or null. + .then((T value) { // The value passed to Navigator.pop() or null. if (value != null) { _scaffoldKey.currentState.showSnackBar(new SnackBar( content: new Text('You selected: $value') diff --git a/examples/flutter_gallery/lib/demo/cupertino/cupertino_navigation_demo.dart b/examples/flutter_gallery/lib/demo/cupertino/cupertino_navigation_demo.dart index 20a74f49b9d..6e0f084b3d8 100644 --- a/examples/flutter_gallery/lib/demo/cupertino/cupertino_navigation_demo.dart +++ b/examples/flutter_gallery/lib/demo/cupertino/cupertino_navigation_demo.dart @@ -173,7 +173,7 @@ class Tab1RowItem extends StatelessWidget { final Widget row = new GestureDetector( behavior: HitTestBehavior.opaque, onTap: () { - Navigator.of(context).push(new CupertinoPageRoute( + Navigator.of(context).push(new CupertinoPageRoute( builder: (BuildContext context) => new Tab1ItemPage( color: color, colorName: colorName, @@ -778,7 +778,7 @@ class Tab3Dialog extends StatelessWidget { color: CupertinoColors.activeBlue, child: const Text('Sign in'), onPressed: () { - Navigator.of(context).pop(); + Navigator.pop(context); }, ), ], diff --git a/examples/flutter_gallery/lib/demo/cupertino/cupertino_picker_demo.dart b/examples/flutter_gallery/lib/demo/cupertino/cupertino_picker_demo.dart index eed429ece0f..556d12fa872 100644 --- a/examples/flutter_gallery/lib/demo/cupertino/cupertino_picker_demo.dart +++ b/examples/flutter_gallery/lib/demo/cupertino/cupertino_picker_demo.dart @@ -112,7 +112,7 @@ class _CupertinoPickerDemoState extends State { const Padding(padding: const EdgeInsets.only(top: 32.0)), new GestureDetector( onTap: () async { - await showModalBottomSheet( + await showModalBottomSheet( context: context, builder: (BuildContext context) { return _buildBottomPicker(); diff --git a/examples/flutter_gallery/lib/demo/material/dialog_demo.dart b/examples/flutter_gallery/lib/demo/material/dialog_demo.dart index a085106f230..aa061c42195 100644 --- a/examples/flutter_gallery/lib/demo/material/dialog_demo.dart +++ b/examples/flutter_gallery/lib/demo/material/dialog_demo.dart @@ -70,7 +70,7 @@ class DialogDemoState extends State { context: context, builder: (BuildContext context) => child, ) - .then((T value) { // The value passed to Navigator.pop() or null. + .then((T value) { // The value passed to Navigator.pop() or null. if (value != null) { _scaffoldKey.currentState.showSnackBar(new SnackBar( content: new Text('You selected: $value') diff --git a/examples/flutter_gallery/lib/demo/material/drawer_demo.dart b/examples/flutter_gallery/lib/demo/material/drawer_demo.dart index 4406e40078b..e3db2f779fb 100644 --- a/examples/flutter_gallery/lib/demo/material/drawer_demo.dart +++ b/examples/flutter_gallery/lib/demo/material/drawer_demo.dart @@ -67,7 +67,7 @@ class _DrawerDemoState extends State with TickerProviderStateMixin { } void _showNotImplementedMessage() { - Navigator.of(context).pop(); // Dismiss the drawer. + Navigator.pop(context); // Dismiss the drawer. _scaffoldKey.currentState.showSnackBar(const SnackBar( content: const Text("The drawer's items don't do anything") )); @@ -231,7 +231,7 @@ class _DrawerDemoState extends State with TickerProviderStateMixin { } void _onOtherAccountsTap(BuildContext context) { - showDialog( + showDialog( context: context, builder: (BuildContext context) { return new AlertDialog( @@ -240,7 +240,7 @@ class _DrawerDemoState extends State with TickerProviderStateMixin { new FlatButton( child: const Text('OK'), onPressed: () { - Navigator.of(context).pop(); + Navigator.pop(context); }, ), ], diff --git a/examples/flutter_gallery/lib/demo/material/grid_list_demo.dart b/examples/flutter_gallery/lib/demo/material/grid_list_demo.dart index 1cac0b31ce2..e08fb5f2a99 100644 --- a/examples/flutter_gallery/lib/demo/material/grid_list_demo.dart +++ b/examples/flutter_gallery/lib/demo/material/grid_list_demo.dart @@ -165,7 +165,7 @@ class GridDemoPhotoItem extends StatelessWidget { final BannerTapCallback onBannerTap; // User taps on the photo's header or footer. void showPhoto(BuildContext context) { - Navigator.push(context, new MaterialPageRoute( + Navigator.push(context, new MaterialPageRoute( builder: (BuildContext context) { return new Scaffold( appBar: new AppBar( diff --git a/examples/flutter_gallery/lib/demo/material/modal_bottom_sheet_demo.dart b/examples/flutter_gallery/lib/demo/material/modal_bottom_sheet_demo.dart index ec1540a4e4b..3aa1db73320 100644 --- a/examples/flutter_gallery/lib/demo/material/modal_bottom_sheet_demo.dart +++ b/examples/flutter_gallery/lib/demo/material/modal_bottom_sheet_demo.dart @@ -15,7 +15,7 @@ class ModalBottomSheetDemo extends StatelessWidget { child: new RaisedButton( child: const Text('SHOW BOTTOM SHEET'), onPressed: () { - showModalBottomSheet(context: context, builder: (BuildContext context) { + showModalBottomSheet(context: context, builder: (BuildContext context) { return new Container( child: new Padding( padding: const EdgeInsets.all(32.0), diff --git a/examples/flutter_gallery/lib/demo/material/persistent_bottom_sheet_demo.dart b/examples/flutter_gallery/lib/demo/material/persistent_bottom_sheet_demo.dart index f35d0baba25..97ff8dfe40b 100644 --- a/examples/flutter_gallery/lib/demo/material/persistent_bottom_sheet_demo.dart +++ b/examples/flutter_gallery/lib/demo/material/persistent_bottom_sheet_demo.dart @@ -54,7 +54,7 @@ class _PersistentBottomSheetDemoState extends State { } void _showMessage() { - showDialog( + showDialog( context: context, builder: (BuildContext context) { return new AlertDialog( diff --git a/examples/flutter_gallery/lib/demo/pesto_demo.dart b/examples/flutter_gallery/lib/demo/pesto_demo.dart index 3b53440bc52..8d39eb37622 100644 --- a/examples/flutter_gallery/lib/demo/pesto_demo.dart +++ b/examples/flutter_gallery/lib/demo/pesto_demo.dart @@ -167,14 +167,14 @@ class _RecipeGridPageState extends State { } void showFavoritesPage(BuildContext context) { - Navigator.push(context, new MaterialPageRoute( + Navigator.push(context, new MaterialPageRoute( settings: const RouteSettings(name: '/pesto/favorites'), builder: (BuildContext context) => new PestoFavorites(), )); } void showRecipePage(BuildContext context, Recipe recipe) { - Navigator.push(context, new MaterialPageRoute( + Navigator.push(context, new MaterialPageRoute( settings: const RouteSettings(name: '/pesto/recipe'), builder: (BuildContext context) { return new Theme( diff --git a/examples/flutter_gallery/lib/demo/shrine/shrine_order.dart b/examples/flutter_gallery/lib/demo/shrine/shrine_order.dart index 13f3d44a79b..d89574dbc38 100644 --- a/examples/flutter_gallery/lib/demo/shrine/shrine_order.dart +++ b/examples/flutter_gallery/lib/demo/shrine/shrine_order.dart @@ -327,7 +327,7 @@ class ShrineOrderRoute extends ShrinePageRoute { ShrineOrderRoute({ @required this.order, WidgetBuilder builder, - RouteSettings settings: const RouteSettings(), + RouteSettings settings, }) : assert(order != null), super(builder: builder, settings: settings); @@ -336,5 +336,5 @@ class ShrineOrderRoute extends ShrinePageRoute { @override Order get currentResult => order; - static ShrineOrderRoute of(BuildContext context) => ModalRoute.of(context); + static ShrineOrderRoute of(BuildContext context) => ModalRoute.of(context); } diff --git a/examples/flutter_gallery/lib/demo/shrine/shrine_page.dart b/examples/flutter_gallery/lib/demo/shrine/shrine_page.dart index b3761ea2f42..ad35e37e40e 100644 --- a/examples/flutter_gallery/lib/demo/shrine/shrine_page.dart +++ b/examples/flutter_gallery/lib/demo/shrine/shrine_page.dart @@ -51,7 +51,7 @@ class ShrinePageState extends State { } void _showShoppingCart() { - showModalBottomSheet(context: context, builder: (BuildContext context) { + showModalBottomSheet(context: context, builder: (BuildContext context) { if (widget.shoppingCart.isEmpty) { return const Padding( padding: const EdgeInsets.all(24.0), diff --git a/examples/flutter_gallery/lib/demo/shrine_demo.dart b/examples/flutter_gallery/lib/demo/shrine_demo.dart index eb5b6451ba4..636dfd08a95 100644 --- a/examples/flutter_gallery/lib/demo/shrine_demo.dart +++ b/examples/flutter_gallery/lib/demo/shrine_demo.dart @@ -25,7 +25,7 @@ Widget buildShrine(BuildContext context, Widget child) { class ShrinePageRoute extends MaterialPageRoute { ShrinePageRoute({ WidgetBuilder builder, - RouteSettings settings: const RouteSettings() + RouteSettings settings, }) : super(builder: builder, settings: settings); @override diff --git a/examples/flutter_gallery/lib/demo/video_demo.dart b/examples/flutter_gallery/lib/demo/video_demo.dart index b82c2c8f431..c906158c5e5 100644 --- a/examples/flutter_gallery/lib/demo/video_demo.dart +++ b/examples/flutter_gallery/lib/demo/video_demo.dart @@ -64,12 +64,12 @@ class VideoCard extends StatelessWidget { } void pushFullScreenWidget() { - final TransitionRoute route = new PageRouteBuilder( + final TransitionRoute route = new PageRouteBuilder( settings: new RouteSettings(name: title, isInitialRoute: false), pageBuilder: fullScreenRoutePageBuilder, ); - route.completed.then((Null _) { + route.completed.then((void result) { controller.setVolume(0.0); }); diff --git a/examples/flutter_gallery/lib/gallery/app.dart b/examples/flutter_gallery/lib/gallery/app.dart index db3a9d6b70e..d3af4df0c0b 100644 --- a/examples/flutter_gallery/lib/gallery/app.dart +++ b/examples/flutter_gallery/lib/gallery/app.dart @@ -165,7 +165,7 @@ class GalleryAppState extends State { checkerboardRasterCacheImages: _checkerboardRasterCacheImages, checkerboardOffscreenLayers: _checkerboardOffscreenLayers, routes: _kRoutes, - home: _applyScaleFactor(home), + home: home, builder: (BuildContext context, Widget child) { return new Directionality( textDirection: _overrideDirection, diff --git a/examples/flutter_gallery/lib/gallery/updates.dart b/examples/flutter_gallery/lib/gallery/updates.dart index 1b61ef7459b..ec83492ed43 100644 --- a/examples/flutter_gallery/lib/gallery/updates.dart +++ b/examples/flutter_gallery/lib/gallery/updates.dart @@ -31,23 +31,23 @@ class UpdaterState extends State { } static DateTime _lastUpdateCheck; - Future _checkForUpdates() async { + Future _checkForUpdates() async { // Only prompt once a day if (_lastUpdateCheck != null && new DateTime.now().difference(_lastUpdateCheck) < const Duration(days: 1)) { - return; // We already checked for updates recently + return null; // We already checked for updates recently } _lastUpdateCheck = new DateTime.now(); final String updateUrl = await widget.updateUrlFetcher(); if (updateUrl != null) { - final bool wantsUpdate = await showDialog(context: context, builder: _buildDialog); + final bool wantsUpdate = await showDialog(context: context, builder: _buildDialog); if (wantsUpdate != null && wantsUpdate) launch(updateUrl); } } - Widget _buildDialog(BuildContext _) { + Widget _buildDialog(BuildContext context) { final ThemeData theme = Theme.of(context); final TextStyle dialogTextStyle = theme.textTheme.subhead.copyWith(color: theme.textTheme.caption.color); @@ -56,16 +56,19 @@ class UpdaterState extends State { content: new Text('A newer version is available.', style: dialogTextStyle), actions: [ new FlatButton( - child: const Text('NO THANKS'), - onPressed: () { - Navigator.pop(context, false); - }), + child: const Text('NO THANKS'), + onPressed: () { + Navigator.pop(context, false); + }, + ), new FlatButton( - child: const Text('UPDATE'), - onPressed: () { - Navigator.pop(context, true); - }), - ]); + child: const Text('UPDATE'), + onPressed: () { + Navigator.pop(context, true); + }, + ), + ], + ); } @override diff --git a/examples/stocks/lib/main.dart b/examples/stocks/lib/main.dart index 5cef3350961..d415902d700 100644 --- a/examples/stocks/lib/main.dart +++ b/examples/stocks/lib/main.dart @@ -83,7 +83,7 @@ class StocksAppState extends State { return null; } - Route _getRoute(RouteSettings settings) { + Route _getRoute(RouteSettings settings) { // Routes, by convention, are split on slashes, like filesystem paths. final List path = settings.name.split('/'); // We only support paths that start with a slash, so bail if @@ -100,7 +100,7 @@ class StocksAppState extends State { // Extract the symbol part of "stock:..." and return a route // for that symbol. final String symbol = path[1].substring(6); - return new MaterialPageRoute( + return new MaterialPageRoute( settings: settings, builder: (BuildContext context) => new StockSymbolPage(symbol: symbol, stocks: stocks), ); diff --git a/examples/stocks/lib/stock_home.dart b/examples/stocks/lib/stock_home.dart index d6ab7545440..4ff6cbf89e5 100644 --- a/examples/stocks/lib/stock_home.dart +++ b/examples/stocks/lib/stock_home.dart @@ -93,7 +93,7 @@ class StockHomeState extends State { }); break; case _StockMenuItem.refresh: - showDialog( + showDialog( context: context, builder: (BuildContext context) => new _NotImplementedDialog(), ); @@ -297,7 +297,7 @@ class StockHomeState extends State { } void _handleCreateCompany() { - showModalBottomSheet( + showModalBottomSheet( context: context, builder: (BuildContext context) => new _CreateCompanySheet(), ); diff --git a/packages/flutter/lib/src/cupertino/nav_bar.dart b/packages/flutter/lib/src/cupertino/nav_bar.dart index c5fcbb864ac..4013a6aff17 100644 --- a/packages/flutter/lib/src/cupertino/nav_bar.dart +++ b/packages/flutter/lib/src/cupertino/nav_bar.dart @@ -397,7 +397,7 @@ class _CupertinoPersistentNavigationBar extends StatelessWidget implements Prefe ) : const Text('Close'), padding: EdgeInsets.zero, - onPressed: () { Navigator.of(context).maybePop(); }, + onPressed: () { Navigator.maybePop(context); }, ); } } diff --git a/packages/flutter/lib/src/cupertino/route.dart b/packages/flutter/lib/src/cupertino/route.dart index 22334938441..9c2671b7552 100644 --- a/packages/flutter/lib/src/cupertino/route.dart +++ b/packages/flutter/lib/src/cupertino/route.dart @@ -76,16 +76,15 @@ final DecorationTween _kGradientShadowTween = new DecorationTween( class CupertinoPageRoute extends PageRoute { /// Creates a page route for use in an iOS designed app. /// - /// The [builder], [settings], [maintainState], and [fullscreenDialog] - /// arguments must not be null. + /// The [builder], [maintainState], and [fullscreenDialog] arguments must not + /// be null. CupertinoPageRoute({ @required this.builder, - RouteSettings settings: const RouteSettings(), + RouteSettings settings, this.maintainState: true, bool fullscreenDialog: false, this.hostRoute, }) : assert(builder != null), - assert(settings != null), assert(maintainState != null), assert(fullscreenDialog != null), super(settings: settings, fullscreenDialog: fullscreenDialog) { @@ -149,7 +148,7 @@ class CupertinoPageRoute extends PageRoute { super.dispose(); } - _CupertinoBackGestureController _backGestureController; + _CupertinoBackGestureController _backGestureController; /// Whether a pop gesture is currently underway. /// @@ -217,11 +216,11 @@ class CupertinoPageRoute extends PageRoute { /// appropriate. /// * [Route.startPopGesture], which describes the contract that this method /// must implement. - _CupertinoBackGestureController _startPopGesture() { + _CupertinoBackGestureController _startPopGesture() { assert(!popGestureInProgress); assert(popGestureEnabled); final PageRoute route = hostRoute ?? this; - _backGestureController = new _CupertinoBackGestureController( + _backGestureController = new _CupertinoBackGestureController( navigator: route.navigator, controller: route.controller, onEnded: _endPopGesture, @@ -265,7 +264,7 @@ class CupertinoPageRoute extends PageRoute { // In the middle of a back gesture drag, let the transition be linear to // match finger motions. linearTransition: popGestureInProgress, - child: new _CupertinoBackGestureDetector( + child: new _CupertinoBackGestureDetector( enabledCallback: () => popGestureEnabled, onStartPopGesture: _startPopGesture, child: child, @@ -392,7 +391,10 @@ class CupertinoFullscreenDialogTransition extends StatelessWidget { /// /// The gesture data is converted from absolute coordinates to logical /// coordinates by this widget. -class _CupertinoBackGestureDetector extends StatefulWidget { +/// +/// The type `T` specifies the return type of the route with which this gesture +/// detector is associated. +class _CupertinoBackGestureDetector extends StatefulWidget { const _CupertinoBackGestureDetector({ Key key, @required this.enabledCallback, @@ -407,14 +409,14 @@ class _CupertinoBackGestureDetector extends StatefulWidget { final ValueGetter enabledCallback; - final ValueGetter<_CupertinoBackGestureController> onStartPopGesture; + final ValueGetter<_CupertinoBackGestureController> onStartPopGesture; @override - _CupertinoBackGestureDetectorState createState() => new _CupertinoBackGestureDetectorState(); + _CupertinoBackGestureDetectorState createState() => new _CupertinoBackGestureDetectorState(); } -class _CupertinoBackGestureDetectorState extends State<_CupertinoBackGestureDetector> { - _CupertinoBackGestureController _backGestureController; +class _CupertinoBackGestureDetectorState extends State<_CupertinoBackGestureDetector> { + _CupertinoBackGestureController _backGestureController; HorizontalDragGestureRecognizer _recognizer; @@ -508,7 +510,10 @@ class _CupertinoBackGestureDetectorState extends State<_CupertinoBackGestureDete /// /// This class works entirely in logical coordinates (0.0 is new page dismissed, /// 1.0 is new page on top). -class _CupertinoBackGestureController { +/// +/// The type `T` specifies the return type of the route with which this gesture +/// detector controller is associated. +class _CupertinoBackGestureController { /// Creates a controller for an iOS-style back gesture. /// /// The [navigator] and [controller] arguments must not be null. @@ -566,7 +571,7 @@ class _CupertinoBackGestureController { controller.removeStatusListener(_handleStatusChanged); _animating = false; if (status == AnimationStatus.dismissed) - navigator.pop(); // this will cause the route to get disposed, which will dispose us + navigator.pop(); // this will cause the route to get disposed, which will dispose us onEnded(); // this will call dispose if popping the route failed to do so } diff --git a/packages/flutter/lib/src/material/about.dart b/packages/flutter/lib/src/material/about.dart index d9acadcc497..c14dcd6ca08 100644 --- a/packages/flutter/lib/src/material/about.dart +++ b/packages/flutter/lib/src/material/about.dart @@ -149,9 +149,9 @@ void showAboutDialog({ String applicationVersion, Widget applicationIcon, String applicationLegalese, - List children + List children, }) { - showDialog( + showDialog( context: context, builder: (BuildContext context) { return new AboutDialog( @@ -185,9 +185,7 @@ void showLicensePage({ Widget applicationIcon, String applicationLegalese }) { - // TODO(ianh): remove pop once https://github.com/flutter/flutter/issues/4667 is fixed - Navigator.pop(context); - Navigator.push(context, new MaterialPageRoute( + Navigator.push(context, new MaterialPageRoute( builder: (BuildContext context) => new LicensePage( applicationName: applicationName, applicationVersion: applicationVersion, diff --git a/packages/flutter/lib/src/material/app.dart b/packages/flutter/lib/src/material/app.dart index 30e14535c35..b11cbbb5970 100644 --- a/packages/flutter/lib/src/material/app.dart +++ b/packages/flutter/lib/src/material/app.dart @@ -607,10 +607,11 @@ class _MaterialAppState extends State { Route _onGenerateRoute(RouteSettings settings) { final String name = settings.name; WidgetBuilder builder; - if (name == Navigator.defaultRouteName && widget.home != null) + if (name == Navigator.defaultRouteName && widget.home != null) { builder = (BuildContext context) => widget.home; - else + } else { builder = widget.routes[name]; + } if (builder != null) { return new MaterialPageRoute( builder: builder, diff --git a/packages/flutter/lib/src/material/back_button.dart b/packages/flutter/lib/src/material/back_button.dart index 1d7f857e1ed..cef56e0e71a 100644 --- a/packages/flutter/lib/src/material/back_button.dart +++ b/packages/flutter/lib/src/material/back_button.dart @@ -86,7 +86,7 @@ class BackButton extends StatelessWidget { color: color, tooltip: MaterialLocalizations.of(context).backButtonTooltip, onPressed: () { - Navigator.of(context).maybePop(); + Navigator.maybePop(context); } ); } @@ -118,7 +118,7 @@ class CloseButton extends StatelessWidget { icon: const Icon(Icons.close), tooltip: MaterialLocalizations.of(context).closeButtonTooltip, onPressed: () { - Navigator.of(context).maybePop(); + Navigator.maybePop(context); }, ); } diff --git a/packages/flutter/lib/src/material/bottom_sheet.dart b/packages/flutter/lib/src/material/bottom_sheet.dart index b9c4959e0b6..080dd2a2b48 100644 --- a/packages/flutter/lib/src/material/bottom_sheet.dart +++ b/packages/flutter/lib/src/material/bottom_sheet.dart @@ -179,14 +179,10 @@ class _ModalBottomSheet extends StatefulWidget { } class _ModalBottomSheetState extends State<_ModalBottomSheet> { - void _navigatorPop() { - Navigator.pop(context); - } - @override Widget build(BuildContext context) { return new GestureDetector( - onTap: _navigatorPop, + onTap: () => Navigator.pop(context), child: new AnimatedBuilder( animation: widget.route.animation, builder: (BuildContext context, Widget child) { @@ -211,7 +207,8 @@ class _ModalBottomSheetRoute extends PopupRoute { this.builder, this.theme, this.barrierLabel, - }); + RouteSettings settings, + }) : super(settings: settings); final WidgetBuilder builder; final ThemeData theme; diff --git a/packages/flutter/lib/src/material/dialog.dart b/packages/flutter/lib/src/material/dialog.dart index a265613c52c..52a3a7908de 100644 --- a/packages/flutter/lib/src/material/dialog.dart +++ b/packages/flutter/lib/src/material/dialog.dart @@ -488,8 +488,10 @@ class _DialogRoute extends PopupRoute { bool barrierDismissible: true, this.barrierLabel, @required this.child, + RouteSettings settings, }) : assert(barrierDismissible != null), - _barrierDismissible = barrierDismissible; + _barrierDismissible = barrierDismissible, + super(settings: settings); final Widget child; final ThemeData theme; diff --git a/packages/flutter/lib/src/material/dropdown.dart b/packages/flutter/lib/src/material/dropdown.dart index f7abdb4aad8..28cd1f46081 100644 --- a/packages/flutter/lib/src/material/dropdown.dart +++ b/packages/flutter/lib/src/material/dropdown.dart @@ -600,7 +600,7 @@ class _DropdownButtonState extends State> with WidgetsBindi Navigator.push(context, _dropdownRoute).then((_DropdownRouteResult newValue) { _dropdownRoute = null; if (!mounted || newValue == null) - return null; + return; if (widget.onChanged != null) widget.onChanged(newValue.result); }); diff --git a/packages/flutter/lib/src/material/page.dart b/packages/flutter/lib/src/material/page.dart index 59cecb6a0c4..5bd8aee6e96 100644 --- a/packages/flutter/lib/src/material/page.dart +++ b/packages/flutter/lib/src/material/page.dart @@ -48,34 +48,38 @@ class _MountainViewPageTransition extends StatelessWidget { } } -/// A modal route that replaces the entire screen with a platform-adaptive transition. +/// A modal route that replaces the entire screen with a platform-adaptive +/// transition. /// -/// For Android, the entrance transition for the page slides the page upwards and fades it -/// in. The exit transition is the same, but in reverse. +/// For Android, the entrance transition for the page slides the page upwards +/// and fades it in. The exit transition is the same, but in reverse. /// -/// The transition is adaptive to the platform and on iOS, the page slides in from the right and -/// exits in reverse. The page also shifts to the left in parallax when another page enters to -/// cover it. +/// The transition is adaptive to the platform and on iOS, the page slides in +/// from the right and exits in reverse. The page also shifts to the left in +/// parallax when another page enters to cover it. (These directions are flipped +/// in environements with a right-to-left reading direction.) /// /// By default, when a modal route is replaced by another, the previous route /// remains in memory. To free all the resources when this is not necessary, set /// [maintainState] to false. /// -/// Specify whether the incoming page is a fullscreen modal dialog. On iOS, those -/// pages animate bottom->up rather than right->left. +/// The `fullscreenDialog` property specifies whether the incoming page is a +/// fullscreen modal dialog. On iOS, those pages animate from the bottom to the +/// top rather than horizontally. /// /// The type `T` specifies the return type of the route which can be supplied as -/// the route is popped from the stack via [Navigator.pop] when an optional -/// `result` can be provided. +/// the route is popped from the stack via [Navigator.pop] by providing the +/// optional `result` argument. /// /// See also: /// -/// * [CupertinoPageRoute], that this [PageRoute] delegates transition animations to for iOS. +/// * [CupertinoPageRoute], which this [PageRoute] delegates transition +/// animations to for iOS. class MaterialPageRoute extends PageRoute { /// Creates a page route for use in a material design app. MaterialPageRoute({ @required this.builder, - RouteSettings settings: const RouteSettings(), + RouteSettings settings, this.maintainState: true, bool fullscreenDialog: false, }) : assert(builder != null), diff --git a/packages/flutter/lib/src/material/popup_menu.dart b/packages/flutter/lib/src/material/popup_menu.dart index 92dfc5b99f5..6d6026cbe2d 100644 --- a/packages/flutter/lib/src/material/popup_menu.dart +++ b/packages/flutter/lib/src/material/popup_menu.dart @@ -196,15 +196,44 @@ class PopupMenuItem extends PopupMenuEntry { bool represents(T value) => value == this.value; @override - _PopupMenuItemState> createState() => new _PopupMenuItemState>(); + PopupMenuItemState> createState() => new PopupMenuItemState>(); } -class _PopupMenuItemState> extends State { - // Override this to put something else in the menu entry. +/// The [State] for [PopupMenuItem] subclasses. +/// +/// By default this implements the basic styling and layout of Material Design +/// popup menu items. +/// +/// The [buildChild] method can be overridden to adjust exactly what gets placed +/// in the menu. By default it returns [PopupMenuItem.child]. +/// +/// The [handleTap] method can be overridden to adjust exactly what happens when +/// the item is tapped. By default, it uses [Navigator.pop] to return the +/// [PopupMenuItem.value] from the menu route. +/// +/// This class takes two type arguments. The second, `W`, is the exact type of +/// the [Widget] that is using this [State]. It must be a subclass of +/// [PopupMenuItem]. The first, `T`, must match the type argument of that widget +/// class, and is the type of values returned from this menu. +class PopupMenuItemState> extends State { + /// The menu item contents. + /// + /// Used by the [build] method. + /// + /// By default, this returns [PopupMenuItem.child]. Override this to put + /// something else in the menu entry. + @protected Widget buildChild() => widget.child; + /// The handler for when the user selects the menu item. + /// + /// Used by the [InkWell] inserted by the [build] method. + /// + /// By default, uses [Navigator.pop] to return the [PopupMenuItem.value] from + /// the menu route. + @protected void handleTap() { - Navigator.pop(context, widget.value); + Navigator.pop(context, widget.value); } @override @@ -238,8 +267,8 @@ class _PopupMenuItemState> extends State { height: widget.height, padding: const EdgeInsets.symmetric(horizontal: _kMenuHorizontalPadding), child: item, - ) - ) + ), + ), ); } } @@ -349,7 +378,7 @@ class CheckedPopupMenuItem extends PopupMenuItem { _CheckedPopupMenuItemState createState() => new _CheckedPopupMenuItemState(); } -class _CheckedPopupMenuItemState extends _PopupMenuItemState> with SingleTickerProviderStateMixin { +class _CheckedPopupMenuItemState extends PopupMenuItemState> with SingleTickerProviderStateMixin { static const Duration _kFadeDuration = const Duration(milliseconds: 150); AnimationController _controller; Animation get _opacity => _controller.view; diff --git a/packages/flutter/lib/src/semantics/semantics.dart b/packages/flutter/lib/src/semantics/semantics.dart index def2db139c5..41e7c0f6b19 100644 --- a/packages/flutter/lib/src/semantics/semantics.dart +++ b/packages/flutter/lib/src/semantics/semantics.dart @@ -680,6 +680,9 @@ class SemanticsProperties extends DiagnosticableTree { properties.add(new EnumProperty('textDirection', textDirection, defaultValue: null)); properties.add(new DiagnosticsProperty('sortKey', sortKey, defaultValue: null)); } + + @override + String toStringShort() => '$runtimeType'; // the hashCode isn't important since we're immutable } /// In tests use this function to reset the counter used to generate diff --git a/packages/flutter/lib/src/widgets/navigator.dart b/packages/flutter/lib/src/widgets/navigator.dart index a0f8844d4be..a2bd4155e62 100644 --- a/packages/flutter/lib/src/widgets/navigator.dart +++ b/packages/flutter/lib/src/widgets/navigator.dart @@ -16,6 +16,25 @@ import 'framework.dart'; import 'overlay.dart'; import 'ticker_provider.dart'; +// Examples can assume: +// class MyPage extends Placeholder { MyPage({String title}); } +// class MyHomePage extends Placeholder { } +// NavigatorState navigator; + +/// Creates a route for the given route settings. +/// +/// Used by [Navigator.onGenerateRoute] and [Navigator.onUnknownRoute]. +typedef Route RouteFactory(RouteSettings settings); + +/// Signature for the [Navigator.popUntil] predicate argument. +typedef bool RoutePredicate(Route route); + +/// Signature for a callback that verifies that it's OK to call [Navigator.pop]. +/// +/// Used by [Form.onWillPop], [ModalRoute.addScopedWillPopCallback], +/// [ModalRoute.removeScopedWillPopCallback], and [WillPopScope]. +typedef Future WillPopCallback(); + /// Indicates whether the current route should be popped. /// /// Used as the return value for [Route.willPop]. @@ -43,12 +62,6 @@ enum RoutePopDisposition { bubble, } -/// Signature for a callback that verifies that it's OK to call [Navigator.pop]. -/// -/// Used by [Form.onWillPop], [ModalRoute.addScopedWillPopCallback], -/// [ModalRoute.removeScopedWillPopCallback], and [WillPopScope]. -typedef Future WillPopCallback(); - /// An abstraction for an entry managed by a [Navigator]. /// /// This class defines an abstract interface between the navigator and the @@ -62,27 +75,35 @@ typedef Future WillPopCallback(); /// See [MaterialPageRoute] for a route that replaces the /// entire screen with a platform-adaptive transition. abstract class Route { + /// Initialize the [Route]. + /// + /// If the [settings] are not provided, an empty [RouteSettings] object is + /// used instead. + Route({ RouteSettings settings }) : this.settings = settings ?? const RouteSettings(); + /// The navigator that the route is in, if any. NavigatorState get navigator => _navigator; NavigatorState _navigator; + /// The settings for this route. + /// + /// See [RouteSettings] for details. + final RouteSettings settings; + /// The overlay entries for this route. List get overlayEntries => const []; - /// A future that completes when this route is popped off the navigator. - /// - /// The future completes with the value given to [Navigator.pop], if any. - Future get popped => _popCompleter.future; - final Completer _popCompleter = new Completer(); - /// Called when the route is inserted into the navigator. /// - /// Use this to populate overlayEntries and add them to the overlay - /// (accessible as navigator.overlay). (The reason the Route is responsible - /// for doing this, rather than the Navigator, is that the Route will be - /// responsible for _removing_ the entries and this way it's symmetric.) + /// Use this to populate [overlayEntries] and add them to the overlay + /// (accessible as [Navigator.overlay]). (The reason the [Route] is + /// responsible for doing this, rather than the [Navigator], is that the + /// [Route] will be responsible for _removing_ the entries and this way it's + /// symmetric.) /// - /// The overlay argument will be null if this is the first route inserted. + /// The `insertionPoint` argument will be null if this is the first route + /// inserted. Otherwise, it indicates the overlay entry to place immediately + /// below the first overlay for this route. @protected @mustCallSuper void install(OverlayEntry insertionPoint) { } @@ -90,14 +111,16 @@ abstract class Route { /// Called after [install] when the route is pushed onto the navigator. /// /// The returned value resolves when the push transition is complete. + /// + /// The [didChangeNext] method is typically called immediately after this + /// method is called. @protected TickerFuture didPush() => new TickerFuture.complete(); - /// When this route is popped (see [Navigator.pop]) if the result isn't - /// specified or if it's null, this value will be used instead. - T get currentResult => null; - /// Called after [install] when the route replaced another in the navigator. + /// + /// The [didChangeNext] method is typically called immediately after this + /// method is called. @protected @mustCallSuper void didReplace(Route oldRoute) { } @@ -119,10 +142,24 @@ abstract class Route { return isFirst ? RoutePopDisposition.bubble : RoutePopDisposition.pop; } + /// Whether calling [didPop] would return false. + bool get willHandlePopInternally => false; + + /// When this route is popped (see [Navigator.pop]) if the result isn't + /// specified or if it's null, this value will be used instead. + T get currentResult => null; + + /// A future that completes when this route is popped off the navigator. + /// + /// The future completes with the value given to [Navigator.pop], if any. + Future get popped => _popCompleter.future; + final Completer _popCompleter = new Completer(); + /// A request was made to pop this route. If the route can handle it /// internally (e.g. because it has its own stack of internal state) then - /// return false, otherwise return true. Returning false will prevent the - /// default behavior of [NavigatorState.pop]. + /// return false, otherwise return true (by return the value of calling + /// `super.didPop`). Returning false will prevent the default behavior of + /// [NavigatorState.pop]. /// /// When this function returns true, the navigator removes this route from /// the history but does not yet call [dispose]. Instead, it is the route's @@ -137,19 +174,28 @@ abstract class Route { return true; } - /// Whether calling [didPop] would return false. - bool get willHandlePopInternally => false; + /// The route was popped or is otherwise being removed somewhat gracefully. + /// + /// This is called by [didPop] and in response to [Navigator.pushReplacement]. + /// + /// The [popped] future is completed by this method. + @protected + @mustCallSuper + void didComplete(T result) { + _popCompleter.complete(result); + } - /// The given route, which came after this one, has been popped off the + /// The given route, which was above this one, has been popped off the /// navigator. @protected @mustCallSuper void didPopNext(Route nextRoute) { } /// This route's next route has changed to the given new route. This is called - /// on a route whenever the next route changes for any reason, except for - /// cases when [didPopNext] would be called, so long as it is in the history. - /// `nextRoute` will be null if there's no next route. + /// on a route whenever the next route changes for any reason, so long as it + /// is in the history, including when a route is first added to a [Navigator] + /// (e.g. by [Navigator.push]), except for cases when [didPopNext] would be + /// called. `nextRoute` will be null if there's no next route. @protected @mustCallSuper void didChangeNext(Route nextRoute) { } @@ -163,14 +209,37 @@ abstract class Route { @mustCallSuper void didChangePrevious(Route previousRoute) { } - /// The route was popped or is otherwise being removed somewhat gracefully. + /// Called whenever the internal state of the route has changed. /// - /// This is called by [didPop] and in response to [Navigator.pushReplacement]. + /// This should be called whenever [willHandlePopInternally], [didPop], + /// [offstage], or other internal state of the route changes value. It is used + /// by [ModalRoute], for example, to report the new information via its + /// inherited widget to any children of the route. + /// + /// See also: + /// + /// * [changedExternalState], which is called when the [Navigator] rebuilds. @protected @mustCallSuper - void didComplete(T result) { - _popCompleter.complete(result); - } + void changedInternalState() { } + + /// Called whenever the [Navigator] has its widget rebuilt, to indicate that + /// the route may wish to rebuild as well. + /// + /// This is called by the [Navigator] whenever the [NavigatorState]'s + /// [widget] changes, for example because the [MaterialApp] has been rebuilt. + /// This ensures that routes that directly refer to the state of the widget + /// that built the [MaterialApp] will be notified when that widget rebuilds, + /// since it would otherwise be difficult to notify the routes that state they + /// depend on may have changed. + /// + /// See also: + /// + /// * [changedInternalState], the equivalent but for changes to the internal + /// state of the route. + @protected + @mustCallSuper + void changedExternalState() { } /// The route should remove its overlays and free any other resources. /// @@ -205,7 +274,7 @@ abstract class Route { /// route), then [isCurrent] will also be true. If it is the first route (the /// bottom-most route), then [isFirst] will also be true. /// - /// If a later route is entirely opaque, then the route will be active but not + /// If a higher route is entirely opaque, then the route will be active but not /// rendered. It is even possible for the route to be active but for the stateful /// widgets within the route to not be instantiated. See [ModalRoute.maintainState]. bool get isActive { @@ -248,11 +317,6 @@ class RouteSettings { String toString() => '"$name"'; } -/// Creates a route for the given route settings. -/// -/// Used by [Navigator.onGenerateRoute] and [Navigator.onUnknownRoute]. -typedef Route RouteFactory(RouteSettings settings); - /// An interface for observing the behavior of a [Navigator]. class NavigatorObserver { /// The navigator that the observer is observing, if any. @@ -260,14 +324,31 @@ class NavigatorObserver { NavigatorState _navigator; /// The [Navigator] pushed `route`. + /// + /// The route immediately below that one, and thus the previously active + /// route, is `previousRoute`. void didPush(Route route, Route previousRoute) { } /// The [Navigator] popped `route`. + /// + /// The route immediately below that one, and thus the newly active + /// route, is `previousRoute`. void didPop(Route route, Route previousRoute) { } /// The [Navigator] removed `route`. + /// + /// If only one route is being removed, then the route immediately below + /// that one, if any, is `previousRoute`. + /// + /// If multiple routes are being removed, then the route below the + /// bottommost route being removed, if any, is `previousRoute`, and this + /// method will be called once for each removed route, from the topmost route + /// to the bottommost route. void didRemove(Route route, Route previousRoute) { } + /// The [Navigator] replaced `oldRoute` with `newRoute`. + void didReplace({ Route newRoute, Route oldRoute }) { } + /// The [Navigator]'s routes are being moved by a user gesture. /// /// For example, this is called when an iOS back gesture starts, and is used @@ -280,9 +361,6 @@ class NavigatorObserver { void didStopUserGesture() { } } -/// Signature for the [Navigator.popUntil] predicate argument. -typedef bool RoutePredicate(Route route); - /// A widget that manages a set of child widgets with a stack discipline. /// /// Many apps have a navigator near the top of their widget hierarchy in order @@ -306,9 +384,9 @@ typedef bool RoutePredicate(Route route); /// the navigator created by a [WidgetsApp] or a [MaterialApp] widget. You /// can refer to that navigator with [Navigator.of]. /// -/// A MaterialApp is the simplest way to set things up. The MaterialApp's -/// home becomes the route at the bottom of the Navigator's stack. It is -/// what you see when the app is launched. +/// A [MaterialApp] is the simplest way to set things up. The [MaterialApp]'s +/// home becomes the route at the bottom of the [Navigator]'s stack. It is what +/// you see when the app is launched. /// /// ```dart /// void main() { @@ -321,7 +399,7 @@ typedef bool RoutePredicate(Route route); /// want to appear on the screen. For example: /// /// ```dart -/// Navigator.of(context).push(new MaterialPageRoute( +/// Navigator.push(context, new MaterialPageRoute( /// builder: (BuildContext context) { /// return new Scaffold( /// appBar: new AppBar(title: new Text('My Page')), @@ -329,7 +407,7 @@ typedef bool RoutePredicate(Route route); /// child: new FlatButton( /// child: new Text('POP'), /// onPressed: () { -/// Navigator.of(context).pop(); +/// Navigator.pop(context); /// }, /// ), /// ), @@ -346,7 +424,7 @@ typedef bool RoutePredicate(Route route); /// page, with the Navigator's pop method: /// /// ```dart -/// Navigator.of(context).pop(); +/// Navigator.pop(context); /// ``` /// /// It usually isn't necessary to provide a widget that pops the Navigator @@ -383,7 +461,7 @@ typedef bool RoutePredicate(Route route); /// To show a route by name: /// /// ```dart -/// Navigator.of(context).pushNamed('/b'); +/// Navigator.pushNamed(context, '/b'); /// ``` /// /// ### Routes can return a value @@ -391,32 +469,35 @@ typedef bool RoutePredicate(Route route); /// When a route is pushed to ask the user for a value, the value can be /// returned via the [pop] method's result parameter. /// -/// Methods that push a route return a Future. The Future resolves when -/// the route is popped and the Future's value is the [pop] method's result +/// Methods that push a route return a [Future]. The Future resolves when the +/// route is popped and the [Future]'s value is the [pop] method's `result` /// parameter. /// /// For example if we wanted to ask the user to press 'OK' to confirm an /// operation we could `await` the result of [Navigator.push]: /// -/// ``` -/// bool value = await Navigator.of(context).push(new MaterialPageRoute( +/// ```dart +/// bool value = await Navigator.push(context, new MaterialPageRoute( /// builder: (BuildContext context) { /// return new Center( /// child: new GestureDetector( /// child: new Text('OK'), -/// onTap: () { Navigator.of(context).pop(true); } +/// onTap: () { Navigator.pop(context, true); } /// ), /// ); /// } /// )); /// ``` +/// /// If the user presses 'OK' then value will be true. If the user backs /// out of the route, for example by pressing the Scaffold's back button, /// the value will be null. /// -/// When a route is used to return a value, the route's type parameter -/// must match the type of [pop]'s result. That's why we've used -/// `MaterialPageRoute` instead of `MaterialPageRoute`. +/// When a route is used to return a value, the route's type parameter must +/// match the type of [pop]'s result. That's why we've used +/// `MaterialPageRoute` instead of `MaterialPageRoute` or just +/// `MaterialPageRoute`. (If you prefer to not specify the types, though, that's +/// fine too.) /// /// ### Popup routes /// @@ -442,18 +523,18 @@ typedef bool RoutePredicate(Route route); /// transition employed to show the route, the color and behavior of the route's /// modal barrier, and other aspects of the route. /// -/// The PageRouteBuilder class makes it possible to define a custom route +/// The [PageRouteBuilder] class makes it possible to define a custom route /// in terms of callbacks. Here's an example that rotates and fades its child /// when the route appears or disappears. This route does not obscure the entire /// screen because it specifies `opaque: false`, just as a popup route does. /// /// ```dart -/// Navigator.of(context).push(new PageRouteBuilder( +/// Navigator.push(context, new PageRouteBuilder( /// opaque: false, /// pageBuilder: (BuildContext context, _, __) { /// return new Center(child: new Text('My PageRoute')); /// }, -/// transitionsBuilder: (_, Animation animation, __, Widget child) { +/// transitionsBuilder: (___, Animation animation, ____, Widget child) { /// return new FadeTransition( /// opacity: animation, /// child: new RotationTransition( @@ -522,26 +603,13 @@ class Navigator extends StatefulWidget { /// application was started with. static const String defaultRouteName = '/'; - /// Push a named route onto the navigator that most tightly encloses the given context. + /// Push a named route onto the navigator that most tightly encloses the given + /// context. /// + /// {@template flutter.widgets.navigator.pushNamed} /// The route name will be passed to that navigator's [onGenerateRoute] /// callback. The returned route will be pushed into the navigator. /// - /// Returns a [Future] that completes to the `result` value passed to [pop] - /// when the pushed route is popped off the navigator. - /// - /// Typical usage is as follows: - /// - /// ```dart - /// Navigator.pushNamed(context, '/nyc/1776'); - /// ``` - static Future pushNamed(BuildContext context, String routeName) { - return Navigator.of(context).pushNamed(routeName); - } - - /// Adds the given route to the history of the navigator that most tightly - /// encloses the given context, and transitions to it. - /// /// The new route and the previous route (if any) are notified (see /// [Route.didPush] and [Route.didChangeNext]). If the [Navigator] has any /// [Navigator.observers], they will be notified as well (see @@ -552,14 +620,398 @@ class Navigator extends StatefulWidget { /// /// Returns a [Future] that completes to the `result` value passed to [pop] /// when the pushed route is popped off the navigator. + /// + /// The `T` type argument is the type of the return value of the route. + /// {@endtemplate} + /// + /// ## Sample code + /// + /// Typical usage is as follows: + /// + /// ```dart + /// void _didPushButton() { + /// Navigator.pushNamed(context, '/nyc/1776'); + /// } + /// ``` @optionalTypeArgs - static Future push(BuildContext context, Route route) { + static Future pushNamed(BuildContext context, String routeName) { + return Navigator.of(context).pushNamed(routeName); + } + + /// Replace the current route of the navigator that most tightly encloses the + /// given context by pushing the route named [routeName] and then disposing + /// the previous route once the new route has finished animating in. + /// + /// {@template flutter.widgets.navigator.pushReplacementNamed} + /// If non-null, `result` will be used as the result of the route that is + /// removed; the future that had been returned from pushing that old route + /// will complete with `result`. Routes such as dialogs or popup menus + /// typically use this mechanism to return the value selected by the user to + /// the widget that created their route. The type of `result`, if provided, + /// must match the type argument of the class of the old route (`TO`). + /// + /// The route name will be passed to the navigator's [onGenerateRoute] + /// callback. The returned route will be pushed into the navigator. + /// + /// The new route and the route below the removed route are notified (see + /// [Route.didPush] and [Route.didChangeNext]). If the [Navigator] has any + /// [Navigator.observers], they will be notified as well (see + /// [NavigatorObserver.didReplace]). The removed route is notified once the + /// new route has finished animating (see [Route.didComplete]). The removed + /// route's exit animation is not run (see [popAndPushNamed] for a variant + /// that does animated the removed route). + /// + /// Ongoing gestures within the current route are canceled when a new route is + /// pushed. + /// + /// Returns a [Future] that completes to the `result` value passed to [pop] + /// when the pushed route is popped off the navigator. + /// + /// The `T` type argument is the type of the return value of the new route, + /// and `TO` is the type of the return value of the old route. + /// {@endtemplate} + /// + /// ## Sample code + /// + /// Typical usage is as follows: + /// + /// ```dart + /// void _showNext() { + /// Navigator.pushReplacementNamed(context, '/jouett/1781'); + /// } + /// ``` + @optionalTypeArgs + static Future pushReplacementNamed(BuildContext context, String routeName, { TO result }) { + return Navigator.of(context).pushReplacementNamed(routeName, result: result); + } + + /// Pop the current route off the navigator that most tightly encloses the + /// given context and push a named route in its place. + /// + /// {@template flutter.widgets.navigator.popAndPushNamed} + /// If non-null, `result` will be used as the result of the route that is + /// popped; the future that had been returned from pushing the popped route + /// will complete with `result`. Routes such as dialogs or popup menus + /// typically use this mechanism to return the value selected by the user to + /// the widget that created their route. The type of `result`, if provided, + /// must match the type argument of the class of the popped route (`TO`). + /// + /// The route name will be passed to the navigator's [onGenerateRoute] + /// callback. The returned route will be pushed into the navigator. + /// + /// The new route, the old route, and the route below the old route (if any) + /// are all notified (see [Route.didPop], [Route.didComplete], + /// [Route.didPopNext], [Route.didPush], and [Route.didChangeNext]). If the + /// [Navigator] has any [Navigator.observers], they will be notified as well + /// (see [NavigatorObserver.didPop] and [NavigatorObservers.didPush]). The + /// animations for the pop and the push are performed simultaneously, so the + /// route below may be briefly visible even if both the old route and the new + /// route are opaque (see [TransitionRoute.opaque]). + /// + /// Ongoing gestures within the current route are canceled when a new route is + /// pushed. + /// + /// Returns a [Future] that completes to the `result` value passed to [pop] + /// when the pushed route is popped off the navigator. + /// + /// The `T` type argument is the type of the return value of the new route, + /// and `TO` is the return value type of the old route. + /// {@endtemplate} + /// + /// ## Sample code + /// + /// Typical usage is as follows: + /// + /// ```dart + /// void _selectNewYork() { + /// Navigator.popAndPushNamed(context, '/nyc/1776'); + /// } + /// ``` + @optionalTypeArgs + static Future popAndPushNamed(BuildContext context, String routeName, { TO result }) { + return Navigator.of(context).popAndPushNamed(routeName, result: result); + } + + /// Push the route with the given name onto the navigator that most tightly + /// encloses the given context, and then remove all the previous routes until + /// the `predicate` returns true. + /// + /// {@template flutter.widgets.navigator.pushNamedAndRemoveUntil} + /// The predicate may be applied to the same route more than once if + /// [Route.willHandlePopInternally] is true. + /// + /// To remove routes until a route with a certain name, use the + /// [RoutePredicate] returned from [ModalRoute.withName]. + /// + /// To remove all the routes below the pushed route, use a [RoutePredicate] + /// that always returns false (e.g. `(Route route) => false`). + /// + /// The removed routes are removed without being completed, so this method + /// does not take a return value argument. + /// + /// The new route's name (`routeName`) will be passed to the navigator's + /// [onGenerateRoute] callback. The returned route will be pushed into the + /// navigator. + /// + /// The new route and the route below the bottommost removed route (which + /// becomes the route below the new route) are notified (see [Route.didPush] + /// and [Route.didChangeNext]). If the [Navigator] has any + /// [Navigator.observers], they will be notified as well (see + /// [NavigatorObservers.didPush] and [NavigatorObservers.didRemove]). The + /// removed routes are disposed, without being notified, once the new route + /// has finished animating. The futures that had been returned from pushing + /// those routes will not complete. + /// + /// Ongoing gestures within the current route are canceled when a new route is + /// pushed. + /// + /// Returns a [Future] that completes to the `result` value passed to [pop] + /// when the pushed route is popped off the navigator. + /// + /// The `T` type argument is the type of the return value of the new route. + /// {@endtemplate} + /// + /// ## Sample code + /// + /// Typical usage is as follows: + /// + /// ```dart + /// void _resetToCalendar() { + /// Navigator.pushNamedAndRemoveUntil(context, '/calendar', ModalRoute.withName('/')); + /// } + /// ``` + @optionalTypeArgs + static Future pushNamedAndRemoveUntil(BuildContext context, String newRouteName, RoutePredicate predicate) { + return Navigator.of(context).pushNamedAndRemoveUntil(newRouteName, predicate); + } + + /// Push the given route onto the navigator that most tightly encloses the + /// given context. + /// + /// {@template flutter.widgets.navigator.push} + /// The new route and the previous route (if any) are notified (see + /// [Route.didPush] and [Route.didChangeNext]). If the [Navigator] has any + /// [Navigator.observers], they will be notified as well (see + /// [NavigatorObserver.didPush]). + /// + /// Ongoing gestures within the current route are canceled when a new route is + /// pushed. + /// + /// Returns a [Future] that completes to the `result` value passed to [pop] + /// when the pushed route is popped off the navigator. + /// + /// The `T` type argument is the type of the return value of the route. + /// {@endtemplate} + /// + /// ## Sample code + /// + /// Typical usage is as follows: + /// + /// ```dart + /// void _openMyPage() { + /// Navigator.push(context, new MaterialPageRoute(builder: (BuildContext context) => new MyPage())); + /// } + /// ``` + @optionalTypeArgs + static Future push(BuildContext context, Route route) { return Navigator.of(context).push(route); } - /// Returns the value of the current route's [Route.willPop] method. This - /// method is typically called before a user-initiated [pop]. For example on - /// Android it's called by the binding for the system's back button. + /// Replace the current route of the navigator that most tightly encloses the + /// given context by pushing the given route and then disposing the previous + /// route once the new route has finished animating in. + /// + /// {@template flutter.widgets.navigator.pushReplacement} + /// If non-null, `result` will be used as the result of the route that is + /// removed; the future that had been returned from pushing that old route will + /// complete with `result`. Routes such as dialogs or popup menus typically + /// use this mechanism to return the value selected by the user to the widget + /// that created their route. The type of `result`, if provided, must match + /// the type argument of the class of the old route (`TO`). + /// + /// The new route and the route below the removed route are notified (see + /// [Route.didPush] and [Route.didChangeNext]). If the [Navigator] has any + /// [Navigator.observers], they will be notified as well (see + /// [NavigatorObserver.didReplace]). The removed route is notified once the + /// new route has finished animating (see [Route.didComplete]). + /// + /// Ongoing gestures within the current route are canceled when a new route is + /// pushed. + /// + /// Returns a [Future] that completes to the `result` value passed to [pop] + /// when the pushed route is popped off the navigator. + /// + /// The `T` type argument is the type of the return value of the new route, + /// and `TO` is the type of the return value of the old route. + /// {@endtemplate} + /// + /// ## Sample code + /// + /// Typical usage is as follows: + /// + /// ```dart + /// void _completeLogin() { + /// Navigator.pushReplacement(context, new MaterialPageRoute(builder: (BuildContext context) => new MyHomePage())); + /// } + /// ``` + @optionalTypeArgs + static Future pushReplacement(BuildContext context, Route newRoute, { TO result }) { + return Navigator.of(context).pushReplacement(newRoute, result: result); + } + + /// Push the given route onto the navigator that most tightly encloses the + /// given context, and then remove all the previous routes until the + /// `predicate` returns true. + /// + /// {@template flutter.widgets.navigator.pushAndRemoveUntil} + /// The predicate may be applied to the same route more than once if + /// [Route.willHandlePopInternally] is true. + /// + /// To remove routes until a route with a certain name, use the + /// [RoutePredicate] returned from [ModalRoute.withName]. + /// + /// To remove all the routes below the pushed route, use a [RoutePredicate] + /// that always returns false (e.g. `(Route route) => false`). + /// + /// The removed routes are removed without being completed, so this method + /// does not take a return value argument. + /// + /// The new route and the route below the bottommost removed route (which + /// becomes the route below the new route) are notified (see [Route.didPush] + /// and [Route.didChangeNext]). If the [Navigator] has any + /// [Navigator.observers], they will be notified as well (see + /// [NavigatorObservers.didPush] and [NavigatorObservers.didRemove]). The + /// removed routes are disposed, without being notified, once the new route + /// has finished animating. The futures that had been returned from pushing + /// those routes will not complete. + /// + /// Ongoing gestures within the current route are canceled when a new route is + /// pushed. + /// + /// Returns a [Future] that completes to the `result` value passed to [pop] + /// when the pushed route is popped off the navigator. + /// + /// The `T` type argument is the type of the return value of the new route. + /// {@endtemplate} + /// + /// ## Sample code + /// + /// Typical usage is as follows: + /// + /// ```dart + /// void _finishAccountCreation() { + /// Navigator.pushAndRemoveUntil( + /// context, + /// new MaterialPageRoute(builder: (BuildContext context) => new MyHomePage()), + /// ModalRoute.withName('/'), + /// ); + /// } + /// ``` + @optionalTypeArgs + static Future pushAndRemoveUntil(BuildContext context, Route newRoute, RoutePredicate predicate) { + return Navigator.of(context).pushAndRemoveUntil(newRoute, predicate); + } + + /// Replaces a route on the navigator that most tightly encloses the given + /// context with a new route. + /// + /// {@template flutter.widgets.navigator.replace} + /// The old route must not be current visible, as this method skips the + /// animations and therefore the removal would be jarring if it was visible. + /// To replace the top-most route, consider [pushReplacement] instead, which + /// _does_ animate the new route, and delays removing the old route until the + /// new route has finished animating. + /// + /// The removed route is removed without being completed, so this method does + /// not take a return value argument. + /// + /// The new route, the route below the new route (if any), and the route above + /// the new route, are all notified (see [Route.didReplace], + /// [Route.didChangeNext], and [Route.didChangePrevious]). If the [Navigator] + /// has any [Navigator.observers], they will be notified as well (see + /// [NavigatorObservers.didReplace]). The removed route is disposed without + /// being notified. The future that had been returned from pushing that routes + /// will not complete. + /// + /// This can be useful in combination with [removeRouteBelow] when building a + /// non-linear user experience. + /// + /// The `T` type argument is the type of the return value of the new route. + /// {@endtemplate} + /// + /// See also: + /// + /// * [replaceRouteBelow], which is the same but identifies the route to be + /// removed by reference to the route above it, rather than directly. + @optionalTypeArgs + static void replace(BuildContext context, { @required Route oldRoute, @required Route newRoute }) { + return Navigator.of(context).replace(oldRoute: oldRoute, newRoute: newRoute); + } + + /// Replaces a route on the navigator that most tightly encloses the given + /// context with a new route. The route to be replaced is the one below the + /// given `anchorRoute`. + /// + /// {@template flutter.widgets.navigator.replaceRouteBelow} + /// The old route must not be current visible, as this method skips the + /// animations and therefore the removal would be jarring if it was visible. + /// To replace the top-most route, consider [pushReplacement] instead, which + /// _does_ animate the new route, and delays removing the old route until the + /// new route has finished animating. + /// + /// The removed route is removed without being completed, so this method does + /// not take a return value argument. + /// + /// The new route, the route below the new route (if any), and the route above + /// the new route, are all notified (see [Route.didReplace], + /// [Route.didChangeNext], and [Route.didChangePrevious]). If the [Navigator] + /// has any [Navigator.observers], they will be notified as well (see + /// [NavigatorObservers.didReplace]). The removed route is disposed without + /// being notified. The future that had been returned from pushing that routes + /// will not complete. + /// + /// The `T` type argument is the type of the return value of the new route. + /// {@endtemplate} + /// + /// See also: + /// + /// * [replace], which is the same but identifies the route to be removed + /// directly. + @optionalTypeArgs + static void replaceRouteBelow(BuildContext context, { @required Route anchorRoute, Route newRoute }) { + return Navigator.of(context).replaceRouteBelow(anchorRoute: anchorRoute, newRoute: newRoute); + } + + /// Whether the navigator that most tightly encloses the given context can be + /// popped. + /// + /// {@template flutter.widgets.navigator.canPop} + /// The initial route cannot be popped off the navigator, which implies that + /// this function returns true only if popping the navigator would not remove + /// the initial route. + /// + /// If there is no [Navigator] in scope, returns false. + /// {@endtemplate} + /// + /// See also: + /// + /// * [Route.isFirst], which returns true for routes for which [canPop] + /// returns false. + static bool canPop(BuildContext context) { + final NavigatorState navigator = Navigator.of(context, nullOk: true); + return navigator != null && navigator.canPop(); + } + + /// Returns the value of the current route's [Route.willPop] method for the + /// navigator that most tightly encloses the given context. + /// + /// {@template flutter.widgets.navigator.maybePop} + /// This method is typically called before a user-initiated [pop]. For example + /// on Android it's called by the binding for the system's back button. + /// + /// The `T` type argument is the type of the return value of the current + /// route. + /// {@endtemplate} /// /// See also: /// @@ -567,138 +1019,140 @@ class Navigator extends StatefulWidget { /// to veto a [pop] initiated by the app's back button. /// * [ModalRoute], which provides a `scopedWillPopCallback` that can be used /// to define the route's `willPop` method. - static Future maybePop(BuildContext context, [ dynamic result ]) { - return Navigator.of(context).maybePop(result); + @optionalTypeArgs + static Future maybePop(BuildContext context, [ T result ]) { + return Navigator.of(context).maybePop(result); } - /// Pop a route off the navigator that most tightly encloses the given context. + /// Pop the top-most route off the navigator that most tightly encloses the + /// given context. /// - /// Tries to removes the current route, calling its didPop() method. If that - /// method returns false, then nothing else happens. Otherwise, the observer - /// (if any) is notified using its didPop() method, and the previous route is - /// notified using [Route.didChangeNext]. + /// {@template flutter.widgets.navigator.pop} + /// The current route's [Route.didPop] method is called first. If that method + /// returns false, then this method returns true but nothing else is changed + /// (the route is expected to have popped some internal state; see e.g. + /// [LocalHistoryRoute]). Otherwise, the rest of this description applies. /// - /// If non-null, `result` will be used as the result of the route. Routes - /// such as dialogs or popup menus typically use this mechanism to return the - /// value selected by the user to the widget that created their route. The - /// type of `result`, if provided, must match the type argument of the class - /// of the current route. (In practice, this is usually "dynamic".) + /// If non-null, `result` will be used as the result of the route that is + /// popped; the future that had been returned from pushing the popped route + /// will complete with `result`. Routes such as dialogs or popup menus + /// typically use this mechanism to return the value selected by the user to + /// the widget that created their route. The type of `result`, if provided, + /// must match the type argument of the class of the popped route (`T`). /// - /// Returns true if a route was popped; returns false if there are no further - /// previous routes. + /// The popped route and the route below it are notified (see [Route.didPop], + /// [Route.didComplete], and [Route.didPopNext]). If the [Navigator] has any + /// [Navigator.observers], they will be notified as well (see + /// [NavigatorObserver.didPop]). /// - /// Typical usage is as follows: + /// The `T` type argument is the type of the return value of the popped route. + /// + /// Returns true if a route was popped (including if [Route.didPop] returned + /// false); returns false if there are no further previous routes. + /// {@endtemplate} + /// + /// ## Sample code + /// + /// Typical usage for closing a route is as follows: /// /// ```dart - /// Navigator.pop(context); + /// void _close() { + /// Navigator.pop(context); + /// } /// ``` - static bool pop(BuildContext context, [ dynamic result ]) { - return Navigator.of(context).pop(result); + /// + /// A dialog box might be closed with a result: + /// + /// ```dart + /// void _accept() { + /// Navigator.pop(context, true); // dialog returns true + /// } + /// ``` + @optionalTypeArgs + static bool pop(BuildContext context, [ T result ]) { + return Navigator.of(context).pop(result); } - /// Calls [pop] repeatedly until the predicate returns true. + /// Calls [pop] repeatedly on the navigator that most tightly encloses the + /// given context until the predicate returns true. /// + /// {@template flutter.widgets.navigator.popUntil} /// The predicate may be applied to the same route more than once if /// [Route.willHandlePopInternally] is true. /// /// To pop until a route with a certain name, use the [RoutePredicate] /// returned from [ModalRoute.withName]. /// + /// The routes are closed with null as their `return` value. + /// + /// See [pop] for more details of the semantics of popping a route. + /// {@endtemplate} + /// + /// ## Sample code + /// /// Typical usage is as follows: /// /// ```dart - /// Navigator.popUntil(context, ModalRoute.withName('/login')); + /// void _logout() { + /// Navigator.popUntil(context, ModalRoute.withName('/login')); + /// } /// ``` static void popUntil(BuildContext context, RoutePredicate predicate) { Navigator.of(context).popUntil(predicate); } - /// Whether the navigator that most tightly encloses the given context can be - /// popped. + /// Immediately remove `route` from the navigator that most tightly encloses + /// the given context, and [Route.dispose] it. /// - /// The initial route cannot be popped off the navigator, which implies that - /// this function returns true only if popping the navigator would not remove - /// the initial route. - static bool canPop(BuildContext context) { - final NavigatorState navigator = context.ancestorStateOfType(const TypeMatcher()); - return navigator != null && navigator.canPop(); - } - - /// Executes a simple transaction that both pops the current route off and - /// pushes a named route into the navigator that most tightly encloses the - /// given context. + /// {@template flutter.widgets.navigator.removeRoute} + /// The removed route is removed without being completed, so this method does + /// not take a return value argument. No animations are run as a result of + /// this method call. /// - /// If non-null, `result` will be used as the result of the route that is - /// popped. Routes such as dialogs or popup menus typically use this mechanism - /// to return the value selected by the user to the widget that created their - /// route. The type of `result`, if provided, must match the type argument of - /// the class of the current route. (In practice, this is usually "dynamic".) + /// The routes below and above the removed route are notified (see + /// [Route.didChangeNext] and [Route.didChangePrevious]). If the [Navigator] + /// has any [Navigator.observers], they will be notified as well (see + /// [NavigatorObserver.didRemove]). The removed route is disposed without + /// being notified. The future that had been returned from pushing that routes + /// will not complete. /// - /// Returns a [Future] that completes to the `result` value passed to [pop] - /// when the pushed route is popped off the navigator. + /// The given `route` must be in the history; this method will throw an + /// exception if it is not. /// - /// Typical usage is as follows: + /// Ongoing gestures within the current route are canceled. + /// {@endtemplate} /// - /// ```dart - /// Navigator.popAndPushNamed(context, '/nyc/1776'); - /// ``` - static Future popAndPushNamed(BuildContext context, String routeName, { dynamic result }) { - final NavigatorState navigator = Navigator.of(context); - navigator.pop(result); - return navigator.pushNamed(routeName); - } - - /// Replace the current route by pushing the route named [routeName] and then - /// disposing the previous route. - /// - /// The route name will be passed to the navigator's [onGenerateRoute] - /// callback. The returned route will be pushed into the navigator. - /// - /// Returns a [Future] that completes to the `result` value passed to [pop] - /// when the pushed route is popped off the navigator. - /// - /// Typical usage is as follows: - /// - /// ```dart - /// Navigator.of(context).pushReplacementNamed('/jouett/1781'); - /// ``` - static Future pushReplacementNamed(BuildContext context, String routeName, { dynamic result }) { - return Navigator.of(context).pushReplacementNamed(routeName, result: result); - } - - /// Replace the current route by pushing [route] and then disposing the - /// current route. - /// - /// The new route and the route below the new route (if any) are notified - /// (see [Route.didPush] and [Route.didChangeNext]). The navigator observer - /// is not notified about the old route. The old route is disposed (see - /// [Route.dispose]). - /// - /// If a [result] is provided, it will be the return value of the old route, - /// as if the old route had been popped. - /// - /// Returns a [Future] that completes to the `result` value passed to [pop] - /// when the pushed route is popped off the navigator. - static Future pushReplacement(BuildContext context, Route route, { dynamic result }) { - return Navigator.of(context).pushReplacement(route, result: result); - } - - /// Immediately remove `route` and [Route.dispose] it. - /// - /// The route's animation does not run and the future returned from pushing - /// the route will not complete. Ongoing input gestures are cancelled. If - /// the [Navigator] has any [Navigator.observers], they will be notified with - /// [NavigatorObserver.didRemove]. - /// - /// The routes before and after the removed route, if any, are notified with - /// [Route.didChangeNext] and [Route.didChangePrevious]. - /// - /// This method is used to dismiss dropdown menus that are up when the screen's - /// orientation changes. + /// This method is used, for example, to instantly dismiss dropdown menus that + /// are up when the screen's orientation changes. static void removeRoute(BuildContext context, Route route) { return Navigator.of(context).removeRoute(route); } + /// Immediately remove a route from the navigator that most tightly encloses + /// the given context, and [Route.dispose] it. The route to be replaced is the + /// one below the given `anchorRoute`. + /// + /// {@template flutter.widgets.navigator.removeRouteBelow} + /// The removed route is removed without being completed, so this method does + /// not take a return value argument. No animations are run as a result of + /// this method call. + /// + /// The routes below and above the removed route are notified (see + /// [Route.didChangeNext] and [Route.didChangePrevious]). If the [Navigator] + /// has any [Navigator.observers], they will be notified as well (see + /// [NavigatorObserver.didRemove]). The removed route is disposed without + /// being notified. The future that had been returned from pushing that routes + /// will not complete. + /// + /// The given `anchorRoute` must be in the history and must have a route below + /// it; this method will throw an exception if it is not or does not. + /// + /// Ongoing gestures within the current route are canceled. + /// {@endtemplate} + static void removeRouteBelow(BuildContext context, Route anchorRoute) { + return Navigator.of(context).removeRouteBelow(anchorRoute); + } + /// The state from the closest instance of this class that encloses the given context. /// /// Typical usage is as follows: @@ -715,16 +1169,18 @@ class Navigator extends StatefulWidget { /// instances of [Navigator]. static NavigatorState of( BuildContext context, { - bool rootNavigator: false + bool rootNavigator: false, + bool nullOk: false, }) { final NavigatorState navigator = rootNavigator ? context.rootAncestorStateOfType(const TypeMatcher()) : context.ancestorStateOfType(const TypeMatcher()); assert(() { - if (navigator == null) { + if (navigator == null && !nullOk) { throw new FlutterError( 'Navigator operation requested with a context that does not include a Navigator.\n' - 'The context used to push or pop routes from the Navigator must be that of a widget that is a descendant of a Navigator widget.' + 'The context used to push or pop routes from the Navigator must be that of a ' + 'widget that is a descendant of a Navigator widget.' ); } return true; @@ -762,7 +1218,7 @@ class NavigatorState extends State with TickerProviderStateMixin { Navigator.defaultRouteName, ]; final List> plannedInitialRoutes = >[ - _routeNamed(Navigator.defaultRouteName, allowNull: true), + _routeNamed(Navigator.defaultRouteName, allowNull: true), ]; final List routeParts = initialRouteName.split('/'); if (initialRouteName.isNotEmpty) { @@ -770,7 +1226,7 @@ class NavigatorState extends State with TickerProviderStateMixin { for (String part in routeParts) { routeName += '/$part'; plannedInitialRouteNames.add(routeName); - plannedInitialRoutes.add(_routeNamed(routeName, allowNull: true)); + plannedInitialRoutes.add(_routeNamed(routeName, allowNull: true)); } } if (plannedInitialRoutes.contains(null)) { @@ -790,15 +1246,15 @@ class NavigatorState extends State with TickerProviderStateMixin { ); return true; }()); - push(_routeNamed(Navigator.defaultRouteName)); + push(_routeNamed(Navigator.defaultRouteName)); } else { plannedInitialRoutes.forEach(push); } } else { - Route route; + Route route; if (initialRouteName != Navigator.defaultRouteName) - route = _routeNamed(initialRouteName, allowNull: true); - route ??= _routeNamed(Navigator.defaultRouteName); + route = _routeNamed(initialRouteName, allowNull: true); + route ??= _routeNamed(Navigator.defaultRouteName); push(route); } for (Route route in _history) @@ -816,6 +1272,8 @@ class NavigatorState extends State with TickerProviderStateMixin { observer._navigator = this; } } + for (Route route in _history) + route.changedExternalState(); } @override @@ -847,14 +1305,14 @@ class NavigatorState extends State with TickerProviderStateMixin { bool _debugLocked = false; // used to prevent re-entrant calls to push, pop, and friends - Route _routeNamed(String name, { bool allowNull: false }) { + Route _routeNamed(String name, { bool allowNull: false }) { assert(!_debugLocked); assert(name != null); final RouteSettings settings = new RouteSettings( name: name, isInitialRoute: _history.isEmpty, ); - Route route = widget.onGenerateRoute(settings); + Route route = widget.onGenerateRoute(settings); if (route == null && !allowNull) { assert(() { if (widget.onUnknownRoute == null) { @@ -887,106 +1345,132 @@ class NavigatorState extends State with TickerProviderStateMixin { /// Push a named route onto the navigator. /// - /// The route name will be passed to [Navigator.onGenerateRoute]. The returned - /// route will be pushed into the navigator. + /// {@macro flutter.widgets.navigator.pushNamed} /// - /// Returns a [Future] that completes to the `result` value passed to [pop] - /// when the pushed route is popped off the navigator. + /// ## Sample code /// /// Typical usage is as follows: /// /// ```dart - /// Navigator.of(context).pushNamed('/nyc/1776'); + /// void _aaronBurrSir() { + /// navigator.pushNamed('/nyc/1776'); + /// } /// ``` - Future pushNamed(String name) { - return push(_routeNamed(name)); + @optionalTypeArgs + Future pushNamed(String routeName) { + return push(_routeNamed(routeName)); } - /// Adds the given route to the navigator's history, and transitions to it. + /// Replace the current route of the navigator by pushing the route named + /// [routeName] and then disposing the previous route once the new route has + /// finished animating in. /// - /// The new route and the previous route (if any) are notified (see - /// [Route.didPush] and [Route.didChangeNext]). If the [Navigator] has any - /// [Navigator.observers], they will be notified as well (see - /// [NavigatorObserver.didPush]). + /// {@macro flutter.widgets.navigator.pushReplacementNamed} /// - /// Ongoing gestures within the current route are canceled when a new route is - /// pushed. + /// ## Sample code /// - /// Returns a [Future] that completes to the `result` value passed to [pop] - /// when the pushed route is popped off the navigator. - Future push(Route route) { + /// Typical usage is as follows: + /// + /// ```dart + /// void _startBike() { + /// navigator.pushReplacementNamed('/jouett/1781'); + /// } + /// ``` + @optionalTypeArgs + Future pushReplacementNamed(String routeName, { TO result }) { + return pushReplacement(_routeNamed(routeName), result: result); + } + + /// Pop the current route off the navigator and push a named route in its + /// place. + /// + /// {@macro flutter.widgets.navigator.popAndPushNamed} + /// + /// ## Sample code + /// + /// Typical usage is as follows: + /// + /// ```dart + /// void _begin() { + /// navigator.popAndPushNamed('/nyc/1776'); + /// } + /// ``` + @optionalTypeArgs + Future popAndPushNamed(String routeName, { TO result }) { + pop(result); + return pushNamed(routeName); + } + + /// Push the route with the given name onto the navigator, and then remove all + /// the previous routes until the `predicate` returns true. + /// + /// {@macro flutter.widgets.navigator.pushNamedAndRemoveUntil} + /// + /// ## Sample code + /// + /// Typical usage is as follows: + /// + /// ```dart + /// void _handleOpenCalendar() { + /// navigator.pushNamedAndRemoveUntil('/calendar', ModalRoute.withName('/')); + /// } + /// ``` + @optionalTypeArgs + Future pushNamedAndRemoveUntil(String newRouteName, RoutePredicate predicate) { + return pushAndRemoveUntil(_routeNamed(newRouteName), predicate); + } + + /// Push the given route onto the navigator. + /// + /// {@macro flutter.widgets.navigator.push} + /// + /// ## Sample code + /// + /// Typical usage is as follows: + /// + /// ```dart + /// void _openPage() { + /// navigator.push(new MaterialPageRoute(builder: (BuildContext context) => new MyPage())); + /// } + /// ``` + @optionalTypeArgs + Future push(Route route) { assert(!_debugLocked); assert(() { _debugLocked = true; return true; }()); assert(route != null); assert(route._navigator == null); - setState(() { - final Route oldRoute = _history.isNotEmpty ? _history.last : null; - route._navigator = this; - route.install(_currentOverlayEntry); - _history.add(route); - route.didPush(); - route.didChangeNext(null); - if (oldRoute != null) - oldRoute.didChangeNext(route); - for (NavigatorObserver observer in widget.observers) - observer.didPush(route, oldRoute); - }); + final Route oldRoute = _history.isNotEmpty ? _history.last : null; + route._navigator = this; + route.install(_currentOverlayEntry); + _history.add(route); + route.didPush(); + route.didChangeNext(null); + if (oldRoute != null) + oldRoute.didChangeNext(route); + for (NavigatorObserver observer in widget.observers) + observer.didPush(route, oldRoute); assert(() { _debugLocked = false; return true; }()); _cancelActivePointers(); return route.popped; } - /// Replaces a route that is not currently visible with a new route. + /// Replace the current route of the navigator by pushing the given route and + /// then disposing the previous route once the new route has finished + /// animating in. /// - /// The new route and the route below the new route (if any) are notified - /// (see [Route.didReplace] and [Route.didChangeNext]). The navigator observer - /// is not notified. The old route is disposed (see [Route.dispose]). + /// {@macro flutter.widgets.navigator.pushReplacement} /// - /// This can be useful in combination with [removeRouteBelow] when building a - /// non-linear user experience. - void replace({ @required Route oldRoute, @required Route newRoute }) { - assert(!_debugLocked); - assert(oldRoute != null); - assert(newRoute != null); - if (oldRoute == newRoute) - return; - assert(() { _debugLocked = true; return true; }()); - assert(oldRoute._navigator == this); - assert(newRoute._navigator == null); - assert(oldRoute.overlayEntries.isNotEmpty); - assert(newRoute.overlayEntries.isEmpty); - assert(!overlay.debugIsVisible(oldRoute.overlayEntries.last)); - setState(() { - final int index = _history.indexOf(oldRoute); - assert(index >= 0); - newRoute._navigator = this; - newRoute.install(oldRoute.overlayEntries.last); - _history[index] = newRoute; - newRoute.didReplace(oldRoute); - if (index + 1 < _history.length) { - newRoute.didChangeNext(_history[index + 1]); - _history[index + 1].didChangePrevious(newRoute); - } else { - newRoute.didChangeNext(null); - } - if (index > 0) - _history[index - 1].didChangeNext(newRoute); - oldRoute.dispose(); - }); - assert(() { _debugLocked = false; return true; }()); - } - - /// Push the [newRoute] and dispose the old current Route. + /// ## Sample code /// - /// The new route and the route below the new route (if any) are notified - /// (see [Route.didPush] and [Route.didChangeNext]). The navigator observer - /// is not notified about the old route. The old route is disposed (see - /// [Route.dispose]). The new route is not notified when the old route - /// is removed (which happens when the new route's animation completes). + /// Typical usage is as follows: /// - /// If a [result] is provided, it will be the return value of the old route, - /// as if the old route had been popped. - Future pushReplacement(Route newRoute, { dynamic result }) { + /// ```dart + /// void _doOpenPage() { + /// navigator.pushReplacement(new MaterialPageRoute(builder: (BuildContext context) => new MyHomePage())); + /// } + /// ``` + @optionalTypeArgs + Future pushReplacement(Route newRoute, { TO result }) { assert(!_debugLocked); assert(() { _debugLocked = true; return true; }()); final Route oldRoute = _history.last; @@ -994,99 +1478,50 @@ class NavigatorState extends State with TickerProviderStateMixin { assert(oldRoute.overlayEntries.isNotEmpty); assert(newRoute._navigator == null); assert(newRoute.overlayEntries.isEmpty); - setState(() { - final int index = _history.length - 1; - assert(index >= 0); - assert(_history.indexOf(oldRoute) == index); - newRoute._navigator = this; - newRoute.install(_currentOverlayEntry); - _history[index] = newRoute; - newRoute.didPush().whenCompleteOrCancel(() { - // The old route's exit is not animated. We're assuming that the - // new route completely obscures the old one. - if (mounted) { - oldRoute - ..didComplete(result ?? oldRoute.currentResult) - ..dispose(); - } - }); - newRoute.didChangeNext(null); - if (index > 0) - _history[index - 1].didChangeNext(newRoute); - for (NavigatorObserver observer in widget.observers) - observer.didPush(newRoute, oldRoute); + final int index = _history.length - 1; + assert(index >= 0); + assert(_history.indexOf(oldRoute) == index); + newRoute._navigator = this; + newRoute.install(_currentOverlayEntry); + _history[index] = newRoute; + newRoute.didPush().whenCompleteOrCancel(() { + // The old route's exit is not animated. We're assuming that the + // new route completely obscures the old one. + if (mounted) { + oldRoute + ..didComplete(result ?? oldRoute.currentResult) + ..dispose(); + } }); + newRoute.didChangeNext(null); + if (index > 0) + _history[index - 1].didChangeNext(newRoute); + for (NavigatorObserver observer in widget.observers) + observer.didReplace(newRoute: newRoute, oldRoute: oldRoute); assert(() { _debugLocked = false; return true; }()); _cancelActivePointers(); return newRoute.popped; } - /// Push the route named [name] and dispose the old current route. + /// Push the given route onto the navigator, and then remove all the previous + /// routes until the `predicate` returns true. /// - /// The route name will be passed to [Navigator.onGenerateRoute]. The returned - /// route will be pushed into the navigator. + /// {@macro flutter.widgets.navigator.pushAndRemoveUntil} /// - /// Returns a [Future] that completes to the `result` value passed to [pop] - /// when the pushed route is popped off the navigator. - Future pushReplacementNamed(String name, { dynamic result }) { - return pushReplacement(_routeNamed(name), result: result); - } - - /// Replaces a route that is not currently visible with a new route. + /// ## Sample code /// - /// The route to be removed is the one below the given `anchorRoute`. That - /// route must not be the first route in the history. + /// Typical usage is as follows: /// - /// In every other way, this acts the same as [replace]. - void replaceRouteBelow({ @required Route anchorRoute, Route newRoute }) { - assert(anchorRoute != null); - assert(anchorRoute._navigator == this); - assert(_history.indexOf(anchorRoute) > 0); - replace(oldRoute: _history[_history.indexOf(anchorRoute)-1], newRoute: newRoute); - } - - /// Removes the route below the given `anchorRoute`. The route to be removed - /// must not currently be visible. The `anchorRoute` must not be the first - /// route in the history. - /// - /// The removed route is disposed (see [Route.dispose]). The route prior to - /// the removed route, if any, is notified (see [Route.didChangeNext]). The - /// route above the removed route, if any, is also notified (see - /// [Route.didChangePrevious]). The navigator observer is not notified. - void removeRouteBelow(Route anchorRoute) { - assert(!_debugLocked); - assert(() { _debugLocked = true; return true; }()); - assert(anchorRoute._navigator == this); - final int index = _history.indexOf(anchorRoute) - 1; - assert(index >= 0); - final Route targetRoute = _history[index]; - assert(targetRoute._navigator == this); - assert(targetRoute.overlayEntries.isEmpty || !overlay.debugIsVisible(targetRoute.overlayEntries.last)); - setState(() { - _history.removeAt(index); - final Route nextRoute = index < _history.length ? _history[index] : null; - final Route previousRoute = index > 0 ? _history[index - 1] : null; - if (previousRoute != null) - previousRoute.didChangeNext(nextRoute); - if (nextRoute != null) - nextRoute.didChangePrevious(previousRoute); - targetRoute.dispose(); - }); - assert(() { _debugLocked = false; return true; }()); - } - - /// Push the given route and then remove all the previous routes until the - /// `predicate` returns true. - /// - /// The predicate may be applied to the same route more than once if - /// [Route.willHandlePopInternally] is true. - /// - /// To remove routes until a route with a certain name, use the - /// [RoutePredicate] returned from [ModalRoute.withName]. - /// - /// To remove all the routes before the pushed route, use a [RoutePredicate] - /// that always returns false. - Future pushAndRemoveUntil(Route newRoute, RoutePredicate predicate) { + /// ```dart + /// void _resetAndOpenPage() { + /// navigator.pushAndRemoveUntil( + /// new MaterialPageRoute(builder: (BuildContext context) => new MyHomePage()), + /// ModalRoute.withName('/'), + /// ); + /// } + /// ``` + @optionalTypeArgs + Future pushAndRemoveUntil(Route newRoute, RoutePredicate predicate) { assert(!_debugLocked); assert(() { _debugLocked = true; return true; }()); final List> removedRoutes = >[]; @@ -1098,58 +1533,114 @@ class NavigatorState extends State with TickerProviderStateMixin { } assert(newRoute._navigator == null); assert(newRoute.overlayEntries.isEmpty); - setState(() { - final Route oldRoute = _history.isNotEmpty ? _history.last : null; - newRoute._navigator = this; - newRoute.install(_currentOverlayEntry); - _history.add(newRoute); - newRoute.didPush().whenCompleteOrCancel(() { - if (mounted) { - for (Route route in removedRoutes) - route.dispose(); - } - }); - newRoute.didChangeNext(null); - if (oldRoute != null) - oldRoute.didChangeNext(newRoute); - for (NavigatorObserver observer in widget.observers) - observer.didPush(newRoute, oldRoute); + final Route oldRoute = _history.isNotEmpty ? _history.last : null; + newRoute._navigator = this; + newRoute.install(_currentOverlayEntry); + _history.add(newRoute); + newRoute.didPush().whenCompleteOrCancel(() { + if (mounted) { + for (Route route in removedRoutes) + route.dispose(); + } }); + newRoute.didChangeNext(null); + if (oldRoute != null) + oldRoute.didChangeNext(newRoute); + for (NavigatorObserver observer in widget.observers) { + observer.didPush(newRoute, oldRoute); + for (Route removedRoute in removedRoutes) + observer.didRemove(removedRoute, oldRoute); + } assert(() { _debugLocked = false; return true; }()); _cancelActivePointers(); return newRoute.popped; } - /// Push the route with the given name and then remove all the previous routes - /// until the `predicate` returns true. + /// Replaces a route on the navigator with a new route. /// - /// The predicate may be applied to the same route more than once if - /// [Route.willHandlePopInternally] is true. - /// - /// To remove routes until a route with a certain name, use the - /// [RoutePredicate] returned from [ModalRoute.withName]. - /// - /// To remove all the routes before the pushed route, use a [RoutePredicate] - /// that always returns false. - Future pushNamedAndRemoveUntil(String routeName, RoutePredicate predicate) { - return pushAndRemoveUntil(_routeNamed(routeName), predicate); - } - - /// Tries to pop the current route, first giving the active route the chance - /// to veto the operation using [Route.willPop]. This method is typically - /// called instead of [pop] when the user uses a back button. For example on - /// Android it's called by the binding for the system's back button. + /// {@macro flutter.widgets.navigator.replace} /// /// See also: /// - /// * [Form], which provides a [Form.onWillPop] callback that enables the form - /// to veto a [maybePop] initiated by the app's back button. - /// * [WillPopScope], a widget that hooks into the route's [Route.willPop] - /// mechanism. - /// * [ModalRoute], which has as a [ModalRoute.willPop] method that can be - /// defined by a list of [WillPopCallback]s. - Future maybePop([dynamic result]) async { - final Route route = _history.last; + /// * [replaceRouteBelow], which is the same but identifies the route to be + /// removed by reference to the route above it, rather than directly. + @optionalTypeArgs + void replace({ @required Route oldRoute, @required Route newRoute }) { + assert(!_debugLocked); + assert(oldRoute != null); + assert(newRoute != null); + if (oldRoute == newRoute) // ignore: unrelated_type_equality_checks, https://github.com/dart-lang/sdk/issues/32522 + return; + assert(() { _debugLocked = true; return true; }()); + assert(oldRoute._navigator == this); + assert(newRoute._navigator == null); + assert(oldRoute.overlayEntries.isNotEmpty); + assert(newRoute.overlayEntries.isEmpty); + assert(!overlay.debugIsVisible(oldRoute.overlayEntries.last)); + final int index = _history.indexOf(oldRoute); + assert(index >= 0); + newRoute._navigator = this; + newRoute.install(oldRoute.overlayEntries.last); + _history[index] = newRoute; + newRoute.didReplace(oldRoute); + if (index + 1 < _history.length) { + newRoute.didChangeNext(_history[index + 1]); + _history[index + 1].didChangePrevious(newRoute); + } else { + newRoute.didChangeNext(null); + } + if (index > 0) + _history[index - 1].didChangeNext(newRoute); + for (NavigatorObserver observer in widget.observers) + observer.didReplace(newRoute: newRoute, oldRoute: oldRoute); + oldRoute.dispose(); + assert(() { _debugLocked = false; return true; }()); + } + + /// Replaces a route on the navigator with a new route. The route to be + /// replaced is the one below the given `anchorRoute`. + /// + /// {@macro flutter.widgets.navigator.replaceRouteBelow} + /// + /// See also: + /// + /// * [replace], which is the same but identifies the route to be removed + /// directly. + @optionalTypeArgs + void replaceRouteBelow({ @required Route anchorRoute, Route newRoute }) { + assert(anchorRoute != null); + assert(anchorRoute._navigator == this); + assert(_history.indexOf(anchorRoute) > 0); + replace(oldRoute: _history[_history.indexOf(anchorRoute) - 1], newRoute: newRoute); + } + + /// Whether the navigator can be popped. + /// + /// {@macro flutter.widgets.navigator.canPop} + /// + /// See also: + /// + /// * [Route.isFirst], which returns true for routes for which [canPop] + /// returns false. + bool canPop() { + assert(_history.isNotEmpty); + return _history.length > 1 || _history[0].willHandlePopInternally; + } + + /// Returns the value of the current route's [Route.willPop] method for the + /// navigator. + /// + /// {@macro flutter.widgets.navigator.maybePop} + /// + /// See also: + /// + /// * [Form], which provides an `onWillPop` callback that enables the form + /// to veto a [pop] initiated by the app's back button. + /// * [ModalRoute], which provides a `scopedWillPopCallback` that can be used + /// to define the route's `willPop` method. + @optionalTypeArgs + Future maybePop([ T result ]) async { + final Route route = _history.last; assert(route._navigator == this); final RoutePopDisposition disposition = await route.willPop(); if (disposition != RoutePopDisposition.bubble && mounted) { @@ -1160,20 +1651,29 @@ class NavigatorState extends State with TickerProviderStateMixin { return false; } - /// Removes the top route in the [Navigator]'s history. + /// Pop the top-most route off the navigator. /// - /// If an argument is provided, that argument will be the return value of the - /// route (see [Route.didPop]). + /// {@macro flutter.widgets.navigator.pop} /// - /// If there are any routes left on the history, the top remaining route is - /// notified (see [Route.didPopNext]), and the method returns true. In that - /// case, if the [Navigator] has any [Navigator.observers], they will be notified - /// as well (see [NavigatorObserver.didPop]). Otherwise, if the popped route - /// was the last route, the method returns false. + /// ## Sample code /// - /// Ongoing gestures within the current route are canceled when a route is - /// popped. - bool pop([dynamic result]) { + /// Typical usage for closing a route is as follows: + /// + /// ```dart + /// void _handleClose() { + /// navigator.pop(); + /// } + /// ``` + /// + /// A dialog box might be closed with a result: + /// + /// ```dart + /// void _handleAccept() { + /// navigator.pop(true); // dialog returns true + /// } + /// ``` + @optionalTypeArgs + bool pop([ T result ]) { assert(!_debugLocked); assert(() { _debugLocked = true; return true; }()); final Route route = _history.last; @@ -1183,20 +1683,15 @@ class NavigatorState extends State with TickerProviderStateMixin { if (route.didPop(result ?? route.currentResult)) { assert(debugPredictedWouldPop); if (_history.length > 1) { - setState(() { - // We use setState to guarantee that we'll rebuild, since the routes - // can't do that for themselves, even if they have changed their own - // state (e.g. ModalScope.isCurrent). - _history.removeLast(); - // If route._navigator is null, the route called finalizeRoute from - // didPop, which means the route has already been disposed and doesn't - // need to be added to _poppedRoutes for later disposal. - if (route._navigator != null) - _poppedRoutes.add(route); - _history.last.didPopNext(route); - for (NavigatorObserver observer in widget.observers) - observer.didPop(route, _history.last); - }); + _history.removeLast(); + // If route._navigator is null, the route called finalizeRoute from + // didPop, which means the route has already been disposed and doesn't + // need to be added to _poppedRoutes for later disposal. + if (route._navigator != null) + _poppedRoutes.add(route); + _history.last.didPopNext(route); + for (NavigatorObserver observer in widget.observers) + observer.didPop(route, _history.last); } else { assert(() { _debugLocked = false; return true; }()); return false; @@ -1209,15 +1704,27 @@ class NavigatorState extends State with TickerProviderStateMixin { return true; } - /// Immediately remove `route` and [Route.dispose] it. + /// Calls [pop] repeatedly until the predicate returns true. /// - /// The route's animation does not run and the future returned from pushing - /// the route will not complete. Ongoing input gestures are cancelled. If - /// the [Navigator] has any [Navigator.observers], they will be notified with - /// [NavigatorObserver.didRemove]. + /// {@macro flutter.widgets.navigator.popUntil} /// - /// This method is used to dismiss dropdown menus that are up when the screen's - /// orientation changes. + /// ## Sample code + /// + /// Typical usage is as follows: + /// + /// ```dart + /// void _doLogout() { + /// navigator.popUntil(ModalRoute.withName('/login')); + /// } + /// ``` + void popUntil(RoutePredicate predicate) { + while (!predicate(_history.last)) + pop(); + } + + /// Immediately remove `route` from the navigator, and [Route.dispose] it. + /// + /// {@macro flutter.widgets.navigator.removeRoute} void removeRoute(Route route) { assert(route != null); assert(!_debugLocked); @@ -1227,18 +1734,40 @@ class NavigatorState extends State with TickerProviderStateMixin { assert(index != -1); final Route previousRoute = index > 0 ? _history[index - 1] : null; final Route nextRoute = (index + 1 < _history.length) ? _history[index + 1] : null; - setState(() { - _history.removeAt(index); - previousRoute?.didChangeNext(nextRoute); - nextRoute?.didChangePrevious(previousRoute); - for (NavigatorObserver observer in widget.observers) - observer.didRemove(route, previousRoute); - route.dispose(); - }); + _history.removeAt(index); + previousRoute?.didChangeNext(nextRoute); + nextRoute?.didChangePrevious(previousRoute); + for (NavigatorObserver observer in widget.observers) + observer.didRemove(route, previousRoute); + route.dispose(); assert(() { _debugLocked = false; return true; }()); _cancelActivePointers(); } + /// Immediately remove a route from the navigator, and [Route.dispose] it. The + /// route to be replaced is the one below the given `anchorRoute`. + /// + /// {@macro flutter.widgets.navigator.removeRouteBelow} + void removeRouteBelow(Route anchorRoute) { + assert(!_debugLocked); + assert(() { _debugLocked = true; return true; }()); + assert(anchorRoute._navigator == this); + final int index = _history.indexOf(anchorRoute) - 1; + assert(index >= 0); + final Route targetRoute = _history[index]; + assert(targetRoute._navigator == this); + assert(targetRoute.overlayEntries.isEmpty || !overlay.debugIsVisible(targetRoute.overlayEntries.last)); + _history.removeAt(index); + final Route nextRoute = index < _history.length ? _history[index] : null; + final Route previousRoute = index > 0 ? _history[index - 1] : null; + if (previousRoute != null) + previousRoute.didChangeNext(nextRoute); + if (nextRoute != null) + nextRoute.didChangePrevious(previousRoute); + targetRoute.dispose(); + assert(() { _debugLocked = false; return true; }()); + } + /// Complete the lifecycle for a route that has been popped off the navigator. /// /// When the navigator pops a route, the navigator retains a reference to the @@ -1255,27 +1784,6 @@ class NavigatorState extends State with TickerProviderStateMixin { route.dispose(); } - /// Repeatedly calls [pop] until the given `predicate` returns true. - /// - /// The predicate may be applied to the same route more than once if - /// [Route.willHandlePopInternally] is true. - /// - /// To pop until a route with a certain name, use the [RoutePredicate] - /// returned from [ModalRoute.withName]. - void popUntil(RoutePredicate predicate) { - while (!predicate(_history.last)) - pop(); - } - - /// Whether this navigator can be popped. - /// - /// The only route that cannot be popped off the navigator is the initial - /// route. - bool canPop() { - assert(_history.isNotEmpty); - return _history.length > 1 || _history[0].willHandlePopInternally; - } - /// Whether a route is currently being manipulated by the user, e.g. /// as during an iOS back gesture. bool get userGestureInProgress => _userGesturesInProgress > 0; @@ -1326,6 +1834,8 @@ class NavigatorState extends State with TickerProviderStateMixin { final RenderAbsorbPointer absorber = _overlayKey.currentContext?.ancestorRenderObjectOfType(const TypeMatcher()); setState(() { absorber?.absorbing = true; + // We do this in setState so that we'll reset the absorbing value back + // to false on the next frame. }); } _activePointers.toList().forEach(WidgetsBinding.instance.cancelPointer); diff --git a/packages/flutter/lib/src/widgets/pages.dart b/packages/flutter/lib/src/widgets/pages.dart index cf54eadb5e5..38062446651 100644 --- a/packages/flutter/lib/src/widgets/pages.dart +++ b/packages/flutter/lib/src/widgets/pages.dart @@ -12,7 +12,7 @@ import 'routes.dart'; abstract class PageRoute extends ModalRoute { /// Creates a modal route that replaces the entire screen. PageRoute({ - RouteSettings settings: const RouteSettings(), + RouteSettings settings, this.fullscreenDialog: false, }) : super(settings: settings); @@ -71,7 +71,7 @@ class PageRouteBuilder extends PageRoute { /// The [pageBuilder], [transitionsBuilder], [opaque], [barrierDismissible], /// and [maintainState] arguments must not be null. PageRouteBuilder({ - RouteSettings settings: const RouteSettings(), + RouteSettings settings, @required this.pageBuilder, this.transitionsBuilder: _defaultTransitionsBuilder, this.transitionDuration: const Duration(milliseconds: 300), diff --git a/packages/flutter/lib/src/widgets/routes.dart b/packages/flutter/lib/src/widgets/routes.dart index b0a4ac71ec8..bfa338d64d0 100644 --- a/packages/flutter/lib/src/widgets/routes.dart +++ b/packages/flutter/lib/src/widgets/routes.dart @@ -14,12 +14,17 @@ import 'modal_barrier.dart'; import 'navigator.dart'; import 'overlay.dart'; import 'page_storage.dart'; -import 'pages.dart'; +import 'transitions.dart'; const Color _kTransparent = const Color(0x00000000); /// A route that displays widgets in the [Navigator]'s [Overlay]. abstract class OverlayRoute extends Route { + /// Creates a route that knows how to interact with an [Overlay]. + OverlayRoute({ + RouteSettings settings, + }) : super(settings: settings); + /// Subclasses should override this getter to return the builders for the overlay. Iterable createOverlayEntries(); @@ -68,12 +73,21 @@ abstract class OverlayRoute extends Route { /// A route with entrance and exit transitions. abstract class TransitionRoute extends OverlayRoute { + /// Creates a route that animates itself when it is pushed or popped. + TransitionRoute({ + RouteSettings settings, + }) : super(settings: settings); + + // TODO(ianh): once https://github.com/dart-lang/sdk/issues/31543 is fixed, + // this should be removed. + TransitionRoute._settings(RouteSettings settings) : super(settings: settings); + /// This future completes only once the transition itself has finished, after /// the overlay entries have been removed from the navigator's overlay. /// /// This future completes once the animation has been dismissed. That will be - /// after [popped], because [popped] completes before the animation even - /// starts, as soon as the route is popped. + /// after [popped], because [popped] typically completes before the animation + /// even starts, as soon as the route is popped. Future get completed => _transitionCompleter.future; final Completer _transitionCompleter = new Completer(); @@ -149,6 +163,7 @@ abstract class TransitionRoute extends OverlayRoute { } break; } + changedInternalState(); } /// The animation for the route being pushed on top of this route. This @@ -179,7 +194,7 @@ abstract class TransitionRoute extends OverlayRoute { void didReplace(Route oldRoute) { assert(_controller != null, '$runtimeType.didReplace called before calling install() or after calling dispose().'); assert(!_transitionCompleter.isCompleted, 'Cannot reuse a $runtimeType after disposing it.'); - if (oldRoute is TransitionRoute) + if (oldRoute is TransitionRoute) _controller.value = oldRoute._controller.value; _animation.addStatusListener(_handleStatusChanged); super.didReplace(oldRoute); @@ -295,6 +310,8 @@ class LocalHistoryEntry { /// pop internally if its list of local history entries is non-empty. Rather /// than being removed as the current route, the most recent [LocalHistoryEntry] /// is removed from the list and its [LocalHistoryEntry.onRemove] is called. +/// +/// This class is typically used as a mixin. abstract class LocalHistoryRoute extends Route { List _localHistory; @@ -356,16 +373,6 @@ abstract class LocalHistoryRoute extends Route { bool get willHandlePopInternally { return _localHistory != null && _localHistory.isNotEmpty; } - - /// Called whenever the internal state of the route has changed. - /// - /// This should be called whenever [willHandlePopInternally] and [didPop] - /// might change the value they return. It is used by [ModalRoute], for - /// example, to report the new information via its inherited widget to any - /// children of the route. - @protected - @mustCallSuper - void changedInternalState() { } } class _ModalScopeStatus extends InheritedWidget { @@ -400,88 +407,99 @@ class _ModalScopeStatus extends InheritedWidget { } } -class _ModalScope extends StatefulWidget { +class _ModalScope extends StatefulWidget { const _ModalScope({ Key key, this.route, - @required this.page, }) : super(key: key); - final ModalRoute route; - final Widget page; + final ModalRoute route; @override - _ModalScopeState createState() => new _ModalScopeState(); + _ModalScopeState createState() => new _ModalScopeState(); } -class _ModalScopeState extends State<_ModalScope> { - // See addScopedWillPopCallback, removeScopedWillPopCallback in ModalRoute. - final List _willPopCallbacks = []; +class _ModalScopeState extends State<_ModalScope> { + // We cache the result of calling the route's buildPage, and clear the cache + // whenever the dependencies change. This implements the contract described in + // the documentation for buildPage, namely that it gets called once, unless + // something like a ModalRoute.of() dependency triggers an update. + Widget _page; + + // This is the combination of the two animations for the route. + Listenable _listenable; @override void initState() { super.initState(); - widget.route.animation?.addStatusListener(_animationStatusChanged); - widget.route.secondaryAnimation?.addStatusListener(_animationStatusChanged); + final List animations = []; + if (widget.route.animation != null) + animations.add(widget.route.animation); + if (widget.route.secondaryAnimation != null) + animations.add(widget.route.secondaryAnimation); + _listenable = new Listenable.merge(animations); } @override - void didUpdateWidget(_ModalScope oldWidget) { + void didUpdateWidget(_ModalScope oldWidget) { super.didUpdateWidget(oldWidget); assert(widget.route == oldWidget.route); } @override - void dispose() { - widget.route.animation?.removeStatusListener(_animationStatusChanged); - widget.route.secondaryAnimation?.removeStatusListener(_animationStatusChanged); - super.dispose(); + void didChangeDependencies() { + super.didChangeDependencies(); + _page = null; } - void addWillPopCallback(WillPopCallback callback) { - assert(mounted); - _willPopCallbacks.add(callback); - } - - void removeWillPopCallback(WillPopCallback callback) { - assert(mounted); - _willPopCallbacks.remove(callback); - } - - void _animationStatusChanged(AnimationStatus status) { + void _forceRebuildPage() { setState(() { - // The animation's states are our build state, and they changed already. + _page = null; }); } + // This should be called to wrap any changes to route.isCurrent, route.canPop, + // and route.offstage. void _routeSetState(VoidCallback fn) { setState(fn); } @override Widget build(BuildContext context) { - return new FocusScope( - node: widget.route.focusScopeNode, + return new _ModalScopeStatus( + route: widget.route, + isCurrent: widget.route.isCurrent, // _routeSetState is called if this updates + canPop: widget.route.canPop, // _routeSetState is called if this updates child: new Offstage( - offstage: widget.route.offstage, - child: new IgnorePointer( - ignoring: widget.route.animation?.status == AnimationStatus.reverse, - // Keep the transition between repaint boundaries so we don't trigger - // deep repaints of the render tree above or below the transition. - child: new RepaintBoundary( - child: widget.route.buildTransitions( - context, - widget.route.animation, - widget.route.secondaryAnimation, - new RepaintBoundary( - child: new PageStorage( - key: widget.route._subtreeKey, - bucket: widget.route._storageBucket, - child: new _ModalScopeStatus( - route: widget.route, - isCurrent: widget.route.isCurrent, - canPop: widget.route.canPop, - child: widget.page, + offstage: widget.route.offstage, // _routeSetState is called if this updates + child: new PageStorage( + bucket: widget.route._storageBucket, // immutable + child: new FocusScope( + node: widget.route.focusScopeNode, // immutable + child: new RepaintBoundary( + child: new AnimatedBuilder( + animation: _listenable, // immutable + builder: (BuildContext context, Widget child) { + return widget.route.buildTransitions( + context, + widget.route.animation, + widget.route.secondaryAnimation, + new IgnorePointer( + ignoring: widget.route.animation?.status == AnimationStatus.reverse, + child: child, + ), + ); + }, + child: _page ??= new RepaintBoundary( + key: widget.route._subtreeKey, // immutable + child: new Builder( + builder: (BuildContext context) { + return widget.route.buildPage( + context, + widget.route.animation, + widget.route.secondaryAnimation, + ); + }, ), ), ), @@ -495,22 +513,20 @@ class _ModalScopeState extends State<_ModalScope> { /// A route that blocks interaction with previous routes. /// -/// ModalRoutes cover the entire [Navigator]. They are not necessarily [opaque], -/// however; for example, a pop-up menu uses a ModalRoute but only shows the menu -/// in a small box overlapping the previous route. +/// [ModalRoute]s cover the entire [Navigator]. They are not necessarily +/// [opaque], however; for example, a pop-up menu uses a [ModalRoute] but only +/// shows the menu in a small box overlapping the previous route. +/// +/// The `T` type argument is the return value of the route. If there is no +/// return value, consider using `void` as the return value. abstract class ModalRoute extends TransitionRoute with LocalHistoryRoute { /// Creates a route that blocks interaction with previous routes. ModalRoute({ - this.settings: const RouteSettings() - }); + RouteSettings settings, + }) : super._settings(settings); // The API for general users of this class - /// The settings for this route. - /// - /// See [RouteSettings] for details. - final RouteSettings settings; - /// Returns the modal route most closely associated with the given context. /// /// Returns null if the given context is not associated with a modal route. @@ -518,19 +534,19 @@ abstract class ModalRoute extends TransitionRoute with LocalHistoryRoute route = ModalRoute.of(context); + /// ModalRoute route = ModalRoute.of(context); /// ``` /// /// The given [BuildContext] will be rebuilt if the state of the route changes /// (specifically, if [isCurrent] or [canPop] change value). - static ModalRoute of(BuildContext context) { + static ModalRoute of(BuildContext context) { final _ModalScopeStatus widget = context.inheritFromWidgetOfExactType(_ModalScopeStatus); return widget?.route; } /// Schedule a call to [buildTransitions]. /// - /// Whenever you need to change internal state for a ModalRoute object, make + /// Whenever you need to change internal state for a [ModalRoute] object, make /// the change in a function that you pass to [setState], as in: /// /// ```dart @@ -579,13 +595,14 @@ abstract class ModalRoute extends TransitionRoute with LocalHistoryRoute extends TransitionRoute with LocalHistoryRoute extends TransitionRoute with LocalHistoryRoute extends TransitionRoute with LocalHistoryRoute extends TransitionRoute with LocalHistoryRoute extends TransitionRoute with LocalHistoryRoute extends TransitionRoute with LocalHistoryRoute get secondaryAnimation => _secondaryAnimationProxy; ProxyAnimation _secondaryAnimationProxy; + final List _willPopCallbacks = []; + /// Returns the value of the first callback added with /// [addScopedWillPopCallback] that returns false. If they all return true, /// returns the inherited method's result (see [Route.willPop]). @@ -862,9 +899,9 @@ abstract class ModalRoute extends TransitionRoute with LocalHistoryRoute willPop() async { - final _ModalScopeState scope = _scopeKey.currentState; + final _ModalScopeState scope = _scopeKey.currentState; assert(scope != null); - for (WillPopCallback callback in new List.from(scope._willPopCallbacks)) { + for (WillPopCallback callback in new List.from(_willPopCallbacks)) { if (!await callback()) return RoutePopDisposition.doNotPop; } @@ -932,8 +969,8 @@ abstract class ModalRoute extends TransitionRoute with LocalHistoryRoute extends TransitionRoute with LocalHistoryRoute extends TransitionRoute with LocalHistoryRoute previousRoute) { + super.didChangePrevious(previousRoute); + changedInternalState(); } @override void changedInternalState() { super.changedInternalState(); setState(() { /* internal state already changed */ }); + _modalBarrier.markNeedsBuild(); } @override - void didChangePrevious(Route previousRoute) { - super.didChangePrevious(previousRoute); - setState(() { /* this might affect canPop */ }); + void changedExternalState() { + super.changedExternalState(); + if (_scopeKey.currentState != null) + _scopeKey.currentState._forceRebuildPage(); } /// Whether this route can be popped. @@ -989,53 +1034,57 @@ abstract class ModalRoute extends TransitionRoute with LocalHistoryRoute _scopeKey = new GlobalKey<_ModalScopeState>(); + final GlobalKey<_ModalScopeState> _scopeKey = new GlobalKey<_ModalScopeState>(); final GlobalKey _subtreeKey = new GlobalKey(); final PageStorageBucket _storageBucket = new PageStorageBucket(); // one of the builders + OverlayEntry _modalBarrier; Widget _buildModalBarrier(BuildContext context) { Widget barrier; - if (barrierColor != null && !offstage) { + if (barrierColor != null && !offstage) { // changedInternalState is called if these update assert(barrierColor != _kTransparent); final Animation color = new ColorTween( begin: _kTransparent, - end: barrierColor + end: barrierColor, // changedInternalState is called if this updates ).animate(new CurvedAnimation( parent: animation, - curve: Curves.ease + curve: Curves.ease, )); barrier = new AnimatedModalBarrier( color: color, - dismissible: barrierDismissible, - semanticsLabel: barrierLabel, + dismissible: barrierDismissible, // changedInternalState is called if this updates + semanticsLabel: barrierLabel, // changedInternalState is called if this updates ); } else { barrier = new ModalBarrier( - dismissible: barrierDismissible, - semanticsLabel: barrierLabel, + dismissible: barrierDismissible, // changedInternalState is called if this updates + semanticsLabel: barrierLabel, // changedInternalState is called if this updates ); } - assert(animation.status != AnimationStatus.dismissed); return new IgnorePointer( - ignoring: animation.status == AnimationStatus.reverse, - child: barrier + ignoring: animation.status == AnimationStatus.reverse || // changedInternalState is called when this updates + animation.status == AnimationStatus.dismissed, // dismissed is possible when doing a manual pop gesture + child: barrier, ); } + // We cache the part of the modal scope that doesn't change from frame to + // frame so that we minimize the amount of building that happens. + Widget _modalScopeCache; + // one of the builders Widget _buildModalScope(BuildContext context) { - return new _ModalScope( + return _modalScopeCache ??= new _ModalScope( key: _scopeKey, route: this, - page: buildPage(context, animation, secondaryAnimation) - // _ModalScope calls buildTransitions(), defined above + // _ModalScope calls buildTransitions() and buildChild(), defined above ); } @override Iterable createOverlayEntries() sync* { - yield new OverlayEntry(builder: _buildModalBarrier); + yield _modalBarrier = new OverlayEntry(builder: _buildModalBarrier); yield new OverlayEntry(builder: _buildModalScope, maintainState: maintainState); } @@ -1045,30 +1094,40 @@ abstract class ModalRoute extends TransitionRoute with LocalHistoryRoute extends ModalRoute { + /// Initializes the [PopupRoute]. + PopupRoute({ + RouteSettings settings, + }) : super(settings: settings); + @override bool get opaque => false; @override bool get maintainState => true; - - @override - void didChangeNext(Route nextRoute) { - assert(nextRoute is! PageRoute); - super.didChangeNext(nextRoute); - } } /// A [Navigator] observer that notifies [RouteAware]s of changes to the /// state of their [Route]. /// -/// [RouteObserver] informs subscribers whenever a route of type `T` is pushed -/// on top of their own route of type `T` or popped from it. This is for example -/// useful to keep track of page transitions, e.i. a `RouteObserver` +/// [RouteObserver] informs subscribers whenever a route of type `R` is pushed +/// on top of their own route of type `R` or popped from it. This is for example +/// useful to keep track of page transitions, e.g. a `RouteObserver` /// will inform subscribed [RouteAware]s whenever the user navigates away from /// the current page route to another page route. /// -/// If you want to be informed about route changes of any type, you should -/// instantiate a `RouteObserver`. +/// To be informed about route changes of any type, consider instantiating a +/// `RouteObserver`. +/// +/// ## Type arguments +/// +/// When using more aggressive +/// [lints](http://dart-lang.github.io/linter/lints/), in particular lints such +/// as `always_specify_types`, the Dart analyzer will require that certain types +/// be given with their type arguments. Since the [Route] class and its +/// subclasses have a type argument, this includes the arguments passed to this +/// class. Consider using `dynamic` to specify the entire class of routes rather +/// than only specific subtypes. For example, to watch for all [PageRoute] +/// variants, the `RouteObserver>` type may be used. /// /// ## Sample code /// @@ -1119,15 +1178,15 @@ abstract class PopupRoute extends ModalRoute { /// /// } /// ``` -class RouteObserver> extends NavigatorObserver { - final Map> _listeners = >{}; +class RouteObserver> extends NavigatorObserver { + final Map> _listeners = >{}; /// Subscribe [routeAware] to be informed about changes to [route]. /// /// Going forward, [routeAware] will be informed about qualifying changes /// to [route], e.g. when [route] is covered by another route or when [route] /// is popped off the [Navigator] stack. - void subscribe(RouteAware routeAware, T route) { + void subscribe(RouteAware routeAware, R route) { assert(routeAware != null); assert(route != null); final Set subscribers = _listeners.putIfAbsent(route, () => new Set()); @@ -1142,7 +1201,7 @@ class RouteObserver> extends NavigatorObserver { /// subscribed to multiple types, this will unregister it (once) from each type. void unsubscribe(RouteAware routeAware) { assert(routeAware != null); - for (T route in _listeners.keys) { + for (R route in _listeners.keys) { final Set subscribers = _listeners[route]; subscribers?.remove(routeAware); } @@ -1150,7 +1209,7 @@ class RouteObserver> extends NavigatorObserver { @override void didPop(Route route, Route previousRoute) { - if (route is T && previousRoute is T) { + if (route is R && previousRoute is R) { final List previousSubscribers = _listeners[previousRoute]?.toList(); if (previousSubscribers != null) { @@ -1171,7 +1230,7 @@ class RouteObserver> extends NavigatorObserver { @override void didPush(Route route, Route previousRoute) { - if (route is T && previousRoute is T) { + if (route is R && previousRoute is R) { final Set previousSubscribers = _listeners[previousRoute]; if (previousSubscribers != null) { diff --git a/packages/flutter/test/cupertino/dialog_test.dart b/packages/flutter/test/cupertino/dialog_test.dart index 7ce05167c09..f913aa0c53d 100644 --- a/packages/flutter/test/cupertino/dialog_test.dart +++ b/packages/flutter/test/cupertino/dialog_test.dart @@ -17,7 +17,7 @@ void main() { builder: (BuildContext context) { return new RaisedButton( onPressed: () { - showDialog( + showDialog( context: context, builder: (BuildContext context) { return new CupertinoAlertDialog( @@ -110,7 +110,7 @@ void main() { child: new Builder(builder: (BuildContext context) { return new RaisedButton( onPressed: () { - showDialog( + showDialog( context: context, builder: (BuildContext context) { return new MediaQuery( diff --git a/packages/flutter/test/cupertino/nav_bar_test.dart b/packages/flutter/test/cupertino/nav_bar_test.dart index 2ef502c8473..c9f4953c5f9 100644 --- a/packages/flutter/test/cupertino/nav_bar_test.dart +++ b/packages/flutter/test/cupertino/nav_bar_test.dart @@ -14,7 +14,7 @@ void main() { new WidgetsApp( color: const Color(0xFFFFFFFF), onGenerateRoute: (RouteSettings settings) { - return new CupertinoPageRoute( + return new CupertinoPageRoute( settings: settings, builder: (BuildContext context) { return const CupertinoNavigationBar( @@ -36,7 +36,7 @@ void main() { new WidgetsApp( color: const Color(0xFFFFFFFF), onGenerateRoute: (RouteSettings settings) { - return new CupertinoPageRoute( + return new CupertinoPageRoute( settings: settings, builder: (BuildContext context) { return const CupertinoNavigationBar( @@ -56,7 +56,7 @@ void main() { new WidgetsApp( color: const Color(0xFFFFFFFF), onGenerateRoute: (RouteSettings settings) { - return new CupertinoPageRoute( + return new CupertinoPageRoute( settings: settings, builder: (BuildContext context) { return const CupertinoNavigationBar( @@ -76,7 +76,7 @@ void main() { new WidgetsApp( color: const Color(0xFFFFFFFF), onGenerateRoute: (RouteSettings settings) { - return new CupertinoPageRoute( + return new CupertinoPageRoute( settings: settings, builder: (BuildContext context) { return const CupertinoNavigationBar( @@ -98,7 +98,7 @@ void main() { new WidgetsApp( color: const Color(0xFFFFFFFF), onGenerateRoute: (RouteSettings settings) { - return new CupertinoPageRoute( + return new CupertinoPageRoute( settings: settings, builder: (BuildContext context) { return const CupertinoPageScaffold( @@ -126,7 +126,7 @@ void main() { new WidgetsApp( color: const Color(0xFFFFFFFF), onGenerateRoute: (RouteSettings settings) { - return new CupertinoPageRoute( + return new CupertinoPageRoute( settings: settings, builder: (BuildContext context) { return new MediaQuery( @@ -178,7 +178,7 @@ void main() { new WidgetsApp( color: const Color(0xFFFFFFFF), onGenerateRoute: (RouteSettings settings) { - return new CupertinoPageRoute( + return new CupertinoPageRoute( settings: settings, builder: (BuildContext context) { return new CupertinoPageScaffold( @@ -266,7 +266,7 @@ void main() { new WidgetsApp( color: const Color(0xFFFFFFFF), onGenerateRoute: (RouteSettings settings) { - return new CupertinoPageRoute( + return new CupertinoPageRoute( settings: settings, builder: (BuildContext context) { return new CupertinoPageScaffold( @@ -337,7 +337,7 @@ void main() { new WidgetsApp( color: const Color(0xFFFFFFFF), onGenerateRoute: (RouteSettings settings) { - return new CupertinoPageRoute( + return new CupertinoPageRoute( settings: settings, builder: (BuildContext context) { return const CupertinoNavigationBar( @@ -351,7 +351,7 @@ void main() { expect(find.byType(CupertinoButton), findsNothing); - tester.state(find.byType(Navigator)).push(new CupertinoPageRoute( + tester.state(find.byType(Navigator)).push(new CupertinoPageRoute( builder: (BuildContext context) { return const CupertinoNavigationBar( middle: const Text('Page 2'), @@ -365,7 +365,7 @@ void main() { expect(find.byType(CupertinoButton), findsOneWidget); expect(find.byType(Icon), findsOneWidget); - tester.state(find.byType(Navigator)).push(new CupertinoPageRoute( + tester.state(find.byType(Navigator)).push(new CupertinoPageRoute( fullscreenDialog: true, builder: (BuildContext context) { return const CupertinoNavigationBar( @@ -401,7 +401,7 @@ void main() { new WidgetsApp( color: const Color(0xFFFFFFFF), onGenerateRoute: (RouteSettings settings) { - return new CupertinoPageRoute( + return new CupertinoPageRoute( settings: settings, builder: (BuildContext context) { return const CupertinoNavigationBar( @@ -428,7 +428,7 @@ void main() { new WidgetsApp( color: const Color(0xFFFFFFFF), onGenerateRoute: (RouteSettings settings) { - return new CupertinoPageRoute( + return new CupertinoPageRoute( settings: settings, builder: (BuildContext context) { return const CupertinoNavigationBar( @@ -462,7 +462,7 @@ void main() { new WidgetsApp( color: const Color(0xFFFFFFFF), onGenerateRoute: (RouteSettings settings) { - return new CupertinoPageRoute( + return new CupertinoPageRoute( settings: settings, builder: (BuildContext context) { return const CupertinoNavigationBar( diff --git a/packages/flutter/test/cupertino/page_test.dart b/packages/flutter/test/cupertino/page_test.dart index fdb562a4c1b..9c5b64c795e 100644 --- a/packages/flutter/test/cupertino/page_test.dart +++ b/packages/flutter/test/cupertino/page_test.dart @@ -11,7 +11,7 @@ void main() { new WidgetsApp( color: const Color(0xFFFFFFFF), onGenerateRoute: (RouteSettings settings) { - return new CupertinoPageRoute( + return new CupertinoPageRoute( settings: settings, builder: (BuildContext context) { final String pageNumber = settings.name == '/' ? '1' : '2'; @@ -82,7 +82,7 @@ void main() { ], color: const Color(0xFFFFFFFF), onGenerateRoute: (RouteSettings settings) { - return new CupertinoPageRoute( + return new CupertinoPageRoute( settings: settings, builder: (BuildContext context) { final String pageNumber = settings.name == '/' ? '1' : '2'; @@ -151,7 +151,7 @@ void main() { new WidgetsApp( color: const Color(0xFFFFFFFF), onGenerateRoute: (RouteSettings settings) { - return new CupertinoPageRoute( + return new CupertinoPageRoute( settings: settings, builder: (BuildContext context) { return const Center(child: const Text('Page 1')); @@ -163,7 +163,7 @@ void main() { final Offset widget1InitialTopLeft = tester.getTopLeft(find.text('Page 1')); - tester.state(find.byType(Navigator)).push(new CupertinoPageRoute( + tester.state(find.byType(Navigator)).push(new CupertinoPageRoute( builder: (BuildContext context) { return const Center(child: const Text('Page 2')); }, @@ -219,7 +219,7 @@ void main() { new WidgetsApp( color: const Color(0xFFFFFFFF), onGenerateRoute: (RouteSettings settings) { - return new CupertinoPageRoute( + return new CupertinoPageRoute( settings: settings, builder: (BuildContext context) { final String pageNumber = settings.name == '/' ? '1' : '2'; @@ -284,7 +284,7 @@ void main() { ], color: const Color(0xFFFFFFFF), onGenerateRoute: (RouteSettings settings) { - return new CupertinoPageRoute( + return new CupertinoPageRoute( settings: settings, builder: (BuildContext context) { final String pageNumber = settings.name == '/' ? '1' : '2'; diff --git a/packages/flutter/test/cupertino/scaffold_test.dart b/packages/flutter/test/cupertino/scaffold_test.dart index 030b786c992..943f21c6684 100644 --- a/packages/flutter/test/cupertino/scaffold_test.dart +++ b/packages/flutter/test/cupertino/scaffold_test.dart @@ -14,7 +14,7 @@ void main() { new WidgetsApp( color: const Color(0xFFFFFFFF), onGenerateRoute: (RouteSettings settings) { - return new CupertinoPageRoute( + return new CupertinoPageRoute( settings: settings, builder: (BuildContext context) { return const CupertinoPageScaffold( @@ -40,7 +40,7 @@ void main() { new WidgetsApp( color: const Color(0xFFFFFFFF), onGenerateRoute: (RouteSettings settings) { - return new CupertinoPageRoute( + return new CupertinoPageRoute( settings: settings, builder: (BuildContext context) { return new CupertinoTabScaffold( @@ -85,7 +85,7 @@ void main() { new WidgetsApp( color: const Color(0xFFFFFFFF), onGenerateRoute: (RouteSettings settings) { - return new CupertinoPageRoute( + return new CupertinoPageRoute( settings: settings, builder: (BuildContext context) { return new MediaQuery( @@ -145,7 +145,7 @@ void main() { new WidgetsApp( color: const Color(0xFFFFFFFF), onGenerateRoute: (RouteSettings settings) { - return new CupertinoPageRoute( + return new CupertinoPageRoute( settings: settings, builder: (BuildContext context) { return new CupertinoTabScaffold( @@ -175,7 +175,7 @@ void main() { child: const Text('Next'), onPressed: () { Navigator.of(context).push( - new CupertinoPageRoute( + new CupertinoPageRoute( builder: (BuildContext context) { return new CupertinoPageScaffold( navigationBar: new CupertinoNavigationBar( @@ -260,7 +260,7 @@ void main() { new WidgetsApp( color: const Color(0xFFFFFFFF), onGenerateRoute: (RouteSettings settings) { - return new CupertinoPageRoute( + return new CupertinoPageRoute( settings: settings, builder: (BuildContext context) { return const CupertinoPageScaffold( @@ -284,7 +284,7 @@ void main() { new WidgetsApp( color: const Color(0xFFFFFFFF), onGenerateRoute: (RouteSettings settings) { - return new CupertinoPageRoute( + return new CupertinoPageRoute( settings: settings, builder: (BuildContext context) { return const CupertinoPageScaffold( diff --git a/packages/flutter/test/cupertino/tab_scaffold_test.dart b/packages/flutter/test/cupertino/tab_scaffold_test.dart index c56002ccb54..f9f70ff55a6 100644 --- a/packages/flutter/test/cupertino/tab_scaffold_test.dart +++ b/packages/flutter/test/cupertino/tab_scaffold_test.dart @@ -23,7 +23,7 @@ void main() { new WidgetsApp( color: const Color(0xFFFFFFFF), onGenerateRoute: (RouteSettings settings) { - return new CupertinoPageRoute( + return new CupertinoPageRoute( settings: settings, builder: (BuildContext context) { return new CupertinoTabScaffold( @@ -85,7 +85,7 @@ void main() { new WidgetsApp( color: const Color(0xFFFFFFFF), onGenerateRoute: (RouteSettings settings) { - return new CupertinoPageRoute( + return new CupertinoPageRoute( settings: settings, builder: (BuildContext context) { return new CupertinoTabScaffold( @@ -129,7 +129,7 @@ void main() { new WidgetsApp( color: const Color(0xFFFFFFFF), onGenerateRoute: (RouteSettings settings) { - return new CupertinoPageRoute( + return new CupertinoPageRoute( settings: settings, builder: (BuildContext context) { return new CupertinoTabScaffold( @@ -171,7 +171,7 @@ void main() { new WidgetsApp( color: const Color(0xFFFFFFFF), onGenerateRoute: (RouteSettings settings) { - return new CupertinoPageRoute( + return new CupertinoPageRoute( settings: settings, builder: (BuildContext context) { return new Material( diff --git a/packages/flutter/test/cupertino/tab_test.dart b/packages/flutter/test/cupertino/tab_test.dart index a3bb8b3d181..6b5f0517c86 100644 --- a/packages/flutter/test/cupertino/tab_test.dart +++ b/packages/flutter/test/cupertino/tab_test.dart @@ -11,7 +11,7 @@ void main() { new WidgetsApp( color: const Color(0xFFFFFFFF), onGenerateRoute: (RouteSettings settings) { - return new CupertinoPageRoute( + return new CupertinoPageRoute( settings: settings, builder: (BuildContext context) { return new CupertinoTabView( @@ -31,7 +31,7 @@ void main() { new WidgetsApp( color: const Color(0xFFFFFFFF), onGenerateRoute: (RouteSettings settings) { - return new CupertinoPageRoute( + return new CupertinoPageRoute( settings: settings, builder: (BuildContext context) { return new CupertinoTabView( @@ -53,7 +53,7 @@ void main() { new WidgetsApp( color: const Color(0xFFFFFFFF), onGenerateRoute: (RouteSettings settings) { - return new CupertinoPageRoute( + return new CupertinoPageRoute( settings: settings, builder: (BuildContext context) { return new CupertinoTabView( @@ -87,13 +87,13 @@ void main() { new WidgetsApp( color: const Color(0xFFFFFFFF), onGenerateRoute: (RouteSettings settings) { - return new CupertinoPageRoute( + return new CupertinoPageRoute( settings: settings, builder: (BuildContext context) { return new CupertinoTabView( onGenerateRoute: (RouteSettings settings) { if (settings.name == Navigator.defaultRouteName) { - return new CupertinoPageRoute( + return new CupertinoPageRoute( settings: settings, builder: (BuildContext context) { return const Text('generated home'); @@ -117,7 +117,7 @@ void main() { new WidgetsApp( color: const Color(0xFFFFFFFF), onGenerateRoute: (RouteSettings settings) { - return new CupertinoPageRoute( + return new CupertinoPageRoute( settings: settings, builder: (BuildContext context) { return new CupertinoTabView( diff --git a/packages/flutter/test/material/app_test.dart b/packages/flutter/test/material/app_test.dart index 43314ad3850..1396bf8c63c 100644 --- a/packages/flutter/test/material/app_test.dart +++ b/packages/flutter/test/material/app_test.dart @@ -87,7 +87,7 @@ void main() { expect(state2.marker, equals('original')); }); - testWidgets('Do not rebuild page on the second frame of the route transition', (WidgetTester tester) async { + testWidgets('Do not rebuild page during a route transition', (WidgetTester tester) async { int buildCounter = 0; await tester.pumpWidget( new MaterialApp( @@ -128,10 +128,82 @@ void main() { await tester.pump(const Duration(milliseconds: 10)); expect(buildCounter, 1); await tester.pump(const Duration(seconds: 1)); - expect(buildCounter, 2); + expect(buildCounter, 1); expect(find.text('Y'), findsOneWidget); }); + testWidgets('Do rebuild the home page if it changes', (WidgetTester tester) async { + int buildCounter = 0; + await tester.pumpWidget( + new MaterialApp( + home: new Builder( + builder: (BuildContext context) { + ++buildCounter; + return const Text('A'); + } + ), + ), + ); + expect(buildCounter, 1); + expect(find.text('A'), findsOneWidget); + await tester.pumpWidget( + new MaterialApp( + home: new Builder( + builder: (BuildContext context) { + ++buildCounter; + return const Text('B'); + } + ), + ), + ); + expect(buildCounter, 2); + expect(find.text('B'), findsOneWidget); + }); + + testWidgets('Do not rebuild the home page if it does not actually change', (WidgetTester tester) async { + int buildCounter = 0; + final Widget home = new Builder( + builder: (BuildContext context) { + ++buildCounter; + return const Placeholder(); + } + ); + await tester.pumpWidget( + new MaterialApp( + home: home, + ), + ); + expect(buildCounter, 1); + await tester.pumpWidget( + new MaterialApp( + home: home, + ), + ); + expect(buildCounter, 1); + }); + + testWidgets('Do rebuild pages that come from the routes table if the MaterialApp changes', (WidgetTester tester) async { + int buildCounter = 0; + final Map routes = { + '/': (BuildContext context) { + ++buildCounter; + return const Placeholder(); + }, + }; + await tester.pumpWidget( + new MaterialApp( + routes: routes, + ), + ); + expect(buildCounter, 1); + await tester.pumpWidget( + new MaterialApp( + routes: routes, + ), + ); + expect(buildCounter, 2); + }); + testWidgets('Cannot pop the initial route', (WidgetTester tester) async { await tester.pumpWidget(new MaterialApp(home: const Text('Home'))); diff --git a/packages/flutter/test/material/dialog_test.dart b/packages/flutter/test/material/dialog_test.dart index f42b15e19dc..c2b44acf855 100644 --- a/packages/flutter/test/material/dialog_test.dart +++ b/packages/flutter/test/material/dialog_test.dart @@ -21,7 +21,7 @@ void main() { child: new RaisedButton( child: const Text('X'), onPressed: () { - showDialog( + showDialog( context: context, builder: (BuildContext context) { return new AlertDialog( @@ -71,7 +71,7 @@ void main() { child: new RaisedButton( child: const Text('X'), onPressed: () { - showDialog( + showDialog( context: context, builder: (BuildContext context) { return const AlertDialog( @@ -118,7 +118,7 @@ void main() { final BuildContext context = tester.element(find.text('Go')); - final Future result = showDialog( + final Future result = showDialog( context: context, builder: (BuildContext context) { return new SimpleDialog( @@ -161,7 +161,7 @@ void main() { final BuildContext context = tester.element(find.text('Go')); - showDialog( + showDialog( context: context, builder: (BuildContext context) { return new Container( @@ -182,7 +182,7 @@ void main() { await tester.pumpAndSettle(const Duration(seconds: 1)); expect(find.text('Dialog1'), findsNothing); - showDialog( + showDialog( context: context, barrierDismissible: false, builder: (BuildContext context) { @@ -227,7 +227,7 @@ void main() { final BuildContext context = tester.element(find.text(buttonText)); const String alertText = 'A button in an overlay alert'; - showDialog( + showDialog( context: context, builder: (BuildContext context) { return const AlertDialog(title: const Text(alertText)); @@ -260,7 +260,7 @@ void main() { ), child: new Navigator( onGenerateRoute: (_) { - return new PageRouteBuilder( + return new PageRouteBuilder( pageBuilder: (BuildContext context, Animation animation, Animation secondaryAnimation) { outerContext = context; return new Container(); @@ -271,7 +271,7 @@ void main() { ), )); - showDialog( + showDialog( context: outerContext, barrierDismissible: false, builder: (BuildContext context) { diff --git a/packages/flutter/test/material/dropdown_test.dart b/packages/flutter/test/material/dropdown_test.dart index 09132c14f0f..1b8c99d9aa4 100644 --- a/packages/flutter/test/material/dropdown_test.dart +++ b/packages/flutter/test/material/dropdown_test.dart @@ -76,7 +76,7 @@ class _TestAppState extends State { child: new Navigator( onGenerateRoute: (RouteSettings settings) { assert(settings.name == '/'); - return new MaterialPageRoute( + return new MaterialPageRoute( settings: settings, builder: (BuildContext context) => widget.child, ); @@ -159,7 +159,7 @@ void main() { child: new Navigator( initialRoute: '/', onGenerateRoute: (RouteSettings settings) { - return new MaterialPageRoute( + return new MaterialPageRoute( settings: settings, builder: (BuildContext context) { return new Material( diff --git a/packages/flutter/test/material/floating_action_button_test.dart b/packages/flutter/test/material/floating_action_button_test.dart index 308d2f670d6..dddbdcd45ae 100644 --- a/packages/flutter/test/material/floating_action_button_test.dart +++ b/packages/flutter/test/material/floating_action_button_test.dart @@ -84,7 +84,7 @@ void main() { ), ), ); - Navigator.push(theContext, new PageRouteBuilder( + Navigator.push(theContext, new PageRouteBuilder( pageBuilder: (BuildContext context, Animation animation, Animation secondaryAnimation) { return const Placeholder(); }, @@ -107,7 +107,7 @@ void main() { ), ), ); - Navigator.push(theContext, new PageRouteBuilder( + Navigator.push(theContext, new PageRouteBuilder( pageBuilder: (BuildContext context, Animation animation, Animation secondaryAnimation) { return const Placeholder(); }, @@ -131,7 +131,7 @@ void main() { ), ), ); - Navigator.push(theContext, new PageRouteBuilder( + Navigator.push(theContext, new PageRouteBuilder( pageBuilder: (BuildContext context, Animation animation, Animation secondaryAnimation) { return const Placeholder(); }, diff --git a/packages/flutter/test/material/modal_bottom_sheet_test.dart b/packages/flutter/test/material/modal_bottom_sheet_test.dart index fb393a65e11..63b5a154c3d 100644 --- a/packages/flutter/test/material/modal_bottom_sheet_test.dart +++ b/packages/flutter/test/material/modal_bottom_sheet_test.dart @@ -27,7 +27,7 @@ void main() { showModalBottomSheet( context: savedContext, builder: (BuildContext context) => const Text('BottomSheet') - ).then((Null result) { + ).then((Null result) { expectSync(result, isNull); showBottomSheetThenCalled = true; }); @@ -49,7 +49,7 @@ void main() { showModalBottomSheet( context: savedContext, builder: (BuildContext context) => const Text('BottomSheet'), - ).then((Null result) { + ).then((Null result) { expectSync(result, isNull); showBottomSheetThenCalled = true; }); @@ -172,7 +172,7 @@ void main() { ), child: new Navigator( onGenerateRoute: (_) { - return new PageRouteBuilder( + return new PageRouteBuilder( pageBuilder: (BuildContext context, Animation animation, Animation secondaryAnimation) { outerContext = context; return new Container(); @@ -184,7 +184,7 @@ void main() { ), )); - showModalBottomSheet( + showModalBottomSheet( context: outerContext, builder: (BuildContext context) { innerContext = context; diff --git a/packages/flutter/test/material/page_test.dart b/packages/flutter/test/material/page_test.dart index 2cd1d4b2f1d..862b67016d4 100644 --- a/packages/flutter/test/material/page_test.dart +++ b/packages/flutter/test/material/page_test.dart @@ -156,7 +156,7 @@ void main() { final Offset widget1InitialTopLeft = tester.getTopLeft(find.text('Page 1')); - tester.state(find.byType(Navigator)).push(new MaterialPageRoute( + tester.state(find.byType(Navigator)).push(new MaterialPageRoute( builder: (BuildContext context) { return const Material(child: const Text('Page 2')); }, @@ -355,7 +355,7 @@ void main() { ) ); - tester.state(find.byType(Navigator)).push(new MaterialPageRoute( + tester.state(find.byType(Navigator)).push(new MaterialPageRoute( builder: (BuildContext context) { return const Scaffold(body: const Text('Page 2')); }, diff --git a/packages/flutter/test/material/popup_menu_test.dart b/packages/flutter/test/material/popup_menu_test.dart index 905ff04dccd..8410efa8621 100644 --- a/packages/flutter/test/material/popup_menu_test.dart +++ b/packages/flutter/test/material/popup_menu_test.dart @@ -453,7 +453,7 @@ class _TestAppState extends State { child: new Navigator( onGenerateRoute: (RouteSettings settings) { assert(settings.name == '/'); - return new MaterialPageRoute( + return new MaterialPageRoute( settings: settings, builder: (BuildContext context) => new Material( child: widget.child, diff --git a/packages/flutter/test/material/scaffold_test.dart b/packages/flutter/test/material/scaffold_test.dart index 62af36b31eb..4e5ea393b14 100644 --- a/packages/flutter/test/material/scaffold_test.dart +++ b/packages/flutter/test/material/scaffold_test.dart @@ -419,7 +419,7 @@ void main() { ) ); - tester.state(find.byType(Navigator)).push(new MaterialPageRoute( + tester.state(find.byType(Navigator)).push(new MaterialPageRoute( builder: (BuildContext context) { return new Scaffold(appBar: new AppBar(), body: const Text('Page 2')); }, diff --git a/packages/flutter/test/material/text_field_focus_test.dart b/packages/flutter/test/material/text_field_focus_test.dart index b6b17e2f58b..70b7d59a924 100644 --- a/packages/flutter/test/material/text_field_focus_test.dart +++ b/packages/flutter/test/material/text_field_focus_test.dart @@ -108,7 +108,7 @@ void main() { final BuildContext context = tester.element(find.byType(TextField)); - showDialog( + showDialog( context: context, builder: (BuildContext context) => const SimpleDialog(title: const Text('Dialog')), ); diff --git a/packages/flutter/test/material/theme_test.dart b/packages/flutter/test/material/theme_test.dart index cc9487a657c..c05a7b1c426 100644 --- a/packages/flutter/test/material/theme_test.dart +++ b/packages/flutter/test/material/theme_test.dart @@ -161,7 +161,7 @@ void main() { builder: (BuildContext context) { return new RaisedButton( onPressed: () { - showModalBottomSheet( + showModalBottomSheet( context: context, builder: (BuildContext context) => const Text('bottomSheet'), ); @@ -198,7 +198,7 @@ void main() { builder: (BuildContext context) { return new RaisedButton( onPressed: () { - showDialog( + showDialog( context: context, builder: (BuildContext context) => const Text('dialog'), ); @@ -230,7 +230,7 @@ void main() { builder: (BuildContext context) { return new GestureDetector( onTap: () { - showDialog( + showDialog( context: context, builder: (BuildContext context) { return const Scaffold( diff --git a/packages/flutter/test/material/time_picker_test.dart b/packages/flutter/test/material/time_picker_test.dart index 7a745268027..9d4b93402e5 100644 --- a/packages/flutter/test/material/time_picker_test.dart +++ b/packages/flutter/test/material/time_picker_test.dart @@ -240,7 +240,7 @@ void _tests() { textDirection: TextDirection.ltr, child: new Navigator( onGenerateRoute: (RouteSettings settings) { - return new MaterialPageRoute(builder: (BuildContext context) { + return new MaterialPageRoute(builder: (BuildContext context) { return new FlatButton( onPressed: () { showTimePicker(context: context, initialTime: initialTime); diff --git a/packages/flutter/test/material/tooltip_test.dart b/packages/flutter/test/material/tooltip_test.dart index 67b2d327894..f57625521ec 100644 --- a/packages/flutter/test/material/tooltip_test.dart +++ b/packages/flutter/test/material/tooltip_test.dart @@ -543,7 +543,7 @@ void main() { textDirection: TextDirection.ltr, child: new Navigator( onGenerateRoute: (RouteSettings settings) { - return new MaterialPageRoute( + return new MaterialPageRoute( builder: (BuildContext context) { return new Center( child: new Tooltip( diff --git a/packages/flutter/test/material/will_pop_test.dart b/packages/flutter/test/material/will_pop_test.dart index 4f5c65e4f16..dc81b292cb3 100644 --- a/packages/flutter/test/material/will_pop_test.dart +++ b/packages/flutter/test/material/will_pop_test.dart @@ -13,7 +13,7 @@ class SamplePage extends StatefulWidget { } class SamplePageState extends State { - ModalRoute _route; + ModalRoute _route; Future _callback() async => willPopValue; @@ -66,7 +66,7 @@ class SampleForm extends StatelessWidget { // Expose the protected hasScopedWillPopCallback getter class TestPageRoute extends MaterialPageRoute { TestPageRoute({ WidgetBuilder builder }) - : super(builder: builder, maintainState: true, settings: const RouteSettings()); + : super(builder: builder, maintainState: true); bool get hasCallback => super.hasScopedWillPopCallback; } @@ -84,7 +84,7 @@ void main() { child: new FlatButton( child: const Text('X'), onPressed: () { - showDialog( + showDialog( context: context, builder: (BuildContext context) => new SamplePage(), ); @@ -138,7 +138,7 @@ void main() { child: new FlatButton( child: const Text('X'), onPressed: () { - Navigator.of(context).push(new MaterialPageRoute( + Navigator.of(context).push(new MaterialPageRoute( builder: (BuildContext context) { return new SampleForm( callback: () => new Future.value(willPopValue), @@ -212,7 +212,7 @@ void main() { child: new FlatButton( child: const Text('X'), onPressed: () { - Navigator.of(context).push(new MaterialPageRoute( + Navigator.of(context).push(new MaterialPageRoute( builder: (BuildContext context) { return new SampleForm( callback: () => showYesNoAlert(context), diff --git a/packages/flutter/test/widgets/app_navigator_key_test.dart b/packages/flutter/test/widgets/app_navigator_key_test.dart index 93bea3661ef..6da1bb19342 100644 --- a/packages/flutter/test/widgets/app_navigator_key_test.dart +++ b/packages/flutter/test/widgets/app_navigator_key_test.dart @@ -5,7 +5,7 @@ import 'package:flutter_test/flutter_test.dart'; import 'package:flutter/widgets.dart'; -final RouteFactory generateRoute = (RouteSettings settings) => new PageRouteBuilder( +final RouteFactory generateRoute = (RouteSettings settings) => new PageRouteBuilder( settings: settings, pageBuilder: (BuildContext context, Animation animation1, Animation animation2) { return const Placeholder(); diff --git a/packages/flutter/test/widgets/app_title_test.dart b/packages/flutter/test/widgets/app_title_test.dart index dfd5b1d2225..3609a8b9f21 100644 --- a/packages/flutter/test/widgets/app_title_test.dart +++ b/packages/flutter/test/widgets/app_title_test.dart @@ -19,7 +19,7 @@ Future pumpApp(WidgetTester tester, { GenerateAppTitle onGenerateTitle }) color: kTitleColor, onGenerateTitle: onGenerateTitle, onGenerateRoute: (RouteSettings settings) { - return new PageRouteBuilder( + return new PageRouteBuilder( pageBuilder: (BuildContext context, Animation animation, Animation secondaryAnimation) { return new Container(); } diff --git a/packages/flutter/test/widgets/heroes_test.dart b/packages/flutter/test/widgets/heroes_test.dart index ce567c1d9c5..85a2afc602e 100644 --- a/packages/flutter/test/widgets/heroes_test.dart +++ b/packages/flutter/test/widgets/heroes_test.dart @@ -81,7 +81,7 @@ final Map routes = { }; -class ThreeRoute extends MaterialPageRoute { +class ThreeRoute extends MaterialPageRoute { ThreeRoute() : super(builder: (BuildContext context) { return new Material( key: routeThreeKey, @@ -96,7 +96,7 @@ class ThreeRoute extends MaterialPageRoute { }); } -class MutatingRoute extends MaterialPageRoute { +class MutatingRoute extends MaterialPageRoute { MutatingRoute() : super(builder: (BuildContext context) { return new Hero(tag: 'a', child: const Text('MutatingRoute'), key: new UniqueKey()); }); @@ -363,7 +363,7 @@ void main() { testWidgets('Popping on first frame does not cause hero observer to crash', (WidgetTester tester) async { await tester.pumpWidget(new MaterialApp( onGenerateRoute: (RouteSettings settings) { - return new MaterialPageRoute( + return new MaterialPageRoute( settings: settings, builder: (BuildContext context) => new Hero(tag: 'test', child: new Container()), ); @@ -389,7 +389,7 @@ void main() { testWidgets('Overlapping starting and ending a hero transition works ok', (WidgetTester tester) async { await tester.pumpWidget(new MaterialApp( onGenerateRoute: (RouteSettings settings) { - return new MaterialPageRoute( + return new MaterialPageRoute( settings: settings, builder: (BuildContext context) => new Hero(tag: 'test', child: new Container()), ); @@ -432,7 +432,7 @@ void main() { return new FlatButton( child: const Text('push'), onPressed: () { - Navigator.push(context, new PageRouteBuilder( + Navigator.push(context, new PageRouteBuilder( pageBuilder: (BuildContext context, Animation _, Animation __) { return const Text('fail'); }, @@ -569,9 +569,9 @@ void main() { // After flying in the opposite direction for 50ms Hero 'a' will // be smaller than it was, but bigger than its initial size. await tester.pump(const Duration(milliseconds: 50)); - final double height100ms = tester.getSize(find.byKey(firstKey)).height; - expect(height100ms, greaterThan(height150ms)); - expect(finalHeight, lessThan(height100ms)); + final double height200ms = tester.getSize(find.byKey(firstKey)).height; + expect(height200ms, greaterThan(height150ms)); + expect(finalHeight, lessThan(height200ms)); // Hero a's return flight at 149ms. The outgoing (push) flight took // 150ms so we should be just about back to where Hero 'a' started. @@ -593,7 +593,7 @@ void main() { StateSetter heroCardSetState; // Show a 200x200 Hero tagged 'H', with key routeHeroKey - final MaterialPageRoute route = new MaterialPageRoute( + final MaterialPageRoute route = new MaterialPageRoute( builder: (BuildContext context) { return new Material( child: new ListView( @@ -695,7 +695,7 @@ void main() { const Key routeContainerKey = const Key('route hero container'); // Show a 200x200 Hero tagged 'H', with key routeHeroKey - final MaterialPageRoute route = new MaterialPageRoute( + final MaterialPageRoute route = new MaterialPageRoute( builder: (BuildContext context) { return new Material( child: new ListView( @@ -777,7 +777,7 @@ void main() { const Key routeContainerKey = const Key('route hero container'); // Show a 200x200 Hero tagged 'H', with key routeHeroKey - final MaterialPageRoute route = new MaterialPageRoute( + final MaterialPageRoute route = new MaterialPageRoute( builder: (BuildContext context) { return new Material( child: new ListView( @@ -855,7 +855,7 @@ void main() { const Key heroBCKey = const Key('BC hero'); // Show a 150x150 Hero tagged 'BC' - final MaterialPageRoute routeC = new MaterialPageRoute( + final MaterialPageRoute routeC = new MaterialPageRoute( builder: (BuildContext context) { return new Material( child: new ListView( @@ -872,7 +872,7 @@ void main() { ); // Show a height=200 Hero tagged 'AB' and a height=50 Hero tagged 'BC' - final MaterialPageRoute routeB = new MaterialPageRoute( + final MaterialPageRoute routeB = new MaterialPageRoute( builder: (BuildContext context) { return new Material( child: new ListView( @@ -956,7 +956,7 @@ void main() { }); testWidgets('Stateful hero child state survives flight', (WidgetTester tester) async { - final MaterialPageRoute route = new MaterialPageRoute( + final MaterialPageRoute route = new MaterialPageRoute( builder: (BuildContext context) { return new Material( child: new ListView( diff --git a/packages/flutter/test/widgets/navigator_test.dart b/packages/flutter/test/widgets/navigator_test.dart index 5f81f5070c9..e30e270a678 100644 --- a/packages/flutter/test/widgets/navigator_test.dart +++ b/packages/flutter/test/widgets/navigator_test.dart @@ -91,6 +91,7 @@ class TestObserver extends NavigatorObserver { OnObservation onPushed; OnObservation onPopped; OnObservation onRemoved; + OnObservation onReplaced; @override void didPush(Route route, Route previousRoute) { @@ -111,6 +112,12 @@ class TestObserver extends NavigatorObserver { if (onRemoved != null) onRemoved(route, previousRoute); } + + @override + void didReplace({ Route oldRoute, Route newRoute }) { + if (onReplaced != null) + onReplaced(newRoute, oldRoute); + } } void main() { @@ -193,19 +200,19 @@ void main() { child: new Navigator( onGenerateRoute: (RouteSettings settings) { if (settings.isInitialRoute) { - return new MaterialPageRoute( + return new MaterialPageRoute( builder: (BuildContext context) { return new RaisedButton( child: const Text('Next'), onPressed: () { Navigator.of(context).push( - new MaterialPageRoute( + new MaterialPageRoute( builder: (BuildContext context) { return new RaisedButton( child: const Text('Inner page'), onPressed: () { Navigator.of(context, rootNavigator: true).push( - new MaterialPageRoute( + new MaterialPageRoute( builder: (BuildContext context) { return const Text('Dialog'); } @@ -608,7 +615,7 @@ void main() { testWidgets('remove a route whose value is awaited', (WidgetTester tester) async { Future pageValue; final Map pageBuilders = { - '/': (BuildContext context) => new OnTapPage(id: '/', onTap: () { pageValue = Navigator.pushNamed(context, '/A'); }), + '/': (BuildContext context) => new OnTapPage(id: '/', onTap: () { pageValue = Navigator.pushNamed(context, '/A'); }), '/A': (BuildContext context) => new OnTapPage(id: 'A', onTap: () { Navigator.pop(context, 'A'); }), }; final Map> routes = >{}; @@ -633,5 +640,133 @@ void main() { navigator.removeRoute(routes['/A']); // stack becomes /, pageValue will not complete }); + testWidgets('replacing route can be observed', (WidgetTester tester) async { + final GlobalKey key = new GlobalKey(); + final List log = []; + final TestObserver observer = new TestObserver() + ..onPushed = (Route route, Route previousRoute) { + log.add('pushed ${route.settings.name} (previous is ${previousRoute == null ? "" : previousRoute.settings.name})'); + } + ..onPopped = (Route route, Route previousRoute) { + log.add('popped ${route.settings.name} (previous is ${previousRoute == null ? "" : previousRoute.settings.name})'); + } + ..onRemoved = (Route route, Route previousRoute) { + log.add('removed ${route.settings.name} (previous is ${previousRoute == null ? "" : previousRoute.settings.name})'); + } + ..onReplaced = (Route newRoute, Route oldRoute) { + log.add('replaced ${oldRoute.settings.name} with ${newRoute.settings.name}'); + }; + Route routeB; + await tester.pumpWidget(new MaterialApp( + navigatorKey: key, + navigatorObservers: [observer], + home: new FlatButton( + child: const Text('A'), + onPressed: () { + key.currentState.push(routeB = new MaterialPageRoute( + settings: const RouteSettings(name: 'B'), + builder: (BuildContext context) { + return new FlatButton( + child: const Text('B'), + onPressed: () { + key.currentState.push(new MaterialPageRoute( + settings: const RouteSettings(name: 'C'), + builder: (BuildContext context) { + return new FlatButton( + child: const Text('C'), + onPressed: () { + key.currentState.replace( + oldRoute: routeB, + newRoute: new MaterialPageRoute( + settings: const RouteSettings(name: 'D'), + builder: (BuildContext context) { + return const Text('D'); + }, + ), + ); + }, + ); + }, + )); + }, + ); + }, + )); + }, + ), + )); + expect(log, ['pushed / (previous is )']); + await tester.tap(find.text('A')); + await tester.pump(); + await tester.pump(const Duration(seconds: 1)); + expect(log, ['pushed / (previous is )', 'pushed B (previous is /)']); + await tester.tap(find.text('B')); + await tester.pump(); + await tester.pump(const Duration(seconds: 1)); + expect(log, ['pushed / (previous is )', 'pushed B (previous is /)', 'pushed C (previous is B)']); + await tester.tap(find.text('C')); + await tester.pump(); + await tester.pump(const Duration(seconds: 1)); + expect(log, ['pushed / (previous is )', 'pushed B (previous is /)', 'pushed C (previous is B)', 'replaced B with D']); + }); + testWidgets('ModalRoute.of sets up a route to rebuild if its state changes', (WidgetTester tester) async { + final GlobalKey key = new GlobalKey(); + final List log = []; + Route routeB; + await tester.pumpWidget(new MaterialApp( + navigatorKey: key, + home: new FlatButton( + child: const Text('A'), + onPressed: () { + key.currentState.push(routeB = new MaterialPageRoute( + settings: const RouteSettings(name: 'B'), + builder: (BuildContext context) { + log.add('building B'); + return new FlatButton( + child: const Text('B'), + onPressed: () { + key.currentState.push(new MaterialPageRoute( + settings: const RouteSettings(name: 'C'), + builder: (BuildContext context) { + log.add('building C'); + log.add('found ${ModalRoute.of(context).settings.name}'); + return new FlatButton( + child: const Text('C'), + onPressed: () { + key.currentState.replace( + oldRoute: routeB, + newRoute: new MaterialPageRoute( + settings: const RouteSettings(name: 'D'), + builder: (BuildContext context) { + log.add('building D'); + return const Text('D'); + }, + ), + ); + }, + ); + }, + )); + }, + ); + }, + )); + }, + ), + )); + expect(log, []); + await tester.tap(find.text('A')); + await tester.pumpAndSettle(const Duration(milliseconds: 10)); + expect(log, ['building B']); + await tester.tap(find.text('B')); + await tester.pumpAndSettle(const Duration(milliseconds: 10)); + expect(log, ['building B', 'building C', 'found C']); + await tester.tap(find.text('C')); + await tester.pumpAndSettle(const Duration(milliseconds: 10)); + expect(log, ['building B', 'building C', 'found C', 'building D']); + key.currentState.pop(); + await tester.pumpAndSettle(const Duration(milliseconds: 10)); + expect(log, ['building B', 'building C', 'found C', 'building D', 'building C', 'found C']); + }); } diff --git a/packages/flutter/test/widgets/page_forward_transitions_test.dart b/packages/flutter/test/widgets/page_forward_transitions_test.dart index fbd796e1563..ffb087b8599 100644 --- a/packages/flutter/test/widgets/page_forward_transitions_test.dart +++ b/packages/flutter/test/widgets/page_forward_transitions_test.dart @@ -86,7 +86,7 @@ void main() { child: new Builder( key: insideKey, builder: (BuildContext context) { - final PageRoute route = ModalRoute.of(context); + final PageRoute route = ModalRoute.of(context); return new Column( children: [ new TestTransition( diff --git a/packages/flutter/test/widgets/page_transitions_test.dart b/packages/flutter/test/widgets/page_transitions_test.dart index 7b18ff04f3c..43973d3ae91 100644 --- a/packages/flutter/test/widgets/page_transitions_test.dart +++ b/packages/flutter/test/widgets/page_transitions_test.dart @@ -5,7 +5,8 @@ import 'package:flutter_test/flutter_test.dart'; import 'package:flutter/material.dart'; -class TestOverlayRoute extends OverlayRoute { +class TestOverlayRoute extends OverlayRoute { + TestOverlayRoute({ RouteSettings settings }) : super(settings: settings); @override Iterable createOverlayEntries() sync* { yield new OverlayEntry(builder: _build); @@ -311,7 +312,7 @@ void main() { await tester.pumpWidget(new MaterialApp(routes: routes)); - final PageRoute route = new MaterialPageRoute( + final PageRoute route = new MaterialPageRoute( settings: const RouteSettings(name: '/page'), builder: (BuildContext context) => const Center(child: const Text('page')), ); diff --git a/packages/flutter/test/widgets/remember_scroll_position_test.dart b/packages/flutter/test/widgets/remember_scroll_position_test.dart index 1fd0568a3bd..d6792ddf4c9 100644 --- a/packages/flutter/test/widgets/remember_scroll_position_test.dart +++ b/packages/flutter/test/widgets/remember_scroll_position_test.dart @@ -37,13 +37,13 @@ Future performTest(WidgetTester tester, bool maintainState) async { key: navigatorKey, onGenerateRoute: (RouteSettings settings) { if (settings.name == '/') { - return new MaterialPageRoute( + return new MaterialPageRoute( settings: settings, builder: (_) => new Container(child: const ThePositiveNumbers(from: 0)), maintainState: maintainState, ); } else if (settings.name == '/second') { - return new MaterialPageRoute( + return new MaterialPageRoute( settings: settings, builder: (_) => new Container(child: const ThePositiveNumbers(from: 10000)), maintainState: maintainState, diff --git a/packages/flutter/test/widgets/routes_test.dart b/packages/flutter/test/widgets/routes_test.dart index 9de6b124554..cd64e8ae2b1 100644 --- a/packages/flutter/test/widgets/routes_test.dart +++ b/packages/flutter/test/widgets/routes_test.dart @@ -45,9 +45,11 @@ class TestRoute extends LocalHistoryRoute { } @override - void didReplace(covariant TestRoute oldRoute) { - log('didReplace ${oldRoute.name}'); - super.didReplace(oldRoute); + void didReplace(Route oldRoute) { + expect(oldRoute, const isInstanceOf()); + final TestRoute castRoute = oldRoute; + log('didReplace ${castRoute.name}'); + super.didReplace(castRoute); } @override @@ -60,15 +62,19 @@ class TestRoute extends LocalHistoryRoute { } @override - void didPopNext(covariant TestRoute nextRoute) { - log('didPopNext ${nextRoute.name}'); - super.didPopNext(nextRoute); + void didPopNext(Route nextRoute) { + expect(nextRoute, const isInstanceOf()); + final TestRoute castRoute = nextRoute; + log('didPopNext ${castRoute.name}'); + super.didPopNext(castRoute); } @override - void didChangeNext(covariant TestRoute nextRoute) { - log('didChangeNext ${nextRoute?.name}'); - super.didChangeNext(nextRoute); + void didChangeNext(Route nextRoute) { + expect(nextRoute, anyOf(isNull, const isInstanceOf())); + final TestRoute castRoute = nextRoute; + log('didChangeNext ${castRoute?.name}'); + super.didChangeNext(castRoute); } @override diff --git a/packages/flutter_localizations/test/override_test.dart b/packages/flutter_localizations/test/override_test.dart index 2184e12dd0d..66644098f0f 100644 --- a/packages/flutter_localizations/test/override_test.dart +++ b/packages/flutter_localizations/test/override_test.dart @@ -54,7 +54,7 @@ Widget buildFrame({ localizationsDelegates: delegates, localeResolutionCallback: localeResolutionCallback, onGenerateRoute: (RouteSettings settings) { - return new MaterialPageRoute( + return new MaterialPageRoute( builder: (BuildContext context) { return buildContent(context); } diff --git a/packages/flutter_localizations/test/time_picker_test.dart b/packages/flutter_localizations/test/time_picker_test.dart index 4395b94467d..66042057413 100644 --- a/packages/flutter_localizations/test/time_picker_test.dart +++ b/packages/flutter_localizations/test/time_picker_test.dart @@ -145,7 +145,7 @@ void main() { textDirection: TextDirection.ltr, child: new Navigator( onGenerateRoute: (RouteSettings settings) { - return new MaterialPageRoute(builder: (BuildContext context) { + return new MaterialPageRoute(builder: (BuildContext context) { return new FlatButton( onPressed: () { showTimePicker(context: context, initialTime: const TimeOfDay(hour: 7, minute: 0)); diff --git a/packages/flutter_localizations/test/widgets_test.dart b/packages/flutter_localizations/test/widgets_test.dart index 597341f7e25..b17013b3e66 100644 --- a/packages/flutter_localizations/test/widgets_test.dart +++ b/packages/flutter_localizations/test/widgets_test.dart @@ -153,7 +153,7 @@ Widget buildFrame({ localeResolutionCallback: localeResolutionCallback, supportedLocales: supportedLocales, onGenerateRoute: (RouteSettings settings) { - return new PageRouteBuilder( + return new PageRouteBuilder( pageBuilder: (BuildContext context, Animation _, Animation __) { return buildContent(context); }