Unverified Commit aba0379d authored by Ian Hickson's avatar Ian Hickson Committed by GitHub

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 `<void>` instead of `<Null>`,
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.
parent 3a2e0d93
...@@ -47,37 +47,37 @@ class _HomeState extends State<Home> { ...@@ -47,37 +47,37 @@ class _HomeState extends State<Home> {
child: const Text('Test Underlines'), child: const Text('Test Underlines'),
color: Colors.red.shade800, color: Colors.red.shade800,
textColor: Colors.white, textColor: Colors.white,
onPressed: () { Navigator.of(context).pushNamed('underlines'); }, onPressed: () { Navigator.pushNamed(context, 'underlines'); },
), ),
new FlatButton( new FlatButton(
child: const Text('Test Font Fallback'), child: const Text('Test Font Fallback'),
color: Colors.orange.shade700, color: Colors.orange.shade700,
textColor: Colors.white, textColor: Colors.white,
onPressed: () { Navigator.of(context).pushNamed('fallback'); }, onPressed: () { Navigator.pushNamed(context, 'fallback'); },
), ),
new FlatButton( new FlatButton(
child: const Text('Test Bidi Formatting'), child: const Text('Test Bidi Formatting'),
color: Colors.yellow.shade700, color: Colors.yellow.shade700,
textColor: Colors.black, textColor: Colors.black,
onPressed: () { Navigator.of(context).pushNamed('bidi'); }, onPressed: () { Navigator.pushNamed(context, 'bidi'); },
), ),
new FlatButton( new FlatButton(
child: const Text('TextSpan Fuzzer'), child: const Text('TextSpan Fuzzer'),
color: Colors.green.shade400, color: Colors.green.shade400,
textColor: Colors.black, textColor: Colors.black,
onPressed: () { Navigator.of(context).pushNamed('fuzzer'); }, onPressed: () { Navigator.pushNamed(context, 'fuzzer'); },
), ),
new FlatButton( new FlatButton(
child: const Text('Diacritics Fuzzer'), child: const Text('Diacritics Fuzzer'),
color: Colors.blue.shade400, color: Colors.blue.shade400,
textColor: Colors.white, textColor: Colors.white,
onPressed: () { Navigator.of(context).pushNamed('zalgo'); }, onPressed: () { Navigator.pushNamed(context, 'zalgo'); },
), ),
new FlatButton( new FlatButton(
child: const Text('Painting Fuzzer'), child: const Text('Painting Fuzzer'),
color: Colors.purple.shade200, color: Colors.purple.shade200,
textColor: Colors.black, textColor: Colors.black,
onPressed: () { Navigator.of(context).pushNamed('painting'); }, onPressed: () { Navigator.pushNamed(context, 'painting'); },
), ),
], ],
), ),
......
...@@ -451,7 +451,7 @@ class _AnimationDemoHomeState extends State<AnimationDemoHome> { ...@@ -451,7 +451,7 @@ class _AnimationDemoHomeState extends State<AnimationDemoHome> {
if (_scrollController.offset >= midScrollOffset) if (_scrollController.offset >= midScrollOffset)
_scrollController.animateTo(0.0, curve: _kScrollCurve, duration: _kScrollDuration); _scrollController.animateTo(0.0, curve: _kScrollCurve, duration: _kScrollDuration);
else else
Navigator.of(context).maybePop(); Navigator.maybePop(context);
} }
// Only enable paging for the heading when the user has scrolled to midScrollOffset. // Only enable paging for the heading when the user has scrolled to midScrollOffset.
......
...@@ -21,7 +21,7 @@ class _CupertinoDialogDemoState extends State<CupertinoDialogDemo> { ...@@ -21,7 +21,7 @@ class _CupertinoDialogDemoState extends State<CupertinoDialogDemo> {
barrierDismissible: false, barrierDismissible: false,
builder: (BuildContext context) => child, builder: (BuildContext context) => child,
) )
.then<Null>((T value) { // The value passed to Navigator.pop() or null. .then<void>((T value) { // The value passed to Navigator.pop() or null.
if (value != null) { if (value != null) {
_scaffoldKey.currentState.showSnackBar(new SnackBar( _scaffoldKey.currentState.showSnackBar(new SnackBar(
content: new Text('You selected: $value') content: new Text('You selected: $value')
......
...@@ -173,7 +173,7 @@ class Tab1RowItem extends StatelessWidget { ...@@ -173,7 +173,7 @@ class Tab1RowItem extends StatelessWidget {
final Widget row = new GestureDetector( final Widget row = new GestureDetector(
behavior: HitTestBehavior.opaque, behavior: HitTestBehavior.opaque,
onTap: () { onTap: () {
Navigator.of(context).push(new CupertinoPageRoute<Null>( Navigator.of(context).push(new CupertinoPageRoute<void>(
builder: (BuildContext context) => new Tab1ItemPage( builder: (BuildContext context) => new Tab1ItemPage(
color: color, color: color,
colorName: colorName, colorName: colorName,
...@@ -778,7 +778,7 @@ class Tab3Dialog extends StatelessWidget { ...@@ -778,7 +778,7 @@ class Tab3Dialog extends StatelessWidget {
color: CupertinoColors.activeBlue, color: CupertinoColors.activeBlue,
child: const Text('Sign in'), child: const Text('Sign in'),
onPressed: () { onPressed: () {
Navigator.of(context).pop(); Navigator.pop(context);
}, },
), ),
], ],
......
...@@ -112,7 +112,7 @@ class _CupertinoPickerDemoState extends State<CupertinoPickerDemo> { ...@@ -112,7 +112,7 @@ class _CupertinoPickerDemoState extends State<CupertinoPickerDemo> {
const Padding(padding: const EdgeInsets.only(top: 32.0)), const Padding(padding: const EdgeInsets.only(top: 32.0)),
new GestureDetector( new GestureDetector(
onTap: () async { onTap: () async {
await showModalBottomSheet<Null>( await showModalBottomSheet<void>(
context: context, context: context,
builder: (BuildContext context) { builder: (BuildContext context) {
return _buildBottomPicker(); return _buildBottomPicker();
......
...@@ -70,7 +70,7 @@ class DialogDemoState extends State<DialogDemo> { ...@@ -70,7 +70,7 @@ class DialogDemoState extends State<DialogDemo> {
context: context, context: context,
builder: (BuildContext context) => child, builder: (BuildContext context) => child,
) )
.then<Null>((T value) { // The value passed to Navigator.pop() or null. .then<void>((T value) { // The value passed to Navigator.pop() or null.
if (value != null) { if (value != null) {
_scaffoldKey.currentState.showSnackBar(new SnackBar( _scaffoldKey.currentState.showSnackBar(new SnackBar(
content: new Text('You selected: $value') content: new Text('You selected: $value')
......
...@@ -67,7 +67,7 @@ class _DrawerDemoState extends State<DrawerDemo> with TickerProviderStateMixin { ...@@ -67,7 +67,7 @@ class _DrawerDemoState extends State<DrawerDemo> with TickerProviderStateMixin {
} }
void _showNotImplementedMessage() { void _showNotImplementedMessage() {
Navigator.of(context).pop(); // Dismiss the drawer. Navigator.pop(context); // Dismiss the drawer.
_scaffoldKey.currentState.showSnackBar(const SnackBar( _scaffoldKey.currentState.showSnackBar(const SnackBar(
content: const Text("The drawer's items don't do anything") content: const Text("The drawer's items don't do anything")
)); ));
...@@ -231,7 +231,7 @@ class _DrawerDemoState extends State<DrawerDemo> with TickerProviderStateMixin { ...@@ -231,7 +231,7 @@ class _DrawerDemoState extends State<DrawerDemo> with TickerProviderStateMixin {
} }
void _onOtherAccountsTap(BuildContext context) { void _onOtherAccountsTap(BuildContext context) {
showDialog<Null>( showDialog<void>(
context: context, context: context,
builder: (BuildContext context) { builder: (BuildContext context) {
return new AlertDialog( return new AlertDialog(
...@@ -240,7 +240,7 @@ class _DrawerDemoState extends State<DrawerDemo> with TickerProviderStateMixin { ...@@ -240,7 +240,7 @@ class _DrawerDemoState extends State<DrawerDemo> with TickerProviderStateMixin {
new FlatButton( new FlatButton(
child: const Text('OK'), child: const Text('OK'),
onPressed: () { onPressed: () {
Navigator.of(context).pop(); Navigator.pop(context);
}, },
), ),
], ],
......
...@@ -165,7 +165,7 @@ class GridDemoPhotoItem extends StatelessWidget { ...@@ -165,7 +165,7 @@ class GridDemoPhotoItem extends StatelessWidget {
final BannerTapCallback onBannerTap; // User taps on the photo's header or footer. final BannerTapCallback onBannerTap; // User taps on the photo's header or footer.
void showPhoto(BuildContext context) { void showPhoto(BuildContext context) {
Navigator.push(context, new MaterialPageRoute<Null>( Navigator.push(context, new MaterialPageRoute<void>(
builder: (BuildContext context) { builder: (BuildContext context) {
return new Scaffold( return new Scaffold(
appBar: new AppBar( appBar: new AppBar(
......
...@@ -15,7 +15,7 @@ class ModalBottomSheetDemo extends StatelessWidget { ...@@ -15,7 +15,7 @@ class ModalBottomSheetDemo extends StatelessWidget {
child: new RaisedButton( child: new RaisedButton(
child: const Text('SHOW BOTTOM SHEET'), child: const Text('SHOW BOTTOM SHEET'),
onPressed: () { onPressed: () {
showModalBottomSheet<Null>(context: context, builder: (BuildContext context) { showModalBottomSheet<void>(context: context, builder: (BuildContext context) {
return new Container( return new Container(
child: new Padding( child: new Padding(
padding: const EdgeInsets.all(32.0), padding: const EdgeInsets.all(32.0),
......
...@@ -54,7 +54,7 @@ class _PersistentBottomSheetDemoState extends State<PersistentBottomSheetDemo> { ...@@ -54,7 +54,7 @@ class _PersistentBottomSheetDemoState extends State<PersistentBottomSheetDemo> {
} }
void _showMessage() { void _showMessage() {
showDialog<Null>( showDialog<void>(
context: context, context: context,
builder: (BuildContext context) { builder: (BuildContext context) {
return new AlertDialog( return new AlertDialog(
......
...@@ -167,14 +167,14 @@ class _RecipeGridPageState extends State<RecipeGridPage> { ...@@ -167,14 +167,14 @@ class _RecipeGridPageState extends State<RecipeGridPage> {
} }
void showFavoritesPage(BuildContext context) { void showFavoritesPage(BuildContext context) {
Navigator.push(context, new MaterialPageRoute<Null>( Navigator.push(context, new MaterialPageRoute<void>(
settings: const RouteSettings(name: '/pesto/favorites'), settings: const RouteSettings(name: '/pesto/favorites'),
builder: (BuildContext context) => new PestoFavorites(), builder: (BuildContext context) => new PestoFavorites(),
)); ));
} }
void showRecipePage(BuildContext context, Recipe recipe) { void showRecipePage(BuildContext context, Recipe recipe) {
Navigator.push(context, new MaterialPageRoute<Null>( Navigator.push(context, new MaterialPageRoute<void>(
settings: const RouteSettings(name: '/pesto/recipe'), settings: const RouteSettings(name: '/pesto/recipe'),
builder: (BuildContext context) { builder: (BuildContext context) {
return new Theme( return new Theme(
......
...@@ -327,7 +327,7 @@ class ShrineOrderRoute extends ShrinePageRoute<Order> { ...@@ -327,7 +327,7 @@ class ShrineOrderRoute extends ShrinePageRoute<Order> {
ShrineOrderRoute({ ShrineOrderRoute({
@required this.order, @required this.order,
WidgetBuilder builder, WidgetBuilder builder,
RouteSettings settings: const RouteSettings(), RouteSettings settings,
}) : assert(order != null), }) : assert(order != null),
super(builder: builder, settings: settings); super(builder: builder, settings: settings);
...@@ -336,5 +336,5 @@ class ShrineOrderRoute extends ShrinePageRoute<Order> { ...@@ -336,5 +336,5 @@ class ShrineOrderRoute extends ShrinePageRoute<Order> {
@override @override
Order get currentResult => order; Order get currentResult => order;
static ShrineOrderRoute of(BuildContext context) => ModalRoute.of(context); static ShrineOrderRoute of(BuildContext context) => ModalRoute.of<Order>(context);
} }
...@@ -51,7 +51,7 @@ class ShrinePageState extends State<ShrinePage> { ...@@ -51,7 +51,7 @@ class ShrinePageState extends State<ShrinePage> {
} }
void _showShoppingCart() { void _showShoppingCart() {
showModalBottomSheet<Null>(context: context, builder: (BuildContext context) { showModalBottomSheet<void>(context: context, builder: (BuildContext context) {
if (widget.shoppingCart.isEmpty) { if (widget.shoppingCart.isEmpty) {
return const Padding( return const Padding(
padding: const EdgeInsets.all(24.0), padding: const EdgeInsets.all(24.0),
......
...@@ -25,7 +25,7 @@ Widget buildShrine(BuildContext context, Widget child) { ...@@ -25,7 +25,7 @@ Widget buildShrine(BuildContext context, Widget child) {
class ShrinePageRoute<T> extends MaterialPageRoute<T> { class ShrinePageRoute<T> extends MaterialPageRoute<T> {
ShrinePageRoute({ ShrinePageRoute({
WidgetBuilder builder, WidgetBuilder builder,
RouteSettings settings: const RouteSettings() RouteSettings settings,
}) : super(builder: builder, settings: settings); }) : super(builder: builder, settings: settings);
@override @override
......
...@@ -64,12 +64,12 @@ class VideoCard extends StatelessWidget { ...@@ -64,12 +64,12 @@ class VideoCard extends StatelessWidget {
} }
void pushFullScreenWidget() { void pushFullScreenWidget() {
final TransitionRoute<Null> route = new PageRouteBuilder<Null>( final TransitionRoute<void> route = new PageRouteBuilder<void>(
settings: new RouteSettings(name: title, isInitialRoute: false), settings: new RouteSettings(name: title, isInitialRoute: false),
pageBuilder: fullScreenRoutePageBuilder, pageBuilder: fullScreenRoutePageBuilder,
); );
route.completed.then((Null _) { route.completed.then((void result) {
controller.setVolume(0.0); controller.setVolume(0.0);
}); });
......
...@@ -165,7 +165,7 @@ class GalleryAppState extends State<GalleryApp> { ...@@ -165,7 +165,7 @@ class GalleryAppState extends State<GalleryApp> {
checkerboardRasterCacheImages: _checkerboardRasterCacheImages, checkerboardRasterCacheImages: _checkerboardRasterCacheImages,
checkerboardOffscreenLayers: _checkerboardOffscreenLayers, checkerboardOffscreenLayers: _checkerboardOffscreenLayers,
routes: _kRoutes, routes: _kRoutes,
home: _applyScaleFactor(home), home: home,
builder: (BuildContext context, Widget child) { builder: (BuildContext context, Widget child) {
return new Directionality( return new Directionality(
textDirection: _overrideDirection, textDirection: _overrideDirection,
......
...@@ -31,23 +31,23 @@ class UpdaterState extends State<Updater> { ...@@ -31,23 +31,23 @@ class UpdaterState extends State<Updater> {
} }
static DateTime _lastUpdateCheck; static DateTime _lastUpdateCheck;
Future<Null> _checkForUpdates() async { Future<void> _checkForUpdates() async {
// Only prompt once a day // Only prompt once a day
if (_lastUpdateCheck != null && if (_lastUpdateCheck != null &&
new DateTime.now().difference(_lastUpdateCheck) < const Duration(days: 1)) { 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(); _lastUpdateCheck = new DateTime.now();
final String updateUrl = await widget.updateUrlFetcher(); final String updateUrl = await widget.updateUrlFetcher();
if (updateUrl != null) { if (updateUrl != null) {
final bool wantsUpdate = await showDialog(context: context, builder: _buildDialog); final bool wantsUpdate = await showDialog<bool>(context: context, builder: _buildDialog);
if (wantsUpdate != null && wantsUpdate) if (wantsUpdate != null && wantsUpdate)
launch(updateUrl); launch(updateUrl);
} }
} }
Widget _buildDialog(BuildContext _) { Widget _buildDialog(BuildContext context) {
final ThemeData theme = Theme.of(context); final ThemeData theme = Theme.of(context);
final TextStyle dialogTextStyle = final TextStyle dialogTextStyle =
theme.textTheme.subhead.copyWith(color: theme.textTheme.caption.color); theme.textTheme.subhead.copyWith(color: theme.textTheme.caption.color);
...@@ -56,16 +56,19 @@ class UpdaterState extends State<Updater> { ...@@ -56,16 +56,19 @@ class UpdaterState extends State<Updater> {
content: new Text('A newer version is available.', style: dialogTextStyle), content: new Text('A newer version is available.', style: dialogTextStyle),
actions: <Widget>[ actions: <Widget>[
new FlatButton( new FlatButton(
child: const Text('NO THANKS'), child: const Text('NO THANKS'),
onPressed: () { onPressed: () {
Navigator.pop(context, false); Navigator.pop(context, false);
}), },
),
new FlatButton( new FlatButton(
child: const Text('UPDATE'), child: const Text('UPDATE'),
onPressed: () { onPressed: () {
Navigator.pop(context, true); Navigator.pop(context, true);
}), },
]); ),
],
);
} }
@override @override
......
...@@ -83,7 +83,7 @@ class StocksAppState extends State<StocksApp> { ...@@ -83,7 +83,7 @@ class StocksAppState extends State<StocksApp> {
return null; return null;
} }
Route<Null> _getRoute(RouteSettings settings) { Route<dynamic> _getRoute(RouteSettings settings) {
// Routes, by convention, are split on slashes, like filesystem paths. // Routes, by convention, are split on slashes, like filesystem paths.
final List<String> path = settings.name.split('/'); final List<String> path = settings.name.split('/');
// We only support paths that start with a slash, so bail if // We only support paths that start with a slash, so bail if
...@@ -100,7 +100,7 @@ class StocksAppState extends State<StocksApp> { ...@@ -100,7 +100,7 @@ class StocksAppState extends State<StocksApp> {
// Extract the symbol part of "stock:..." and return a route // Extract the symbol part of "stock:..." and return a route
// for that symbol. // for that symbol.
final String symbol = path[1].substring(6); final String symbol = path[1].substring(6);
return new MaterialPageRoute<Null>( return new MaterialPageRoute<void>(
settings: settings, settings: settings,
builder: (BuildContext context) => new StockSymbolPage(symbol: symbol, stocks: stocks), builder: (BuildContext context) => new StockSymbolPage(symbol: symbol, stocks: stocks),
); );
......
...@@ -93,7 +93,7 @@ class StockHomeState extends State<StockHome> { ...@@ -93,7 +93,7 @@ class StockHomeState extends State<StockHome> {
}); });
break; break;
case _StockMenuItem.refresh: case _StockMenuItem.refresh:
showDialog<Null>( showDialog<void>(
context: context, context: context,
builder: (BuildContext context) => new _NotImplementedDialog(), builder: (BuildContext context) => new _NotImplementedDialog(),
); );
...@@ -297,7 +297,7 @@ class StockHomeState extends State<StockHome> { ...@@ -297,7 +297,7 @@ class StockHomeState extends State<StockHome> {
} }
void _handleCreateCompany() { void _handleCreateCompany() {
showModalBottomSheet<Null>( showModalBottomSheet<void>(
context: context, context: context,
builder: (BuildContext context) => new _CreateCompanySheet(), builder: (BuildContext context) => new _CreateCompanySheet(),
); );
......
...@@ -397,7 +397,7 @@ class _CupertinoPersistentNavigationBar extends StatelessWidget implements Prefe ...@@ -397,7 +397,7 @@ class _CupertinoPersistentNavigationBar extends StatelessWidget implements Prefe
) )
: const Text('Close'), : const Text('Close'),
padding: EdgeInsets.zero, padding: EdgeInsets.zero,
onPressed: () { Navigator.of(context).maybePop(); }, onPressed: () { Navigator.maybePop(context); },
); );
} }
} }
......
...@@ -76,16 +76,15 @@ final DecorationTween _kGradientShadowTween = new DecorationTween( ...@@ -76,16 +76,15 @@ final DecorationTween _kGradientShadowTween = new DecorationTween(
class CupertinoPageRoute<T> extends PageRoute<T> { class CupertinoPageRoute<T> extends PageRoute<T> {
/// Creates a page route for use in an iOS designed app. /// Creates a page route for use in an iOS designed app.
/// ///
/// The [builder], [settings], [maintainState], and [fullscreenDialog] /// The [builder], [maintainState], and [fullscreenDialog] arguments must not
/// arguments must not be null. /// be null.
CupertinoPageRoute({ CupertinoPageRoute({
@required this.builder, @required this.builder,
RouteSettings settings: const RouteSettings(), RouteSettings settings,
this.maintainState: true, this.maintainState: true,
bool fullscreenDialog: false, bool fullscreenDialog: false,
this.hostRoute, this.hostRoute,
}) : assert(builder != null), }) : assert(builder != null),
assert(settings != null),
assert(maintainState != null), assert(maintainState != null),
assert(fullscreenDialog != null), assert(fullscreenDialog != null),
super(settings: settings, fullscreenDialog: fullscreenDialog) { super(settings: settings, fullscreenDialog: fullscreenDialog) {
...@@ -149,7 +148,7 @@ class CupertinoPageRoute<T> extends PageRoute<T> { ...@@ -149,7 +148,7 @@ class CupertinoPageRoute<T> extends PageRoute<T> {
super.dispose(); super.dispose();
} }
_CupertinoBackGestureController _backGestureController; _CupertinoBackGestureController<T> _backGestureController;
/// Whether a pop gesture is currently underway. /// Whether a pop gesture is currently underway.
/// ///
...@@ -217,11 +216,11 @@ class CupertinoPageRoute<T> extends PageRoute<T> { ...@@ -217,11 +216,11 @@ class CupertinoPageRoute<T> extends PageRoute<T> {
/// appropriate. /// appropriate.
/// * [Route.startPopGesture], which describes the contract that this method /// * [Route.startPopGesture], which describes the contract that this method
/// must implement. /// must implement.
_CupertinoBackGestureController _startPopGesture() { _CupertinoBackGestureController<T> _startPopGesture() {
assert(!popGestureInProgress); assert(!popGestureInProgress);
assert(popGestureEnabled); assert(popGestureEnabled);
final PageRoute<T> route = hostRoute ?? this; final PageRoute<T> route = hostRoute ?? this;
_backGestureController = new _CupertinoBackGestureController( _backGestureController = new _CupertinoBackGestureController<T>(
navigator: route.navigator, navigator: route.navigator,
controller: route.controller, controller: route.controller,
onEnded: _endPopGesture, onEnded: _endPopGesture,
...@@ -265,7 +264,7 @@ class CupertinoPageRoute<T> extends PageRoute<T> { ...@@ -265,7 +264,7 @@ class CupertinoPageRoute<T> extends PageRoute<T> {
// In the middle of a back gesture drag, let the transition be linear to // In the middle of a back gesture drag, let the transition be linear to
// match finger motions. // match finger motions.
linearTransition: popGestureInProgress, linearTransition: popGestureInProgress,
child: new _CupertinoBackGestureDetector( child: new _CupertinoBackGestureDetector<T>(
enabledCallback: () => popGestureEnabled, enabledCallback: () => popGestureEnabled,
onStartPopGesture: _startPopGesture, onStartPopGesture: _startPopGesture,
child: child, child: child,
...@@ -392,7 +391,10 @@ class CupertinoFullscreenDialogTransition extends StatelessWidget { ...@@ -392,7 +391,10 @@ class CupertinoFullscreenDialogTransition extends StatelessWidget {
/// ///
/// The gesture data is converted from absolute coordinates to logical /// The gesture data is converted from absolute coordinates to logical
/// coordinates by this widget. /// 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<T> extends StatefulWidget {
const _CupertinoBackGestureDetector({ const _CupertinoBackGestureDetector({
Key key, Key key,
@required this.enabledCallback, @required this.enabledCallback,
...@@ -407,14 +409,14 @@ class _CupertinoBackGestureDetector extends StatefulWidget { ...@@ -407,14 +409,14 @@ class _CupertinoBackGestureDetector extends StatefulWidget {
final ValueGetter<bool> enabledCallback; final ValueGetter<bool> enabledCallback;
final ValueGetter<_CupertinoBackGestureController> onStartPopGesture; final ValueGetter<_CupertinoBackGestureController<T>> onStartPopGesture;
@override @override
_CupertinoBackGestureDetectorState createState() => new _CupertinoBackGestureDetectorState(); _CupertinoBackGestureDetectorState<T> createState() => new _CupertinoBackGestureDetectorState<T>();
} }
class _CupertinoBackGestureDetectorState extends State<_CupertinoBackGestureDetector> { class _CupertinoBackGestureDetectorState<T> extends State<_CupertinoBackGestureDetector<T>> {
_CupertinoBackGestureController _backGestureController; _CupertinoBackGestureController<T> _backGestureController;
HorizontalDragGestureRecognizer _recognizer; HorizontalDragGestureRecognizer _recognizer;
...@@ -508,7 +510,10 @@ class _CupertinoBackGestureDetectorState extends State<_CupertinoBackGestureDete ...@@ -508,7 +510,10 @@ class _CupertinoBackGestureDetectorState extends State<_CupertinoBackGestureDete
/// ///
/// This class works entirely in logical coordinates (0.0 is new page dismissed, /// This class works entirely in logical coordinates (0.0 is new page dismissed,
/// 1.0 is new page on top). /// 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<T> {
/// Creates a controller for an iOS-style back gesture. /// Creates a controller for an iOS-style back gesture.
/// ///
/// The [navigator] and [controller] arguments must not be null. /// The [navigator] and [controller] arguments must not be null.
...@@ -566,7 +571,7 @@ class _CupertinoBackGestureController { ...@@ -566,7 +571,7 @@ class _CupertinoBackGestureController {
controller.removeStatusListener(_handleStatusChanged); controller.removeStatusListener(_handleStatusChanged);
_animating = false; _animating = false;
if (status == AnimationStatus.dismissed) if (status == AnimationStatus.dismissed)
navigator.pop(); // this will cause the route to get disposed, which will dispose us navigator.pop<T>(); // 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 onEnded(); // this will call dispose if popping the route failed to do so
} }
......
...@@ -149,9 +149,9 @@ void showAboutDialog({ ...@@ -149,9 +149,9 @@ void showAboutDialog({
String applicationVersion, String applicationVersion,
Widget applicationIcon, Widget applicationIcon,
String applicationLegalese, String applicationLegalese,
List<Widget> children List<Widget> children,
}) { }) {
showDialog<Null>( showDialog<void>(
context: context, context: context,
builder: (BuildContext context) { builder: (BuildContext context) {
return new AboutDialog( return new AboutDialog(
...@@ -185,9 +185,7 @@ void showLicensePage({ ...@@ -185,9 +185,7 @@ void showLicensePage({
Widget applicationIcon, Widget applicationIcon,
String applicationLegalese String applicationLegalese
}) { }) {
// TODO(ianh): remove pop once https://github.com/flutter/flutter/issues/4667 is fixed Navigator.push(context, new MaterialPageRoute<void>(
Navigator.pop(context);
Navigator.push(context, new MaterialPageRoute<Null>(
builder: (BuildContext context) => new LicensePage( builder: (BuildContext context) => new LicensePage(
applicationName: applicationName, applicationName: applicationName,
applicationVersion: applicationVersion, applicationVersion: applicationVersion,
......
...@@ -607,10 +607,11 @@ class _MaterialAppState extends State<MaterialApp> { ...@@ -607,10 +607,11 @@ class _MaterialAppState extends State<MaterialApp> {
Route<dynamic> _onGenerateRoute(RouteSettings settings) { Route<dynamic> _onGenerateRoute(RouteSettings settings) {
final String name = settings.name; final String name = settings.name;
WidgetBuilder builder; WidgetBuilder builder;
if (name == Navigator.defaultRouteName && widget.home != null) if (name == Navigator.defaultRouteName && widget.home != null) {
builder = (BuildContext context) => widget.home; builder = (BuildContext context) => widget.home;
else } else {
builder = widget.routes[name]; builder = widget.routes[name];
}
if (builder != null) { if (builder != null) {
return new MaterialPageRoute<dynamic>( return new MaterialPageRoute<dynamic>(
builder: builder, builder: builder,
......
...@@ -86,7 +86,7 @@ class BackButton extends StatelessWidget { ...@@ -86,7 +86,7 @@ class BackButton extends StatelessWidget {
color: color, color: color,
tooltip: MaterialLocalizations.of(context).backButtonTooltip, tooltip: MaterialLocalizations.of(context).backButtonTooltip,
onPressed: () { onPressed: () {
Navigator.of(context).maybePop(); Navigator.maybePop(context);
} }
); );
} }
...@@ -118,7 +118,7 @@ class CloseButton extends StatelessWidget { ...@@ -118,7 +118,7 @@ class CloseButton extends StatelessWidget {
icon: const Icon(Icons.close), icon: const Icon(Icons.close),
tooltip: MaterialLocalizations.of(context).closeButtonTooltip, tooltip: MaterialLocalizations.of(context).closeButtonTooltip,
onPressed: () { onPressed: () {
Navigator.of(context).maybePop(); Navigator.maybePop(context);
}, },
); );
} }
......
...@@ -179,14 +179,10 @@ class _ModalBottomSheet<T> extends StatefulWidget { ...@@ -179,14 +179,10 @@ class _ModalBottomSheet<T> extends StatefulWidget {
} }
class _ModalBottomSheetState<T> extends State<_ModalBottomSheet<T>> { class _ModalBottomSheetState<T> extends State<_ModalBottomSheet<T>> {
void _navigatorPop() {
Navigator.pop(context);
}
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
return new GestureDetector( return new GestureDetector(
onTap: _navigatorPop, onTap: () => Navigator.pop(context),
child: new AnimatedBuilder( child: new AnimatedBuilder(
animation: widget.route.animation, animation: widget.route.animation,
builder: (BuildContext context, Widget child) { builder: (BuildContext context, Widget child) {
...@@ -211,7 +207,8 @@ class _ModalBottomSheetRoute<T> extends PopupRoute<T> { ...@@ -211,7 +207,8 @@ class _ModalBottomSheetRoute<T> extends PopupRoute<T> {
this.builder, this.builder,
this.theme, this.theme,
this.barrierLabel, this.barrierLabel,
}); RouteSettings settings,
}) : super(settings: settings);
final WidgetBuilder builder; final WidgetBuilder builder;
final ThemeData theme; final ThemeData theme;
......
...@@ -488,8 +488,10 @@ class _DialogRoute<T> extends PopupRoute<T> { ...@@ -488,8 +488,10 @@ class _DialogRoute<T> extends PopupRoute<T> {
bool barrierDismissible: true, bool barrierDismissible: true,
this.barrierLabel, this.barrierLabel,
@required this.child, @required this.child,
RouteSettings settings,
}) : assert(barrierDismissible != null), }) : assert(barrierDismissible != null),
_barrierDismissible = barrierDismissible; _barrierDismissible = barrierDismissible,
super(settings: settings);
final Widget child; final Widget child;
final ThemeData theme; final ThemeData theme;
......
...@@ -600,7 +600,7 @@ class _DropdownButtonState<T> extends State<DropdownButton<T>> with WidgetsBindi ...@@ -600,7 +600,7 @@ class _DropdownButtonState<T> extends State<DropdownButton<T>> with WidgetsBindi
Navigator.push(context, _dropdownRoute).then<void>((_DropdownRouteResult<T> newValue) { Navigator.push(context, _dropdownRoute).then<void>((_DropdownRouteResult<T> newValue) {
_dropdownRoute = null; _dropdownRoute = null;
if (!mounted || newValue == null) if (!mounted || newValue == null)
return null; return;
if (widget.onChanged != null) if (widget.onChanged != null)
widget.onChanged(newValue.result); widget.onChanged(newValue.result);
}); });
......
...@@ -48,34 +48,38 @@ class _MountainViewPageTransition extends StatelessWidget { ...@@ -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 /// For Android, the entrance transition for the page slides the page upwards
/// in. The exit transition is the same, but in reverse. /// 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 /// The transition is adaptive to the platform and on iOS, the page slides in
/// exits in reverse. The page also shifts to the left in parallax when another page enters to /// from the right and exits in reverse. The page also shifts to the left in
/// cover it. /// 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 /// 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 /// remains in memory. To free all the resources when this is not necessary, set
/// [maintainState] to false. /// [maintainState] to false.
/// ///
/// Specify whether the incoming page is a fullscreen modal dialog. On iOS, those /// The `fullscreenDialog` property specifies whether the incoming page is a
/// pages animate bottom->up rather than right->left. /// 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 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 /// the route is popped from the stack via [Navigator.pop] by providing the
/// `result` can be provided. /// optional `result` argument.
/// ///
/// See also: /// See also:
/// ///
/// * [CupertinoPageRoute], that this [PageRoute] delegates transition animations to for iOS. /// * [CupertinoPageRoute], which this [PageRoute] delegates transition
/// animations to for iOS.
class MaterialPageRoute<T> extends PageRoute<T> { class MaterialPageRoute<T> extends PageRoute<T> {
/// Creates a page route for use in a material design app. /// Creates a page route for use in a material design app.
MaterialPageRoute({ MaterialPageRoute({
@required this.builder, @required this.builder,
RouteSettings settings: const RouteSettings(), RouteSettings settings,
this.maintainState: true, this.maintainState: true,
bool fullscreenDialog: false, bool fullscreenDialog: false,
}) : assert(builder != null), }) : assert(builder != null),
......
...@@ -196,15 +196,44 @@ class PopupMenuItem<T> extends PopupMenuEntry<T> { ...@@ -196,15 +196,44 @@ class PopupMenuItem<T> extends PopupMenuEntry<T> {
bool represents(T value) => value == this.value; bool represents(T value) => value == this.value;
@override @override
_PopupMenuItemState<PopupMenuItem<T>> createState() => new _PopupMenuItemState<PopupMenuItem<T>>(); PopupMenuItemState<T, PopupMenuItem<T>> createState() => new PopupMenuItemState<T, PopupMenuItem<T>>();
} }
class _PopupMenuItemState<T extends PopupMenuItem<dynamic>> extends State<T> { /// The [State] for [PopupMenuItem] subclasses.
// Override this to put something else in the menu entry. ///
/// 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<T, W extends PopupMenuItem<T>> extends State<W> {
/// 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; 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() { void handleTap() {
Navigator.pop(context, widget.value); Navigator.pop<T>(context, widget.value);
} }
@override @override
...@@ -238,8 +267,8 @@ class _PopupMenuItemState<T extends PopupMenuItem<dynamic>> extends State<T> { ...@@ -238,8 +267,8 @@ class _PopupMenuItemState<T extends PopupMenuItem<dynamic>> extends State<T> {
height: widget.height, height: widget.height,
padding: const EdgeInsets.symmetric(horizontal: _kMenuHorizontalPadding), padding: const EdgeInsets.symmetric(horizontal: _kMenuHorizontalPadding),
child: item, child: item,
) ),
) ),
); );
} }
} }
...@@ -349,7 +378,7 @@ class CheckedPopupMenuItem<T> extends PopupMenuItem<T> { ...@@ -349,7 +378,7 @@ class CheckedPopupMenuItem<T> extends PopupMenuItem<T> {
_CheckedPopupMenuItemState<T> createState() => new _CheckedPopupMenuItemState<T>(); _CheckedPopupMenuItemState<T> createState() => new _CheckedPopupMenuItemState<T>();
} }
class _CheckedPopupMenuItemState<T> extends _PopupMenuItemState<CheckedPopupMenuItem<T>> with SingleTickerProviderStateMixin { class _CheckedPopupMenuItemState<T> extends PopupMenuItemState<T, CheckedPopupMenuItem<T>> with SingleTickerProviderStateMixin {
static const Duration _kFadeDuration = const Duration(milliseconds: 150); static const Duration _kFadeDuration = const Duration(milliseconds: 150);
AnimationController _controller; AnimationController _controller;
Animation<double> get _opacity => _controller.view; Animation<double> get _opacity => _controller.view;
......
...@@ -680,6 +680,9 @@ class SemanticsProperties extends DiagnosticableTree { ...@@ -680,6 +680,9 @@ class SemanticsProperties extends DiagnosticableTree {
properties.add(new EnumProperty<TextDirection>('textDirection', textDirection, defaultValue: null)); properties.add(new EnumProperty<TextDirection>('textDirection', textDirection, defaultValue: null));
properties.add(new DiagnosticsProperty<SemanticsSortKey>('sortKey', sortKey, defaultValue: null)); properties.add(new DiagnosticsProperty<SemanticsSortKey>('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 /// In tests use this function to reset the counter used to generate
......
...@@ -16,6 +16,25 @@ import 'framework.dart'; ...@@ -16,6 +16,25 @@ import 'framework.dart';
import 'overlay.dart'; import 'overlay.dart';
import 'ticker_provider.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<dynamic> RouteFactory(RouteSettings settings);
/// Signature for the [Navigator.popUntil] predicate argument.
typedef bool RoutePredicate(Route<dynamic> 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<bool> WillPopCallback();
/// Indicates whether the current route should be popped. /// Indicates whether the current route should be popped.
/// ///
/// Used as the return value for [Route.willPop]. /// Used as the return value for [Route.willPop].
...@@ -43,12 +62,6 @@ enum RoutePopDisposition { ...@@ -43,12 +62,6 @@ enum RoutePopDisposition {
bubble, 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<bool> WillPopCallback();
/// An abstraction for an entry managed by a [Navigator]. /// An abstraction for an entry managed by a [Navigator].
/// ///
/// This class defines an abstract interface between the navigator and the /// This class defines an abstract interface between the navigator and the
...@@ -62,27 +75,35 @@ typedef Future<bool> WillPopCallback(); ...@@ -62,27 +75,35 @@ typedef Future<bool> WillPopCallback();
/// See [MaterialPageRoute] for a route that replaces the /// See [MaterialPageRoute] for a route that replaces the
/// entire screen with a platform-adaptive transition. /// entire screen with a platform-adaptive transition.
abstract class Route<T> { abstract class Route<T> {
/// 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. /// The navigator that the route is in, if any.
NavigatorState get navigator => _navigator; NavigatorState get navigator => _navigator;
NavigatorState _navigator; NavigatorState _navigator;
/// The settings for this route.
///
/// See [RouteSettings] for details.
final RouteSettings settings;
/// The overlay entries for this route. /// The overlay entries for this route.
List<OverlayEntry> get overlayEntries => const <OverlayEntry>[]; List<OverlayEntry> get overlayEntries => const <OverlayEntry>[];
/// 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<T> get popped => _popCompleter.future;
final Completer<T> _popCompleter = new Completer<T>();
/// Called when the route is inserted into the navigator. /// Called when the route is inserted into the navigator.
/// ///
/// Use this to populate overlayEntries and add them to the overlay /// Use this to populate [overlayEntries] and add them to the overlay
/// (accessible as navigator.overlay). (The reason the Route is responsible /// (accessible as [Navigator.overlay]). (The reason the [Route] is
/// for doing this, rather than the Navigator, is that the Route will be /// responsible for doing this, rather than the [Navigator], is that the
/// responsible for _removing_ the entries and this way it's symmetric.) /// [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 @protected
@mustCallSuper @mustCallSuper
void install(OverlayEntry insertionPoint) { } void install(OverlayEntry insertionPoint) { }
...@@ -90,14 +111,16 @@ abstract class Route<T> { ...@@ -90,14 +111,16 @@ abstract class Route<T> {
/// Called after [install] when the route is pushed onto the navigator. /// Called after [install] when the route is pushed onto the navigator.
/// ///
/// The returned value resolves when the push transition is complete. /// The returned value resolves when the push transition is complete.
///
/// The [didChangeNext] method is typically called immediately after this
/// method is called.
@protected @protected
TickerFuture didPush() => new TickerFuture.complete(); 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. /// Called after [install] when the route replaced another in the navigator.
///
/// The [didChangeNext] method is typically called immediately after this
/// method is called.
@protected @protected
@mustCallSuper @mustCallSuper
void didReplace(Route<dynamic> oldRoute) { } void didReplace(Route<dynamic> oldRoute) { }
...@@ -119,10 +142,24 @@ abstract class Route<T> { ...@@ -119,10 +142,24 @@ abstract class Route<T> {
return isFirst ? RoutePopDisposition.bubble : RoutePopDisposition.pop; 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<T> get popped => _popCompleter.future;
final Completer<T> _popCompleter = new Completer<T>();
/// A request was made to pop this route. If the route can handle it /// 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 /// internally (e.g. because it has its own stack of internal state) then
/// return false, otherwise return true. Returning false will prevent the /// return false, otherwise return true (by return the value of calling
/// default behavior of [NavigatorState.pop]. /// `super.didPop`). Returning false will prevent the default behavior of
/// [NavigatorState.pop].
/// ///
/// When this function returns true, the navigator removes this route from /// 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 /// the history but does not yet call [dispose]. Instead, it is the route's
...@@ -137,19 +174,28 @@ abstract class Route<T> { ...@@ -137,19 +174,28 @@ abstract class Route<T> {
return true; return true;
} }
/// Whether calling [didPop] would return false. /// The route was popped or is otherwise being removed somewhat gracefully.
bool get willHandlePopInternally => false; ///
/// 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. /// navigator.
@protected @protected
@mustCallSuper @mustCallSuper
void didPopNext(Route<dynamic> nextRoute) { } void didPopNext(Route<dynamic> nextRoute) { }
/// This route's next route has changed to the given new route. This is called /// 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 /// on a route whenever the next route changes for any reason, so long as it
/// cases when [didPopNext] would be called, so long as it is in the history. /// is in the history, including when a route is first added to a [Navigator]
/// `nextRoute` will be null if there's no next route. /// (e.g. by [Navigator.push]), except for cases when [didPopNext] would be
/// called. `nextRoute` will be null if there's no next route.
@protected @protected
@mustCallSuper @mustCallSuper
void didChangeNext(Route<dynamic> nextRoute) { } void didChangeNext(Route<dynamic> nextRoute) { }
...@@ -163,14 +209,37 @@ abstract class Route<T> { ...@@ -163,14 +209,37 @@ abstract class Route<T> {
@mustCallSuper @mustCallSuper
void didChangePrevious(Route<dynamic> previousRoute) { } void didChangePrevious(Route<dynamic> 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 @protected
@mustCallSuper @mustCallSuper
void didComplete(T result) { void changedInternalState() { }
_popCompleter.complete(result);
} /// 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. /// The route should remove its overlays and free any other resources.
/// ///
...@@ -205,7 +274,7 @@ abstract class Route<T> { ...@@ -205,7 +274,7 @@ abstract class Route<T> {
/// route), then [isCurrent] will also be true. If it is the first route (the /// route), then [isCurrent] will also be true. If it is the first route (the
/// bottom-most route), then [isFirst] will also be true. /// 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 /// 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]. /// widgets within the route to not be instantiated. See [ModalRoute.maintainState].
bool get isActive { bool get isActive {
...@@ -248,11 +317,6 @@ class RouteSettings { ...@@ -248,11 +317,6 @@ class RouteSettings {
String toString() => '"$name"'; String toString() => '"$name"';
} }
/// Creates a route for the given route settings.
///
/// Used by [Navigator.onGenerateRoute] and [Navigator.onUnknownRoute].
typedef Route<dynamic> RouteFactory(RouteSettings settings);
/// An interface for observing the behavior of a [Navigator]. /// An interface for observing the behavior of a [Navigator].
class NavigatorObserver { class NavigatorObserver {
/// The navigator that the observer is observing, if any. /// The navigator that the observer is observing, if any.
...@@ -260,14 +324,31 @@ class NavigatorObserver { ...@@ -260,14 +324,31 @@ class NavigatorObserver {
NavigatorState _navigator; NavigatorState _navigator;
/// The [Navigator] pushed `route`. /// The [Navigator] pushed `route`.
///
/// The route immediately below that one, and thus the previously active
/// route, is `previousRoute`.
void didPush(Route<dynamic> route, Route<dynamic> previousRoute) { } void didPush(Route<dynamic> route, Route<dynamic> previousRoute) { }
/// The [Navigator] popped `route`. /// The [Navigator] popped `route`.
///
/// The route immediately below that one, and thus the newly active
/// route, is `previousRoute`.
void didPop(Route<dynamic> route, Route<dynamic> previousRoute) { } void didPop(Route<dynamic> route, Route<dynamic> previousRoute) { }
/// The [Navigator] removed `route`. /// 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<dynamic> route, Route<dynamic> previousRoute) { } void didRemove(Route<dynamic> route, Route<dynamic> previousRoute) { }
/// The [Navigator] replaced `oldRoute` with `newRoute`.
void didReplace({ Route<dynamic> newRoute, Route<dynamic> oldRoute }) { }
/// The [Navigator]'s routes are being moved by a user gesture. /// 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 /// For example, this is called when an iOS back gesture starts, and is used
...@@ -280,9 +361,6 @@ class NavigatorObserver { ...@@ -280,9 +361,6 @@ class NavigatorObserver {
void didStopUserGesture() { } void didStopUserGesture() { }
} }
/// Signature for the [Navigator.popUntil] predicate argument.
typedef bool RoutePredicate(Route<dynamic> route);
/// A widget that manages a set of child widgets with a stack discipline. /// 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 /// Many apps have a navigator near the top of their widget hierarchy in order
...@@ -306,9 +384,9 @@ typedef bool RoutePredicate(Route<dynamic> route); ...@@ -306,9 +384,9 @@ typedef bool RoutePredicate(Route<dynamic> route);
/// the navigator created by a [WidgetsApp] or a [MaterialApp] widget. You /// the navigator created by a [WidgetsApp] or a [MaterialApp] widget. You
/// can refer to that navigator with [Navigator.of]. /// can refer to that navigator with [Navigator.of].
/// ///
/// A MaterialApp is the simplest way to set things up. The MaterialApp's /// 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 /// home becomes the route at the bottom of the [Navigator]'s stack. It is what
/// what you see when the app is launched. /// you see when the app is launched.
/// ///
/// ```dart /// ```dart
/// void main() { /// void main() {
...@@ -321,7 +399,7 @@ typedef bool RoutePredicate(Route<dynamic> route); ...@@ -321,7 +399,7 @@ typedef bool RoutePredicate(Route<dynamic> route);
/// want to appear on the screen. For example: /// want to appear on the screen. For example:
/// ///
/// ```dart /// ```dart
/// Navigator.of(context).push(new MaterialPageRoute<Null>( /// Navigator.push(context, new MaterialPageRoute<void>(
/// builder: (BuildContext context) { /// builder: (BuildContext context) {
/// return new Scaffold( /// return new Scaffold(
/// appBar: new AppBar(title: new Text('My Page')), /// appBar: new AppBar(title: new Text('My Page')),
...@@ -329,7 +407,7 @@ typedef bool RoutePredicate(Route<dynamic> route); ...@@ -329,7 +407,7 @@ typedef bool RoutePredicate(Route<dynamic> route);
/// child: new FlatButton( /// child: new FlatButton(
/// child: new Text('POP'), /// child: new Text('POP'),
/// onPressed: () { /// onPressed: () {
/// Navigator.of(context).pop(); /// Navigator.pop(context);
/// }, /// },
/// ), /// ),
/// ), /// ),
...@@ -346,7 +424,7 @@ typedef bool RoutePredicate(Route<dynamic> route); ...@@ -346,7 +424,7 @@ typedef bool RoutePredicate(Route<dynamic> route);
/// page, with the Navigator's pop method: /// page, with the Navigator's pop method:
/// ///
/// ```dart /// ```dart
/// Navigator.of(context).pop(); /// Navigator.pop(context);
/// ``` /// ```
/// ///
/// It usually isn't necessary to provide a widget that pops the Navigator /// It usually isn't necessary to provide a widget that pops the Navigator
...@@ -383,7 +461,7 @@ typedef bool RoutePredicate(Route<dynamic> route); ...@@ -383,7 +461,7 @@ typedef bool RoutePredicate(Route<dynamic> route);
/// To show a route by name: /// To show a route by name:
/// ///
/// ```dart /// ```dart
/// Navigator.of(context).pushNamed('/b'); /// Navigator.pushNamed(context, '/b');
/// ``` /// ```
/// ///
/// ### Routes can return a value /// ### Routes can return a value
...@@ -391,32 +469,35 @@ typedef bool RoutePredicate(Route<dynamic> route); ...@@ -391,32 +469,35 @@ typedef bool RoutePredicate(Route<dynamic> route);
/// When a route is pushed to ask the user for a value, the value can be /// When a route is pushed to ask the user for a value, the value can be
/// returned via the [pop] method's result parameter. /// returned via the [pop] method's result parameter.
/// ///
/// Methods that push a route return a Future. The Future resolves when /// Methods that push a route return a [Future]. The Future resolves when the
/// the route is popped and the Future's value is the [pop] method's result /// route is popped and the [Future]'s value is the [pop] method's `result`
/// parameter. /// parameter.
/// ///
/// For example if we wanted to ask the user to press 'OK' to confirm an /// For example if we wanted to ask the user to press 'OK' to confirm an
/// operation we could `await` the result of [Navigator.push]: /// operation we could `await` the result of [Navigator.push]:
/// ///
/// ``` /// ```dart
/// bool value = await Navigator.of(context).push(new MaterialPageRoute<bool>( /// bool value = await Navigator.push(context, new MaterialPageRoute<bool>(
/// builder: (BuildContext context) { /// builder: (BuildContext context) {
/// return new Center( /// return new Center(
/// child: new GestureDetector( /// child: new GestureDetector(
/// child: new Text('OK'), /// 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 /// 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, /// out of the route, for example by pressing the Scaffold's back button,
/// the value will be null. /// the value will be null.
/// ///
/// When a route is used to return a value, the route's type parameter /// When a route is used to return a value, the route's type parameter must
/// must match the type of [pop]'s result. That's why we've used /// match the type of [pop]'s result. That's why we've used
/// `MaterialPageRoute<bool>` instead of `MaterialPageRoute<Null>`. /// `MaterialPageRoute<bool>` instead of `MaterialPageRoute<void>` or just
/// `MaterialPageRoute`. (If you prefer to not specify the types, though, that's
/// fine too.)
/// ///
/// ### Popup routes /// ### Popup routes
/// ///
...@@ -442,18 +523,18 @@ typedef bool RoutePredicate(Route<dynamic> route); ...@@ -442,18 +523,18 @@ typedef bool RoutePredicate(Route<dynamic> route);
/// transition employed to show the route, the color and behavior of the route's /// transition employed to show the route, the color and behavior of the route's
/// modal barrier, and other aspects of the route. /// 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 /// 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 /// 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. /// screen because it specifies `opaque: false`, just as a popup route does.
/// ///
/// ```dart /// ```dart
/// Navigator.of(context).push(new PageRouteBuilder( /// Navigator.push(context, new PageRouteBuilder(
/// opaque: false, /// opaque: false,
/// pageBuilder: (BuildContext context, _, __) { /// pageBuilder: (BuildContext context, _, __) {
/// return new Center(child: new Text('My PageRoute')); /// return new Center(child: new Text('My PageRoute'));
/// }, /// },
/// transitionsBuilder: (_, Animation<double> animation, __, Widget child) { /// transitionsBuilder: (___, Animation<double> animation, ____, Widget child) {
/// return new FadeTransition( /// return new FadeTransition(
/// opacity: animation, /// opacity: animation,
/// child: new RotationTransition( /// child: new RotationTransition(
...@@ -522,221 +603,596 @@ class Navigator extends StatefulWidget { ...@@ -522,221 +603,596 @@ class Navigator extends StatefulWidget {
/// application was started with. /// application was started with.
static const String defaultRouteName = '/'; 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] /// The route name will be passed to that navigator's [onGenerateRoute]
/// callback. The returned route will be pushed into the navigator. /// callback. The returned route will be pushed into the navigator.
/// ///
/// 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] /// Returns a [Future] that completes to the `result` value passed to [pop]
/// when the pushed route is popped off the navigator. /// 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: /// Typical usage is as follows:
/// ///
/// ```dart /// ```dart
/// Navigator.pushNamed(context, '/nyc/1776'); /// void _didPushButton() {
/// Navigator.pushNamed(context, '/nyc/1776');
/// }
/// ``` /// ```
static Future<dynamic> pushNamed(BuildContext context, String routeName) { @optionalTypeArgs
return Navigator.of(context).pushNamed(routeName); static Future<T> pushNamed<T extends Object>(BuildContext context, String routeName) {
return Navigator.of(context).pushNamed<T>(routeName);
} }
/// Adds the given route to the history of the navigator that most tightly /// Replace the current route of the navigator that most tightly encloses the
/// encloses the given context, and transitions to it. /// given context 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 /// {@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 /// [Route.didPush] and [Route.didChangeNext]). If the [Navigator] has any
/// [Navigator.observers], they will be notified as well (see /// [Navigator.observers], they will be notified as well (see
/// [NavigatorObserver.didPush]). /// [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 /// Ongoing gestures within the current route are canceled when a new route is
/// pushed. /// pushed.
/// ///
/// Returns a [Future] that completes to the `result` value passed to [pop] /// Returns a [Future] that completes to the `result` value passed to [pop]
/// when the pushed route is popped off the navigator. /// 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 @optionalTypeArgs
static Future<T> push<T>(BuildContext context, Route<T> route) { static Future<T> pushReplacementNamed<T extends Object, TO extends Object>(BuildContext context, String routeName, { TO result }) {
return Navigator.of(context).push(route); return Navigator.of(context).pushReplacementNamed<T, TO>(routeName, result: result);
} }
/// Returns the value of the current route's [Route.willPop] method. This /// Pop the current route off the navigator that most tightly encloses the
/// method is typically called before a user-initiated [pop]. For example on /// given context and push a named route in its place.
/// Android it's called by the binding for the system's back button.
/// ///
/// See also: /// {@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`).
/// ///
/// * [Form], which provides an `onWillPop` callback that enables the form /// The route name will be passed to the navigator's [onGenerateRoute]
/// to veto a [pop] initiated by the app's back button. /// callback. The returned route will be pushed into the navigator.
/// * [ModalRoute], which provides a `scopedWillPopCallback` that can be used
/// to define the route's `willPop` method.
static Future<bool> maybePop(BuildContext context, [ dynamic result ]) {
return Navigator.of(context).maybePop(result);
}
/// Pop a route off the navigator that most tightly encloses the given context.
/// ///
/// Tries to removes the current route, calling its didPop() method. If that /// The new route, the old route, and the route below the old route (if any)
/// method returns false, then nothing else happens. Otherwise, the observer /// are all notified (see [Route.didPop], [Route.didComplete],
/// (if any) is notified using its didPop() method, and the previous route is /// [Route.didPopNext], [Route.didPush], and [Route.didChangeNext]). If the
/// notified using [Route.didChangeNext]. /// [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]).
/// ///
/// If non-null, `result` will be used as the result of the route. Routes /// Ongoing gestures within the current route are canceled when a new route is
/// such as dialogs or popup menus typically use this mechanism to return the /// pushed.
/// 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 /// Returns a [Future] that completes to the `result` value passed to [pop]
/// of the current route. (In practice, this is usually "dynamic".) /// 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}
/// ///
/// Returns true if a route was popped; returns false if there are no further /// ## Sample code
/// previous routes.
/// ///
/// Typical usage is as follows: /// Typical usage is as follows:
/// ///
/// ```dart /// ```dart
/// Navigator.pop(context); /// void _selectNewYork() {
/// Navigator.popAndPushNamed(context, '/nyc/1776');
/// }
/// ``` /// ```
static bool pop(BuildContext context, [ dynamic result ]) { @optionalTypeArgs
return Navigator.of(context).pop(result); static Future<T> popAndPushNamed<T extends Object, TO extends Object>(BuildContext context, String routeName, { TO result }) {
return Navigator.of(context).popAndPushNamed<T, TO>(routeName, result: result);
} }
/// Calls [pop] repeatedly until the predicate returns true. /// 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 /// The predicate may be applied to the same route more than once if
/// [Route.willHandlePopInternally] is true. /// [Route.willHandlePopInternally] is true.
/// ///
/// To pop until a route with a certain name, use the [RoutePredicate] /// To remove routes until a route with a certain name, use the
/// returned from [ModalRoute.withName]. /// [RoutePredicate] returned from [ModalRoute.withName].
///
/// To remove all the routes below the pushed route, use a [RoutePredicate]
/// that always returns false (e.g. `(Route<dynamic> 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: /// Typical usage is as follows:
/// ///
/// ```dart /// ```dart
/// Navigator.popUntil(context, ModalRoute.withName('/login')); /// void _resetToCalendar() {
/// Navigator.pushNamedAndRemoveUntil(context, '/calendar', ModalRoute.withName('/'));
/// }
/// ``` /// ```
static void popUntil(BuildContext context, RoutePredicate predicate) { @optionalTypeArgs
Navigator.of(context).popUntil(predicate); static Future<T> pushNamedAndRemoveUntil<T extends Object>(BuildContext context, String newRouteName, RoutePredicate predicate) {
} return Navigator.of(context).pushNamedAndRemoveUntil<T>(newRouteName, predicate);
/// Whether the navigator that most tightly encloses the given context can be
/// popped.
///
/// 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<NavigatorState>());
return navigator != null && navigator.canPop();
} }
/// Executes a simple transaction that both pops the current route off and /// Push the given route onto the navigator that most tightly encloses the
/// pushes a named route into the navigator that most tightly encloses the
/// given context. /// given context.
/// ///
/// If non-null, `result` will be used as the result of the route that is /// {@template flutter.widgets.navigator.push}
/// popped. Routes such as dialogs or popup menus typically use this mechanism /// The new route and the previous route (if any) are notified (see
/// to return the value selected by the user to the widget that created their /// [Route.didPush] and [Route.didChangeNext]). If the [Navigator] has any
/// route. The type of `result`, if provided, must match the type argument of /// [Navigator.observers], they will be notified as well (see
/// the class of the current route. (In practice, this is usually "dynamic".) /// [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] /// Returns a [Future] that completes to the `result` value passed to [pop]
/// when the pushed route is popped off the navigator. /// 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: /// Typical usage is as follows:
/// ///
/// ```dart /// ```dart
/// Navigator.popAndPushNamed(context, '/nyc/1776'); /// void _openMyPage() {
/// Navigator.push(context, new MaterialPageRoute(builder: (BuildContext context) => new MyPage()));
/// }
/// ``` /// ```
static Future<dynamic> popAndPushNamed(BuildContext context, String routeName, { dynamic result }) { @optionalTypeArgs
final NavigatorState navigator = Navigator.of(context); static Future<T> push<T extends Object>(BuildContext context, Route<T> route) {
navigator.pop(result); return Navigator.of(context).push(route);
return navigator.pushNamed(routeName);
} }
/// Replace the current route by pushing the route named [routeName] and then /// Replace the current route of the navigator that most tightly encloses the
/// disposing the previous route. /// given context by pushing the given route and then disposing the previous
/// route once the new route has finished animating in.
/// ///
/// The route name will be passed to the navigator's [onGenerateRoute] /// {@template flutter.widgets.navigator.pushReplacement}
/// callback. The returned route will be pushed into the navigator. /// 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] /// Returns a [Future] that completes to the `result` value passed to [pop]
/// when the pushed route is popped off the navigator. /// 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: /// Typical usage is as follows:
/// ///
/// ```dart /// ```dart
/// Navigator.of(context).pushReplacementNamed('/jouett/1781'); /// void _completeLogin() {
/// Navigator.pushReplacement(context, new MaterialPageRoute(builder: (BuildContext context) => new MyHomePage()));
/// }
/// ``` /// ```
static Future<dynamic> pushReplacementNamed(BuildContext context, String routeName, { dynamic result }) { @optionalTypeArgs
return Navigator.of(context).pushReplacementNamed(routeName, result: result); static Future<T> pushReplacement<T extends Object, TO extends Object>(BuildContext context, Route<T> newRoute, { TO result }) {
return Navigator.of(context).pushReplacement<T, TO>(newRoute, result: result);
} }
/// Replace the current route by pushing [route] and then disposing the /// Push the given route onto the navigator that most tightly encloses the
/// current route. /// given context, and then remove all the previous routes until the
/// `predicate` returns true.
/// ///
/// The new route and the route below the new route (if any) are notified /// {@template flutter.widgets.navigator.pushAndRemoveUntil}
/// (see [Route.didPush] and [Route.didChangeNext]). The navigator observer /// The predicate may be applied to the same route more than once if
/// is not notified about the old route. The old route is disposed (see /// [Route.willHandlePopInternally] is true.
/// [Route.dispose]). ///
/// 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<dynamic> 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.
/// ///
/// If a [result] is provided, it will be the return value of the old route, /// Ongoing gestures within the current route are canceled when a new route is
/// as if the old route had been popped. /// pushed.
/// ///
/// Returns a [Future] that completes to the `result` value passed to [pop] /// Returns a [Future] that completes to the `result` value passed to [pop]
/// when the pushed route is popped off the navigator. /// when the pushed route is popped off the navigator.
static Future<dynamic> pushReplacement(BuildContext context, Route<dynamic> route, { dynamic result }) { ///
return Navigator.of(context).pushReplacement(route, result: result); /// 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<T> pushAndRemoveUntil<T extends Object>(BuildContext context, Route<T> newRoute, RoutePredicate predicate) {
return Navigator.of(context).pushAndRemoveUntil<T>(newRoute, predicate);
} }
/// Immediately remove `route` and [Route.dispose] it. /// Replaces a route on the navigator that most tightly encloses the given
/// context with a new route.
/// ///
/// The route's animation does not run and the future returned from pushing /// {@template flutter.widgets.navigator.replace}
/// the route will not complete. Ongoing input gestures are cancelled. If /// The old route must not be current visible, as this method skips the
/// the [Navigator] has any [Navigator.observers], they will be notified with /// animations and therefore the removal would be jarring if it was visible.
/// [NavigatorObserver.didRemove]. /// 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 routes before and after the removed route, if any, are notified with /// The removed route is removed without being completed, so this method does
/// [Route.didChangeNext] and [Route.didChangePrevious]. /// not take a return value argument.
/// ///
/// This method is used to dismiss dropdown menus that are up when the screen's /// The new route, the route below the new route (if any), and the route above
/// orientation changes. /// the new route, are all notified (see [Route.didReplace],
static void removeRoute(BuildContext context, Route<dynamic> route) { /// [Route.didChangeNext], and [Route.didChangePrevious]). If the [Navigator]
return Navigator.of(context).removeRoute(route); /// 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<T extends Object>(BuildContext context, { @required Route<dynamic> oldRoute, @required Route<T> newRoute }) {
return Navigator.of(context).replace<T>(oldRoute: oldRoute, newRoute: newRoute);
} }
/// The state from the closest instance of this class that encloses the given context. /// 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`.
/// ///
/// Typical usage is as follows: /// {@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.
/// ///
/// ```dart /// The removed route is removed without being completed, so this method does
/// Navigator.of(context) /// not take a return value argument.
/// ..pop()
/// ..pop()
/// ..pushNamed('/settings');
/// ```
/// ///
/// If `rootNavigator` is set to true, the state from the furthest instance of /// The new route, the route below the new route (if any), and the route above
/// this class is given instead. Useful for pushing contents above all subsequent /// the new route, are all notified (see [Route.didReplace],
/// instances of [Navigator]. /// [Route.didChangeNext], and [Route.didChangePrevious]). If the [Navigator]
static NavigatorState of( /// has any [Navigator.observers], they will be notified as well (see
BuildContext context, { /// [NavigatorObservers.didReplace]). The removed route is disposed without
bool rootNavigator: false /// being notified. The future that had been returned from pushing that routes
}) { /// will not complete.
final NavigatorState navigator = rootNavigator ///
? context.rootAncestorStateOfType(const TypeMatcher<NavigatorState>()) /// The `T` type argument is the type of the return value of the new route.
: context.ancestorStateOfType(const TypeMatcher<NavigatorState>()); /// {@endtemplate}
assert(() { ///
if (navigator == null) { /// See also:
throw new FlutterError( ///
'Navigator operation requested with a context that does not include a Navigator.\n' /// * [replace], which is the same but identifies the route to be removed
'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.' /// directly.
); @optionalTypeArgs
} static void replaceRouteBelow<T extends Object>(BuildContext context, { @required Route<dynamic> anchorRoute, Route<T> newRoute }) {
return true; return Navigator.of(context).replaceRouteBelow<T>(anchorRoute: anchorRoute, newRoute: newRoute);
}());
return navigator;
} }
@override /// Whether the navigator that most tightly encloses the given context can be
NavigatorState createState() => new NavigatorState(); /// popped.
} ///
/// {@template flutter.widgets.navigator.canPop}
/// The state for a [Navigator] widget. /// 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:
///
/// * [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
static Future<bool> maybePop<T extends Object>(BuildContext context, [ T result ]) {
return Navigator.of(context).maybePop<T>(result);
}
/// Pop the top-most route off the navigator that most tightly encloses the
/// given context.
///
/// {@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 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`).
///
/// 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]).
///
/// 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
/// void _close() {
/// Navigator.pop(context);
/// }
/// ```
///
/// A dialog box might be closed with a result:
///
/// ```dart
/// void _accept() {
/// Navigator.pop(context, true); // dialog returns true
/// }
/// ```
@optionalTypeArgs
static bool pop<T extends Object>(BuildContext context, [ T result ]) {
return Navigator.of(context).pop<T>(result);
}
/// 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
/// void _logout() {
/// Navigator.popUntil(context, ModalRoute.withName('/login'));
/// }
/// ```
static void popUntil(BuildContext context, RoutePredicate predicate) {
Navigator.of(context).popUntil(predicate);
}
/// Immediately remove `route` from the navigator that most tightly encloses
/// the given context, and [Route.dispose] it.
///
/// {@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.
///
/// 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 `route` must be in the history; this method will throw an
/// exception if it is not.
///
/// Ongoing gestures within the current route are canceled.
/// {@endtemplate}
///
/// 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<dynamic> 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<dynamic> 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:
///
/// ```dart
/// Navigator.of(context)
/// ..pop()
/// ..pop()
/// ..pushNamed('/settings');
/// ```
///
/// If `rootNavigator` is set to true, the state from the furthest instance of
/// this class is given instead. Useful for pushing contents above all subsequent
/// instances of [Navigator].
static NavigatorState of(
BuildContext context, {
bool rootNavigator: false,
bool nullOk: false,
}) {
final NavigatorState navigator = rootNavigator
? context.rootAncestorStateOfType(const TypeMatcher<NavigatorState>())
: context.ancestorStateOfType(const TypeMatcher<NavigatorState>());
assert(() {
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.'
);
}
return true;
}());
return navigator;
}
@override
NavigatorState createState() => new NavigatorState();
}
/// The state for a [Navigator] widget.
class NavigatorState extends State<Navigator> with TickerProviderStateMixin { class NavigatorState extends State<Navigator> with TickerProviderStateMixin {
final GlobalKey<OverlayState> _overlayKey = new GlobalKey<OverlayState>(); final GlobalKey<OverlayState> _overlayKey = new GlobalKey<OverlayState>();
final List<Route<dynamic>> _history = <Route<dynamic>>[]; final List<Route<dynamic>> _history = <Route<dynamic>>[];
...@@ -762,7 +1218,7 @@ class NavigatorState extends State<Navigator> with TickerProviderStateMixin { ...@@ -762,7 +1218,7 @@ class NavigatorState extends State<Navigator> with TickerProviderStateMixin {
Navigator.defaultRouteName, Navigator.defaultRouteName,
]; ];
final List<Route<dynamic>> plannedInitialRoutes = <Route<dynamic>>[ final List<Route<dynamic>> plannedInitialRoutes = <Route<dynamic>>[
_routeNamed(Navigator.defaultRouteName, allowNull: true), _routeNamed<dynamic>(Navigator.defaultRouteName, allowNull: true),
]; ];
final List<String> routeParts = initialRouteName.split('/'); final List<String> routeParts = initialRouteName.split('/');
if (initialRouteName.isNotEmpty) { if (initialRouteName.isNotEmpty) {
...@@ -770,7 +1226,7 @@ class NavigatorState extends State<Navigator> with TickerProviderStateMixin { ...@@ -770,7 +1226,7 @@ class NavigatorState extends State<Navigator> with TickerProviderStateMixin {
for (String part in routeParts) { for (String part in routeParts) {
routeName += '/$part'; routeName += '/$part';
plannedInitialRouteNames.add(routeName); plannedInitialRouteNames.add(routeName);
plannedInitialRoutes.add(_routeNamed(routeName, allowNull: true)); plannedInitialRoutes.add(_routeNamed<dynamic>(routeName, allowNull: true));
} }
} }
if (plannedInitialRoutes.contains(null)) { if (plannedInitialRoutes.contains(null)) {
...@@ -790,15 +1246,15 @@ class NavigatorState extends State<Navigator> with TickerProviderStateMixin { ...@@ -790,15 +1246,15 @@ class NavigatorState extends State<Navigator> with TickerProviderStateMixin {
); );
return true; return true;
}()); }());
push(_routeNamed(Navigator.defaultRouteName)); push(_routeNamed<Object>(Navigator.defaultRouteName));
} else { } else {
plannedInitialRoutes.forEach(push); plannedInitialRoutes.forEach(push);
} }
} else { } else {
Route<dynamic> route; Route<Object> route;
if (initialRouteName != Navigator.defaultRouteName) if (initialRouteName != Navigator.defaultRouteName)
route = _routeNamed(initialRouteName, allowNull: true); route = _routeNamed<Object>(initialRouteName, allowNull: true);
route ??= _routeNamed(Navigator.defaultRouteName); route ??= _routeNamed<Object>(Navigator.defaultRouteName);
push(route); push(route);
} }
for (Route<dynamic> route in _history) for (Route<dynamic> route in _history)
...@@ -816,6 +1272,8 @@ class NavigatorState extends State<Navigator> with TickerProviderStateMixin { ...@@ -816,6 +1272,8 @@ class NavigatorState extends State<Navigator> with TickerProviderStateMixin {
observer._navigator = this; observer._navigator = this;
} }
} }
for (Route<dynamic> route in _history)
route.changedExternalState();
} }
@override @override
...@@ -847,14 +1305,14 @@ class NavigatorState extends State<Navigator> with TickerProviderStateMixin { ...@@ -847,14 +1305,14 @@ class NavigatorState extends State<Navigator> with TickerProviderStateMixin {
bool _debugLocked = false; // used to prevent re-entrant calls to push, pop, and friends bool _debugLocked = false; // used to prevent re-entrant calls to push, pop, and friends
Route<dynamic> _routeNamed(String name, { bool allowNull: false }) { Route<T> _routeNamed<T>(String name, { bool allowNull: false }) {
assert(!_debugLocked); assert(!_debugLocked);
assert(name != null); assert(name != null);
final RouteSettings settings = new RouteSettings( final RouteSettings settings = new RouteSettings(
name: name, name: name,
isInitialRoute: _history.isEmpty, isInitialRoute: _history.isEmpty,
); );
Route<dynamic> route = widget.onGenerateRoute(settings); Route<T> route = widget.onGenerateRoute(settings);
if (route == null && !allowNull) { if (route == null && !allowNull) {
assert(() { assert(() {
if (widget.onUnknownRoute == null) { if (widget.onUnknownRoute == null) {
...@@ -887,106 +1345,132 @@ class NavigatorState extends State<Navigator> with TickerProviderStateMixin { ...@@ -887,106 +1345,132 @@ class NavigatorState extends State<Navigator> with TickerProviderStateMixin {
/// Push a named route onto the navigator. /// Push a named route onto the navigator.
/// ///
/// The route name will be passed to [Navigator.onGenerateRoute]. The returned /// {@macro flutter.widgets.navigator.pushNamed}
/// route will be pushed into the navigator.
/// ///
/// Returns a [Future] that completes to the `result` value passed to [pop] /// ## Sample code
/// when the pushed route is popped off the navigator.
/// ///
/// Typical usage is as follows: /// Typical usage is as follows:
/// ///
/// ```dart /// ```dart
/// Navigator.of(context).pushNamed('/nyc/1776'); /// void _aaronBurrSir() {
/// navigator.pushNamed('/nyc/1776');
/// }
/// ``` /// ```
Future<dynamic> pushNamed(String name) { @optionalTypeArgs
return push(_routeNamed(name)); Future<T> pushNamed<T extends Object>(String routeName) {
return push<T>(_routeNamed<T>(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 /// {@macro flutter.widgets.navigator.pushReplacementNamed}
/// [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 /// ## Sample code
/// pushed.
/// ///
/// Returns a [Future] that completes to the `result` value passed to [pop] /// Typical usage is as follows:
/// when the pushed route is popped off the navigator. ///
Future<Object> push(Route<Object> route) { /// ```dart
/// void _startBike() {
/// navigator.pushReplacementNamed('/jouett/1781');
/// }
/// ```
@optionalTypeArgs
Future<T> pushReplacementNamed<T extends Object, TO extends Object>(String routeName, { TO result }) {
return pushReplacement<T, TO>(_routeNamed<T>(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<T> popAndPushNamed<T extends Object, TO extends Object>(String routeName, { TO result }) {
pop<TO>(result);
return pushNamed<T>(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<T> pushNamedAndRemoveUntil<T extends Object>(String newRouteName, RoutePredicate predicate) {
return pushAndRemoveUntil<T>(_routeNamed<T>(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<T> push<T extends Object>(Route<T> route) {
assert(!_debugLocked); assert(!_debugLocked);
assert(() { _debugLocked = true; return true; }()); assert(() { _debugLocked = true; return true; }());
assert(route != null); assert(route != null);
assert(route._navigator == null); assert(route._navigator == null);
setState(() { final Route<dynamic> oldRoute = _history.isNotEmpty ? _history.last : null;
final Route<dynamic> oldRoute = _history.isNotEmpty ? _history.last : null; route._navigator = this;
route._navigator = this; route.install(_currentOverlayEntry);
route.install(_currentOverlayEntry); _history.add(route);
_history.add(route); route.didPush();
route.didPush(); route.didChangeNext(null);
route.didChangeNext(null); if (oldRoute != null)
if (oldRoute != null) oldRoute.didChangeNext(route);
oldRoute.didChangeNext(route); for (NavigatorObserver observer in widget.observers)
for (NavigatorObserver observer in widget.observers) observer.didPush(route, oldRoute);
observer.didPush(route, oldRoute);
});
assert(() { _debugLocked = false; return true; }()); assert(() { _debugLocked = false; return true; }());
_cancelActivePointers(); _cancelActivePointers();
return route.popped; 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 /// {@macro flutter.widgets.navigator.pushReplacement}
/// (see [Route.didReplace] and [Route.didChangeNext]). The navigator observer
/// is not notified. The old route is disposed (see [Route.dispose]).
/// ///
/// This can be useful in combination with [removeRouteBelow] when building a /// ## Sample code
/// non-linear user experience.
void replace({ @required Route<dynamic> oldRoute, @required Route<dynamic> 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.
/// ///
/// The new route and the route below the new route (if any) are notified /// Typical usage is as follows:
/// (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).
/// ///
/// If a [result] is provided, it will be the return value of the old route, /// ```dart
/// as if the old route had been popped. /// void _doOpenPage() {
Future<dynamic> pushReplacement(Route<dynamic> newRoute, { dynamic result }) { /// navigator.pushReplacement(new MaterialPageRoute(builder: (BuildContext context) => new MyHomePage()));
/// }
/// ```
@optionalTypeArgs
Future<T> pushReplacement<T extends Object, TO extends Object>(Route<T> newRoute, { TO result }) {
assert(!_debugLocked); assert(!_debugLocked);
assert(() { _debugLocked = true; return true; }()); assert(() { _debugLocked = true; return true; }());
final Route<dynamic> oldRoute = _history.last; final Route<dynamic> oldRoute = _history.last;
...@@ -994,99 +1478,50 @@ class NavigatorState extends State<Navigator> with TickerProviderStateMixin { ...@@ -994,99 +1478,50 @@ class NavigatorState extends State<Navigator> with TickerProviderStateMixin {
assert(oldRoute.overlayEntries.isNotEmpty); assert(oldRoute.overlayEntries.isNotEmpty);
assert(newRoute._navigator == null); assert(newRoute._navigator == null);
assert(newRoute.overlayEntries.isEmpty); assert(newRoute.overlayEntries.isEmpty);
setState(() { final int index = _history.length - 1;
final int index = _history.length - 1; assert(index >= 0);
assert(index >= 0); assert(_history.indexOf(oldRoute) == index);
assert(_history.indexOf(oldRoute) == index); newRoute._navigator = this;
newRoute._navigator = this; newRoute.install(_currentOverlayEntry);
newRoute.install(_currentOverlayEntry); _history[index] = newRoute;
_history[index] = newRoute; newRoute.didPush().whenCompleteOrCancel(() {
newRoute.didPush().whenCompleteOrCancel(() { // The old route's exit is not animated. We're assuming that the
// The old route's exit is not animated. We're assuming that the // new route completely obscures the old one.
// new route completely obscures the old one. if (mounted) {
if (mounted) { oldRoute
oldRoute ..didComplete(result ?? oldRoute.currentResult)
..didComplete(result ?? oldRoute.currentResult) ..dispose();
..dispose(); }
}
});
newRoute.didChangeNext(null);
if (index > 0)
_history[index - 1].didChangeNext(newRoute);
for (NavigatorObserver observer in widget.observers)
observer.didPush(newRoute, oldRoute);
}); });
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; }()); assert(() { _debugLocked = false; return true; }());
_cancelActivePointers(); _cancelActivePointers();
return newRoute.popped; 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 /// {@macro flutter.widgets.navigator.pushAndRemoveUntil}
/// route will be pushed into the navigator.
/// ///
/// Returns a [Future] that completes to the `result` value passed to [pop] /// ## Sample code
/// when the pushed route is popped off the navigator.
Future<dynamic> pushReplacementNamed(String name, { dynamic result }) {
return pushReplacement(_routeNamed(name), result: result);
}
/// Replaces a route that is not currently visible with a new route.
///
/// The route to be removed is the one below the given `anchorRoute`. That
/// route must not be the first route in the history.
/// ///
/// In every other way, this acts the same as [replace]. /// Typical usage is as follows:
void replaceRouteBelow({ @required Route<dynamic> anchorRoute, Route<dynamic> 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<dynamic> anchorRoute) {
assert(!_debugLocked);
assert(() { _debugLocked = true; return true; }());
assert(anchorRoute._navigator == this);
final int index = _history.indexOf(anchorRoute) - 1;
assert(index >= 0);
final Route<dynamic> targetRoute = _history[index];
assert(targetRoute._navigator == this);
assert(targetRoute.overlayEntries.isEmpty || !overlay.debugIsVisible(targetRoute.overlayEntries.last));
setState(() {
_history.removeAt(index);
final Route<dynamic> nextRoute = index < _history.length ? _history[index] : null;
final Route<dynamic> 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] /// ```dart
/// that always returns false. /// void _resetAndOpenPage() {
Future<dynamic> pushAndRemoveUntil(Route<dynamic> newRoute, RoutePredicate predicate) { /// navigator.pushAndRemoveUntil(
/// new MaterialPageRoute(builder: (BuildContext context) => new MyHomePage()),
/// ModalRoute.withName('/'),
/// );
/// }
/// ```
@optionalTypeArgs
Future<T> pushAndRemoveUntil<T extends Object>(Route<T> newRoute, RoutePredicate predicate) {
assert(!_debugLocked); assert(!_debugLocked);
assert(() { _debugLocked = true; return true; }()); assert(() { _debugLocked = true; return true; }());
final List<Route<dynamic>> removedRoutes = <Route<dynamic>>[]; final List<Route<dynamic>> removedRoutes = <Route<dynamic>>[];
...@@ -1098,58 +1533,114 @@ class NavigatorState extends State<Navigator> with TickerProviderStateMixin { ...@@ -1098,58 +1533,114 @@ class NavigatorState extends State<Navigator> with TickerProviderStateMixin {
} }
assert(newRoute._navigator == null); assert(newRoute._navigator == null);
assert(newRoute.overlayEntries.isEmpty); assert(newRoute.overlayEntries.isEmpty);
setState(() { final Route<dynamic> oldRoute = _history.isNotEmpty ? _history.last : null;
final Route<dynamic> oldRoute = _history.isNotEmpty ? _history.last : null; newRoute._navigator = this;
newRoute._navigator = this; newRoute.install(_currentOverlayEntry);
newRoute.install(_currentOverlayEntry); _history.add(newRoute);
_history.add(newRoute); newRoute.didPush().whenCompleteOrCancel(() {
newRoute.didPush().whenCompleteOrCancel(() { if (mounted) {
if (mounted) { for (Route<dynamic> route in removedRoutes)
for (Route<dynamic> route in removedRoutes) route.dispose();
route.dispose(); }
}
});
newRoute.didChangeNext(null);
if (oldRoute != null)
oldRoute.didChangeNext(newRoute);
for (NavigatorObserver observer in widget.observers)
observer.didPush(newRoute, oldRoute);
}); });
newRoute.didChangeNext(null);
if (oldRoute != null)
oldRoute.didChangeNext(newRoute);
for (NavigatorObserver observer in widget.observers) {
observer.didPush(newRoute, oldRoute);
for (Route<dynamic> removedRoute in removedRoutes)
observer.didRemove(removedRoute, oldRoute);
}
assert(() { _debugLocked = false; return true; }()); assert(() { _debugLocked = false; return true; }());
_cancelActivePointers(); _cancelActivePointers();
return newRoute.popped; return newRoute.popped;
} }
/// Push the route with the given name and then remove all the previous routes /// Replaces a route on the navigator with a new route.
/// until the `predicate` returns true.
/// ///
/// The predicate may be applied to the same route more than once if /// {@macro flutter.widgets.navigator.replace}
/// [Route.willHandlePopInternally] is true.
/// ///
/// To remove routes until a route with a certain name, use the /// See also:
/// [RoutePredicate] returned from [ModalRoute.withName].
/// ///
/// To remove all the routes before the pushed route, use a [RoutePredicate] /// * [replaceRouteBelow], which is the same but identifies the route to be
/// that always returns false. /// removed by reference to the route above it, rather than directly.
Future<dynamic> pushNamedAndRemoveUntil(String routeName, RoutePredicate predicate) { @optionalTypeArgs
return pushAndRemoveUntil(_routeNamed(routeName), predicate); void replace<T extends Object>({ @required Route<dynamic> oldRoute, @required Route<T> 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; }());
} }
/// Tries to pop the current route, first giving the active route the chance /// Replaces a route on the navigator with a new route. The route to be
/// to veto the operation using [Route.willPop]. This method is typically /// replaced is the one below the given `anchorRoute`.
/// 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.replaceRouteBelow}
/// ///
/// See also: /// See also:
/// ///
/// * [Form], which provides a [Form.onWillPop] callback that enables the form /// * [replace], which is the same but identifies the route to be removed
/// to veto a [maybePop] initiated by the app's back button. /// directly.
/// * [WillPopScope], a widget that hooks into the route's [Route.willPop] @optionalTypeArgs
/// mechanism. void replaceRouteBelow<T extends Object>({ @required Route<dynamic> anchorRoute, Route<T> newRoute }) {
/// * [ModalRoute], which has as a [ModalRoute.willPop] method that can be assert(anchorRoute != null);
/// defined by a list of [WillPopCallback]s. assert(anchorRoute._navigator == this);
Future<bool> maybePop([dynamic result]) async { assert(_history.indexOf(anchorRoute) > 0);
final Route<dynamic> route = _history.last; replace<T>(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<bool> maybePop<T extends Object>([ T result ]) async {
final Route<T> route = _history.last;
assert(route._navigator == this); assert(route._navigator == this);
final RoutePopDisposition disposition = await route.willPop(); final RoutePopDisposition disposition = await route.willPop();
if (disposition != RoutePopDisposition.bubble && mounted) { if (disposition != RoutePopDisposition.bubble && mounted) {
...@@ -1160,20 +1651,29 @@ class NavigatorState extends State<Navigator> with TickerProviderStateMixin { ...@@ -1160,20 +1651,29 @@ class NavigatorState extends State<Navigator> with TickerProviderStateMixin {
return false; 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 /// {@macro flutter.widgets.navigator.pop}
/// route (see [Route.didPop]).
/// ///
/// If there are any routes left on the history, the top remaining route is /// ## Sample code
/// 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.
/// ///
/// Ongoing gestures within the current route are canceled when a route is /// Typical usage for closing a route is as follows:
/// popped. ///
bool pop([dynamic result]) { /// ```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 extends Object>([ T result ]) {
assert(!_debugLocked); assert(!_debugLocked);
assert(() { _debugLocked = true; return true; }()); assert(() { _debugLocked = true; return true; }());
final Route<dynamic> route = _history.last; final Route<dynamic> route = _history.last;
...@@ -1183,20 +1683,15 @@ class NavigatorState extends State<Navigator> with TickerProviderStateMixin { ...@@ -1183,20 +1683,15 @@ class NavigatorState extends State<Navigator> with TickerProviderStateMixin {
if (route.didPop(result ?? route.currentResult)) { if (route.didPop(result ?? route.currentResult)) {
assert(debugPredictedWouldPop); assert(debugPredictedWouldPop);
if (_history.length > 1) { if (_history.length > 1) {
setState(() { _history.removeLast();
// We use setState to guarantee that we'll rebuild, since the routes // If route._navigator is null, the route called finalizeRoute from
// can't do that for themselves, even if they have changed their own // didPop, which means the route has already been disposed and doesn't
// state (e.g. ModalScope.isCurrent). // need to be added to _poppedRoutes for later disposal.
_history.removeLast(); if (route._navigator != null)
// If route._navigator is null, the route called finalizeRoute from _poppedRoutes.add(route);
// didPop, which means the route has already been disposed and doesn't _history.last.didPopNext(route);
// need to be added to _poppedRoutes for later disposal. for (NavigatorObserver observer in widget.observers)
if (route._navigator != null) observer.didPop(route, _history.last);
_poppedRoutes.add(route);
_history.last.didPopNext(route);
for (NavigatorObserver observer in widget.observers)
observer.didPop(route, _history.last);
});
} else { } else {
assert(() { _debugLocked = false; return true; }()); assert(() { _debugLocked = false; return true; }());
return false; return false;
...@@ -1209,15 +1704,27 @@ class NavigatorState extends State<Navigator> with TickerProviderStateMixin { ...@@ -1209,15 +1704,27 @@ class NavigatorState extends State<Navigator> with TickerProviderStateMixin {
return true; return true;
} }
/// Immediately remove `route` and [Route.dispose] it. /// Calls [pop] repeatedly until the predicate returns true.
///
/// {@macro flutter.widgets.navigator.popUntil}
/// ///
/// The route's animation does not run and the future returned from pushing /// ## Sample code
/// the route will not complete. Ongoing input gestures are cancelled. If ///
/// the [Navigator] has any [Navigator.observers], they will be notified with /// Typical usage is as follows:
/// [NavigatorObserver.didRemove]. ///
/// ```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.
/// ///
/// This method is used to dismiss dropdown menus that are up when the screen's /// {@macro flutter.widgets.navigator.removeRoute}
/// orientation changes.
void removeRoute(Route<dynamic> route) { void removeRoute(Route<dynamic> route) {
assert(route != null); assert(route != null);
assert(!_debugLocked); assert(!_debugLocked);
...@@ -1227,18 +1734,40 @@ class NavigatorState extends State<Navigator> with TickerProviderStateMixin { ...@@ -1227,18 +1734,40 @@ class NavigatorState extends State<Navigator> with TickerProviderStateMixin {
assert(index != -1); assert(index != -1);
final Route<dynamic> previousRoute = index > 0 ? _history[index - 1] : null; final Route<dynamic> previousRoute = index > 0 ? _history[index - 1] : null;
final Route<dynamic> nextRoute = (index + 1 < _history.length) ? _history[index + 1] : null; final Route<dynamic> nextRoute = (index + 1 < _history.length) ? _history[index + 1] : null;
setState(() { _history.removeAt(index);
_history.removeAt(index); previousRoute?.didChangeNext(nextRoute);
previousRoute?.didChangeNext(nextRoute); nextRoute?.didChangePrevious(previousRoute);
nextRoute?.didChangePrevious(previousRoute); for (NavigatorObserver observer in widget.observers)
for (NavigatorObserver observer in widget.observers) observer.didRemove(route, previousRoute);
observer.didRemove(route, previousRoute); route.dispose();
route.dispose();
});
assert(() { _debugLocked = false; return true; }()); assert(() { _debugLocked = false; return true; }());
_cancelActivePointers(); _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<dynamic> anchorRoute) {
assert(!_debugLocked);
assert(() { _debugLocked = true; return true; }());
assert(anchorRoute._navigator == this);
final int index = _history.indexOf(anchorRoute) - 1;
assert(index >= 0);
final Route<dynamic> targetRoute = _history[index];
assert(targetRoute._navigator == this);
assert(targetRoute.overlayEntries.isEmpty || !overlay.debugIsVisible(targetRoute.overlayEntries.last));
_history.removeAt(index);
final Route<dynamic> nextRoute = index < _history.length ? _history[index] : null;
final Route<dynamic> 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. /// 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 /// When the navigator pops a route, the navigator retains a reference to the
...@@ -1255,27 +1784,6 @@ class NavigatorState extends State<Navigator> with TickerProviderStateMixin { ...@@ -1255,27 +1784,6 @@ class NavigatorState extends State<Navigator> with TickerProviderStateMixin {
route.dispose(); 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. /// Whether a route is currently being manipulated by the user, e.g.
/// as during an iOS back gesture. /// as during an iOS back gesture.
bool get userGestureInProgress => _userGesturesInProgress > 0; bool get userGestureInProgress => _userGesturesInProgress > 0;
...@@ -1326,6 +1834,8 @@ class NavigatorState extends State<Navigator> with TickerProviderStateMixin { ...@@ -1326,6 +1834,8 @@ class NavigatorState extends State<Navigator> with TickerProviderStateMixin {
final RenderAbsorbPointer absorber = _overlayKey.currentContext?.ancestorRenderObjectOfType(const TypeMatcher<RenderAbsorbPointer>()); final RenderAbsorbPointer absorber = _overlayKey.currentContext?.ancestorRenderObjectOfType(const TypeMatcher<RenderAbsorbPointer>());
setState(() { setState(() {
absorber?.absorbing = true; 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); _activePointers.toList().forEach(WidgetsBinding.instance.cancelPointer);
......
...@@ -12,7 +12,7 @@ import 'routes.dart'; ...@@ -12,7 +12,7 @@ import 'routes.dart';
abstract class PageRoute<T> extends ModalRoute<T> { abstract class PageRoute<T> extends ModalRoute<T> {
/// Creates a modal route that replaces the entire screen. /// Creates a modal route that replaces the entire screen.
PageRoute({ PageRoute({
RouteSettings settings: const RouteSettings(), RouteSettings settings,
this.fullscreenDialog: false, this.fullscreenDialog: false,
}) : super(settings: settings); }) : super(settings: settings);
...@@ -71,7 +71,7 @@ class PageRouteBuilder<T> extends PageRoute<T> { ...@@ -71,7 +71,7 @@ class PageRouteBuilder<T> extends PageRoute<T> {
/// The [pageBuilder], [transitionsBuilder], [opaque], [barrierDismissible], /// The [pageBuilder], [transitionsBuilder], [opaque], [barrierDismissible],
/// and [maintainState] arguments must not be null. /// and [maintainState] arguments must not be null.
PageRouteBuilder({ PageRouteBuilder({
RouteSettings settings: const RouteSettings(), RouteSettings settings,
@required this.pageBuilder, @required this.pageBuilder,
this.transitionsBuilder: _defaultTransitionsBuilder, this.transitionsBuilder: _defaultTransitionsBuilder,
this.transitionDuration: const Duration(milliseconds: 300), this.transitionDuration: const Duration(milliseconds: 300),
......
...@@ -14,12 +14,17 @@ import 'modal_barrier.dart'; ...@@ -14,12 +14,17 @@ import 'modal_barrier.dart';
import 'navigator.dart'; import 'navigator.dart';
import 'overlay.dart'; import 'overlay.dart';
import 'page_storage.dart'; import 'page_storage.dart';
import 'pages.dart'; import 'transitions.dart';
const Color _kTransparent = const Color(0x00000000); const Color _kTransparent = const Color(0x00000000);
/// A route that displays widgets in the [Navigator]'s [Overlay]. /// A route that displays widgets in the [Navigator]'s [Overlay].
abstract class OverlayRoute<T> extends Route<T> { abstract class OverlayRoute<T> extends Route<T> {
/// 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. /// Subclasses should override this getter to return the builders for the overlay.
Iterable<OverlayEntry> createOverlayEntries(); Iterable<OverlayEntry> createOverlayEntries();
...@@ -68,12 +73,21 @@ abstract class OverlayRoute<T> extends Route<T> { ...@@ -68,12 +73,21 @@ abstract class OverlayRoute<T> extends Route<T> {
/// A route with entrance and exit transitions. /// A route with entrance and exit transitions.
abstract class TransitionRoute<T> extends OverlayRoute<T> { abstract class TransitionRoute<T> extends OverlayRoute<T> {
/// 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 /// This future completes only once the transition itself has finished, after
/// the overlay entries have been removed from the navigator's overlay. /// the overlay entries have been removed from the navigator's overlay.
/// ///
/// This future completes once the animation has been dismissed. That will be /// This future completes once the animation has been dismissed. That will be
/// after [popped], because [popped] completes before the animation even /// after [popped], because [popped] typically completes before the animation
/// starts, as soon as the route is popped. /// even starts, as soon as the route is popped.
Future<T> get completed => _transitionCompleter.future; Future<T> get completed => _transitionCompleter.future;
final Completer<T> _transitionCompleter = new Completer<T>(); final Completer<T> _transitionCompleter = new Completer<T>();
...@@ -149,6 +163,7 @@ abstract class TransitionRoute<T> extends OverlayRoute<T> { ...@@ -149,6 +163,7 @@ abstract class TransitionRoute<T> extends OverlayRoute<T> {
} }
break; break;
} }
changedInternalState();
} }
/// The animation for the route being pushed on top of this route. This /// The animation for the route being pushed on top of this route. This
...@@ -179,7 +194,7 @@ abstract class TransitionRoute<T> extends OverlayRoute<T> { ...@@ -179,7 +194,7 @@ abstract class TransitionRoute<T> extends OverlayRoute<T> {
void didReplace(Route<dynamic> oldRoute) { void didReplace(Route<dynamic> oldRoute) {
assert(_controller != null, '$runtimeType.didReplace called before calling install() or after calling dispose().'); assert(_controller != null, '$runtimeType.didReplace called before calling install() or after calling dispose().');
assert(!_transitionCompleter.isCompleted, 'Cannot reuse a $runtimeType after disposing it.'); assert(!_transitionCompleter.isCompleted, 'Cannot reuse a $runtimeType after disposing it.');
if (oldRoute is TransitionRoute<dynamic>) if (oldRoute is TransitionRoute)
_controller.value = oldRoute._controller.value; _controller.value = oldRoute._controller.value;
_animation.addStatusListener(_handleStatusChanged); _animation.addStatusListener(_handleStatusChanged);
super.didReplace(oldRoute); super.didReplace(oldRoute);
...@@ -295,6 +310,8 @@ class LocalHistoryEntry { ...@@ -295,6 +310,8 @@ class LocalHistoryEntry {
/// pop internally if its list of local history entries is non-empty. Rather /// pop internally if its list of local history entries is non-empty. Rather
/// than being removed as the current route, the most recent [LocalHistoryEntry] /// than being removed as the current route, the most recent [LocalHistoryEntry]
/// is removed from the list and its [LocalHistoryEntry.onRemove] is called. /// is removed from the list and its [LocalHistoryEntry.onRemove] is called.
///
/// This class is typically used as a mixin.
abstract class LocalHistoryRoute<T> extends Route<T> { abstract class LocalHistoryRoute<T> extends Route<T> {
List<LocalHistoryEntry> _localHistory; List<LocalHistoryEntry> _localHistory;
...@@ -356,16 +373,6 @@ abstract class LocalHistoryRoute<T> extends Route<T> { ...@@ -356,16 +373,6 @@ abstract class LocalHistoryRoute<T> extends Route<T> {
bool get willHandlePopInternally { bool get willHandlePopInternally {
return _localHistory != null && _localHistory.isNotEmpty; 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 { class _ModalScopeStatus extends InheritedWidget {
...@@ -400,88 +407,99 @@ class _ModalScopeStatus extends InheritedWidget { ...@@ -400,88 +407,99 @@ class _ModalScopeStatus extends InheritedWidget {
} }
} }
class _ModalScope extends StatefulWidget { class _ModalScope<T> extends StatefulWidget {
const _ModalScope({ const _ModalScope({
Key key, Key key,
this.route, this.route,
@required this.page,
}) : super(key: key); }) : super(key: key);
final ModalRoute<dynamic> route; final ModalRoute<T> route;
final Widget page;
@override @override
_ModalScopeState createState() => new _ModalScopeState(); _ModalScopeState<T> createState() => new _ModalScopeState<T>();
} }
class _ModalScopeState extends State<_ModalScope> { class _ModalScopeState<T> extends State<_ModalScope<T>> {
// See addScopedWillPopCallback, removeScopedWillPopCallback in ModalRoute. // We cache the result of calling the route's buildPage, and clear the cache
final List<WillPopCallback> _willPopCallbacks = <WillPopCallback>[]; // 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 @override
void initState() { void initState() {
super.initState(); super.initState();
widget.route.animation?.addStatusListener(_animationStatusChanged); final List<Listenable> animations = <Listenable>[];
widget.route.secondaryAnimation?.addStatusListener(_animationStatusChanged); 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 @override
void didUpdateWidget(_ModalScope oldWidget) { void didUpdateWidget(_ModalScope<T> oldWidget) {
super.didUpdateWidget(oldWidget); super.didUpdateWidget(oldWidget);
assert(widget.route == oldWidget.route); assert(widget.route == oldWidget.route);
} }
@override @override
void dispose() { void didChangeDependencies() {
widget.route.animation?.removeStatusListener(_animationStatusChanged); super.didChangeDependencies();
widget.route.secondaryAnimation?.removeStatusListener(_animationStatusChanged); _page = null;
super.dispose();
} }
void addWillPopCallback(WillPopCallback callback) { void _forceRebuildPage() {
assert(mounted);
_willPopCallbacks.add(callback);
}
void removeWillPopCallback(WillPopCallback callback) {
assert(mounted);
_willPopCallbacks.remove(callback);
}
void _animationStatusChanged(AnimationStatus status) {
setState(() { 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) { void _routeSetState(VoidCallback fn) {
setState(fn); setState(fn);
} }
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
return new FocusScope( return new _ModalScopeStatus(
node: widget.route.focusScopeNode, 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( child: new Offstage(
offstage: widget.route.offstage, offstage: widget.route.offstage, // _routeSetState is called if this updates
child: new IgnorePointer( child: new PageStorage(
ignoring: widget.route.animation?.status == AnimationStatus.reverse, bucket: widget.route._storageBucket, // immutable
// Keep the transition between repaint boundaries so we don't trigger child: new FocusScope(
// deep repaints of the render tree above or below the transition. node: widget.route.focusScopeNode, // immutable
child: new RepaintBoundary( child: new RepaintBoundary(
child: widget.route.buildTransitions( child: new AnimatedBuilder(
context, animation: _listenable, // immutable
widget.route.animation, builder: (BuildContext context, Widget child) {
widget.route.secondaryAnimation, return widget.route.buildTransitions(
new RepaintBoundary( context,
child: new PageStorage( widget.route.animation,
key: widget.route._subtreeKey, widget.route.secondaryAnimation,
bucket: widget.route._storageBucket, new IgnorePointer(
child: new _ModalScopeStatus( ignoring: widget.route.animation?.status == AnimationStatus.reverse,
route: widget.route, child: child,
isCurrent: widget.route.isCurrent, ),
canPop: widget.route.canPop, );
child: widget.page, },
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> { ...@@ -495,22 +513,20 @@ class _ModalScopeState extends State<_ModalScope> {
/// A route that blocks interaction with previous routes. /// A route that blocks interaction with previous routes.
/// ///
/// ModalRoutes cover the entire [Navigator]. They are not necessarily [opaque], /// [ModalRoute]s cover the entire [Navigator]. They are not necessarily
/// however; for example, a pop-up menu uses a ModalRoute but only shows the menu /// [opaque], however; for example, a pop-up menu uses a [ModalRoute] but only
/// in a small box overlapping the previous route. /// 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<T> extends TransitionRoute<T> with LocalHistoryRoute<T> { abstract class ModalRoute<T> extends TransitionRoute<T> with LocalHistoryRoute<T> {
/// Creates a route that blocks interaction with previous routes. /// Creates a route that blocks interaction with previous routes.
ModalRoute({ ModalRoute({
this.settings: const RouteSettings() RouteSettings settings,
}); }) : super._settings(settings);
// The API for general users of this class // 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 the modal route most closely associated with the given context.
/// ///
/// Returns null if the given context is not associated with a modal route. /// Returns null if the given context is not associated with a modal route.
...@@ -518,19 +534,19 @@ abstract class ModalRoute<T> extends TransitionRoute<T> with LocalHistoryRoute<T ...@@ -518,19 +534,19 @@ abstract class ModalRoute<T> extends TransitionRoute<T> with LocalHistoryRoute<T
/// Typical usage is as follows: /// Typical usage is as follows:
/// ///
/// ```dart /// ```dart
/// ModalRoute<dynamic> route = ModalRoute.of(context); /// ModalRoute route = ModalRoute.of(context);
/// ``` /// ```
/// ///
/// The given [BuildContext] will be rebuilt if the state of the route changes /// The given [BuildContext] will be rebuilt if the state of the route changes
/// (specifically, if [isCurrent] or [canPop] change value). /// (specifically, if [isCurrent] or [canPop] change value).
static ModalRoute<dynamic> of(BuildContext context) { static ModalRoute<T> of<T extends Object>(BuildContext context) {
final _ModalScopeStatus widget = context.inheritFromWidgetOfExactType(_ModalScopeStatus); final _ModalScopeStatus widget = context.inheritFromWidgetOfExactType(_ModalScopeStatus);
return widget?.route; return widget?.route;
} }
/// Schedule a call to [buildTransitions]. /// 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: /// the change in a function that you pass to [setState], as in:
/// ///
/// ```dart /// ```dart
...@@ -579,13 +595,14 @@ abstract class ModalRoute<T> extends TransitionRoute<T> with LocalHistoryRoute<T ...@@ -579,13 +595,14 @@ abstract class ModalRoute<T> extends TransitionRoute<T> with LocalHistoryRoute<T
/// this route. This animation lets this route coordinate with the entrance /// this route. This animation lets this route coordinate with the entrance
/// and exit transition of routes pushed on top of this route. /// and exit transition of routes pushed on top of this route.
/// ///
/// This method is called when the route is first built, and rarely /// This method is only called when the route is first built, and rarely
/// thereafter. In particular, it is not called again when the route's state /// thereafter. In particular, it is not automatically called again when the
/// changes. For a builder that is called every time the route's state /// route's state changes unless it uses [ModalRoute.of]. For a builder that
/// changes, consider [buildTransitions]. For widgets that change their /// is called every time the route's state changes, consider
/// behavior when the route's state changes, consider [ModalRoute.of] to /// [buildTransitions]. For widgets that change their behavior when the
/// obtain a reference to the route; this will cause the widget to be rebuilt /// route's state changes, consider [ModalRoute.of] to obtain a reference to
/// each time the route changes state. /// the route; this will cause the widget to be rebuilt each time the route
/// changes state.
/// ///
/// In general, [buildPage] should be used to build the page contents, and /// In general, [buildPage] should be used to build the page contents, and
/// [buildTransitions] for the widgets that change as the page is brought in /// [buildTransitions] for the widgets that change as the page is brought in
...@@ -700,7 +717,7 @@ abstract class ModalRoute<T> extends TransitionRoute<T> with LocalHistoryRoute<T ...@@ -700,7 +717,7 @@ abstract class ModalRoute<T> extends TransitionRoute<T> with LocalHistoryRoute<T
/// on the top of its stack, the old topmost route's [secondaryAnimation] /// on the top of its stack, the old topmost route's [secondaryAnimation]
/// runs from 0.0 to 1.0. When the [Navigator] pops the topmost route, the /// runs from 0.0 to 1.0. When the [Navigator] pops the topmost route, the
/// [secondaryAnimation] for the route below it runs from 1.0 to 0.0. /// [secondaryAnimation] for the route below it runs from 1.0 to 0.0.
/// * `child`, the page contents. /// * `child`, the page contents, as returned by [buildPage].
/// ///
/// See also: /// See also:
/// ///
...@@ -753,6 +770,10 @@ abstract class ModalRoute<T> extends TransitionRoute<T> with LocalHistoryRoute<T ...@@ -753,6 +770,10 @@ abstract class ModalRoute<T> extends TransitionRoute<T> with LocalHistoryRoute<T
/// ///
/// If [barrierDismissible] is false, then tapping the barrier has no effect. /// If [barrierDismissible] is false, then tapping the barrier has no effect.
/// ///
/// If this getter would ever start returning a different color,
/// [changedInternalState] should be invoked so that the change can take
/// effect.
///
/// See also: /// See also:
/// ///
/// * [barrierColor], which controls the color of the scrim for this route. /// * [barrierColor], which controls the color of the scrim for this route.
...@@ -775,6 +796,10 @@ abstract class ModalRoute<T> extends TransitionRoute<T> with LocalHistoryRoute<T ...@@ -775,6 +796,10 @@ abstract class ModalRoute<T> extends TransitionRoute<T> with LocalHistoryRoute<T
/// While the route is animating into position, the color is animated from /// While the route is animating into position, the color is animated from
/// transparent to the specified color. /// transparent to the specified color.
/// ///
/// If this getter would ever start returning a different color,
/// [changedInternalState] should be invoked so that the change can take
/// effect.
///
/// See also: /// See also:
/// ///
/// * [barrierDismissible], which controls the behavior of the barrier when /// * [barrierDismissible], which controls the behavior of the barrier when
...@@ -794,6 +819,10 @@ abstract class ModalRoute<T> extends TransitionRoute<T> with LocalHistoryRoute<T ...@@ -794,6 +819,10 @@ abstract class ModalRoute<T> extends TransitionRoute<T> with LocalHistoryRoute<T
/// For example, when a dialog is on the screen, the page below the dialog is /// For example, when a dialog is on the screen, the page below the dialog is
/// usually darkened by the modal barrier. /// usually darkened by the modal barrier.
/// ///
/// If this getter would ever start returning a different color,
/// [changedInternalState] should be invoked so that the change can take
/// effect.
///
/// See also: /// See also:
/// ///
/// * [barrierDismissible], which controls the behavior of the barrier when /// * [barrierDismissible], which controls the behavior of the barrier when
...@@ -801,11 +830,17 @@ abstract class ModalRoute<T> extends TransitionRoute<T> with LocalHistoryRoute<T ...@@ -801,11 +830,17 @@ abstract class ModalRoute<T> extends TransitionRoute<T> with LocalHistoryRoute<T
/// * [ModalBarrier], the widget that implements this feature. /// * [ModalBarrier], the widget that implements this feature.
String get barrierLabel; String get barrierLabel;
/// Whether the route should remain in memory when it is inactive. If this is /// Whether the route should remain in memory when it is inactive.
/// true, then the route is maintained, so that any futures it is holding from ///
/// the next route will properly resolve when the next route pops. If this is /// If this is true, then the route is maintained, so that any futures it is
/// not necessary, this can be set to false to allow the framework to entirely /// holding from the next route will properly resolve when the next route
/// discard the route's widget hierarchy when it is not visible. /// pops. If this is not necessary, this can be set to false to allow the
/// framework to entirely discard the route's widget hierarchy when it is not
/// visible.
///
/// The value of this getter should not change during the lifetime of the
/// object. It is used by [createOverlayEntries], which is called by
/// [install] near the beginning of the route lifecycle.
bool get maintainState; bool get maintainState;
...@@ -844,6 +879,8 @@ abstract class ModalRoute<T> extends TransitionRoute<T> with LocalHistoryRoute<T ...@@ -844,6 +879,8 @@ abstract class ModalRoute<T> extends TransitionRoute<T> with LocalHistoryRoute<T
Animation<double> get secondaryAnimation => _secondaryAnimationProxy; Animation<double> get secondaryAnimation => _secondaryAnimationProxy;
ProxyAnimation _secondaryAnimationProxy; ProxyAnimation _secondaryAnimationProxy;
final List<WillPopCallback> _willPopCallbacks = <WillPopCallback>[];
/// Returns the value of the first callback added with /// Returns the value of the first callback added with
/// [addScopedWillPopCallback] that returns false. If they all return true, /// [addScopedWillPopCallback] that returns false. If they all return true,
/// returns the inherited method's result (see [Route.willPop]). /// returns the inherited method's result (see [Route.willPop]).
...@@ -862,9 +899,9 @@ abstract class ModalRoute<T> extends TransitionRoute<T> with LocalHistoryRoute<T ...@@ -862,9 +899,9 @@ abstract class ModalRoute<T> extends TransitionRoute<T> with LocalHistoryRoute<T
/// this method checks. /// this method checks.
@override @override
Future<RoutePopDisposition> willPop() async { Future<RoutePopDisposition> willPop() async {
final _ModalScopeState scope = _scopeKey.currentState; final _ModalScopeState<T> scope = _scopeKey.currentState;
assert(scope != null); assert(scope != null);
for (WillPopCallback callback in new List<WillPopCallback>.from(scope._willPopCallbacks)) { for (WillPopCallback callback in new List<WillPopCallback>.from(_willPopCallbacks)) {
if (!await callback()) if (!await callback())
return RoutePopDisposition.doNotPop; return RoutePopDisposition.doNotPop;
} }
...@@ -932,8 +969,8 @@ abstract class ModalRoute<T> extends TransitionRoute<T> with LocalHistoryRoute<T ...@@ -932,8 +969,8 @@ abstract class ModalRoute<T> extends TransitionRoute<T> with LocalHistoryRoute<T
/// * [removeScopedWillPopCallback], which removes a callback from the list /// * [removeScopedWillPopCallback], which removes a callback from the list
/// that [willPop] checks. /// that [willPop] checks.
void addScopedWillPopCallback(WillPopCallback callback) { void addScopedWillPopCallback(WillPopCallback callback) {
assert(_scopeKey.currentState != null); assert(_scopeKey.currentState != null, 'Tried to add a willPop callback to a route that is not currently in the tree.');
_scopeKey.currentState.addWillPopCallback(callback); _willPopCallbacks.add(callback);
} }
/// Remove one of the callbacks run by [willPop]. /// Remove one of the callbacks run by [willPop].
...@@ -944,8 +981,8 @@ abstract class ModalRoute<T> extends TransitionRoute<T> with LocalHistoryRoute<T ...@@ -944,8 +981,8 @@ abstract class ModalRoute<T> extends TransitionRoute<T> with LocalHistoryRoute<T
/// * [addScopedWillPopCallback], which adds callback to the list /// * [addScopedWillPopCallback], which adds callback to the list
/// checked by [willPop]. /// checked by [willPop].
void removeScopedWillPopCallback(WillPopCallback callback) { void removeScopedWillPopCallback(WillPopCallback callback) {
assert(_scopeKey.currentState != null); assert(_scopeKey.currentState != null, 'Tried to remove a willPop callback from a route that is not currently in the tree.');
_scopeKey.currentState.removeWillPopCallback(callback); _willPopCallbacks.remove(callback);
} }
/// True if one or more [WillPopCallback] callbacks exist. /// True if one or more [WillPopCallback] callbacks exist.
...@@ -966,19 +1003,27 @@ abstract class ModalRoute<T> extends TransitionRoute<T> with LocalHistoryRoute<T ...@@ -966,19 +1003,27 @@ abstract class ModalRoute<T> extends TransitionRoute<T> with LocalHistoryRoute<T
/// a pop might be vetoed. /// a pop might be vetoed.
@protected @protected
bool get hasScopedWillPopCallback { bool get hasScopedWillPopCallback {
return _scopeKey.currentState == null || _scopeKey.currentState._willPopCallbacks.isNotEmpty; return _willPopCallbacks.isNotEmpty;
}
@override
void didChangePrevious(Route<dynamic> previousRoute) {
super.didChangePrevious(previousRoute);
changedInternalState();
} }
@override @override
void changedInternalState() { void changedInternalState() {
super.changedInternalState(); super.changedInternalState();
setState(() { /* internal state already changed */ }); setState(() { /* internal state already changed */ });
_modalBarrier.markNeedsBuild();
} }
@override @override
void didChangePrevious(Route<dynamic> previousRoute) { void changedExternalState() {
super.didChangePrevious(previousRoute); super.changedExternalState();
setState(() { /* this might affect canPop */ }); if (_scopeKey.currentState != null)
_scopeKey.currentState._forceRebuildPage();
} }
/// Whether this route can be popped. /// Whether this route can be popped.
...@@ -989,53 +1034,57 @@ abstract class ModalRoute<T> extends TransitionRoute<T> with LocalHistoryRoute<T ...@@ -989,53 +1034,57 @@ abstract class ModalRoute<T> extends TransitionRoute<T> with LocalHistoryRoute<T
// Internals // Internals
final GlobalKey<_ModalScopeState> _scopeKey = new GlobalKey<_ModalScopeState>(); final GlobalKey<_ModalScopeState<T>> _scopeKey = new GlobalKey<_ModalScopeState<T>>();
final GlobalKey _subtreeKey = new GlobalKey(); final GlobalKey _subtreeKey = new GlobalKey();
final PageStorageBucket _storageBucket = new PageStorageBucket(); final PageStorageBucket _storageBucket = new PageStorageBucket();
// one of the builders // one of the builders
OverlayEntry _modalBarrier;
Widget _buildModalBarrier(BuildContext context) { Widget _buildModalBarrier(BuildContext context) {
Widget barrier; Widget barrier;
if (barrierColor != null && !offstage) { if (barrierColor != null && !offstage) { // changedInternalState is called if these update
assert(barrierColor != _kTransparent); assert(barrierColor != _kTransparent);
final Animation<Color> color = new ColorTween( final Animation<Color> color = new ColorTween(
begin: _kTransparent, begin: _kTransparent,
end: barrierColor end: barrierColor, // changedInternalState is called if this updates
).animate(new CurvedAnimation( ).animate(new CurvedAnimation(
parent: animation, parent: animation,
curve: Curves.ease curve: Curves.ease,
)); ));
barrier = new AnimatedModalBarrier( barrier = new AnimatedModalBarrier(
color: color, color: color,
dismissible: barrierDismissible, dismissible: barrierDismissible, // changedInternalState is called if this updates
semanticsLabel: barrierLabel, semanticsLabel: barrierLabel, // changedInternalState is called if this updates
); );
} else { } else {
barrier = new ModalBarrier( barrier = new ModalBarrier(
dismissible: barrierDismissible, dismissible: barrierDismissible, // changedInternalState is called if this updates
semanticsLabel: barrierLabel, semanticsLabel: barrierLabel, // changedInternalState is called if this updates
); );
} }
assert(animation.status != AnimationStatus.dismissed);
return new IgnorePointer( return new IgnorePointer(
ignoring: animation.status == AnimationStatus.reverse, ignoring: animation.status == AnimationStatus.reverse || // changedInternalState is called when this updates
child: barrier 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 // one of the builders
Widget _buildModalScope(BuildContext context) { Widget _buildModalScope(BuildContext context) {
return new _ModalScope( return _modalScopeCache ??= new _ModalScope<T>(
key: _scopeKey, key: _scopeKey,
route: this, route: this,
page: buildPage(context, animation, secondaryAnimation) // _ModalScope calls buildTransitions() and buildChild(), defined above
// _ModalScope calls buildTransitions(), defined above
); );
} }
@override @override
Iterable<OverlayEntry> createOverlayEntries() sync* { Iterable<OverlayEntry> createOverlayEntries() sync* {
yield new OverlayEntry(builder: _buildModalBarrier); yield _modalBarrier = new OverlayEntry(builder: _buildModalBarrier);
yield new OverlayEntry(builder: _buildModalScope, maintainState: maintainState); yield new OverlayEntry(builder: _buildModalScope, maintainState: maintainState);
} }
...@@ -1045,30 +1094,40 @@ abstract class ModalRoute<T> extends TransitionRoute<T> with LocalHistoryRoute<T ...@@ -1045,30 +1094,40 @@ abstract class ModalRoute<T> extends TransitionRoute<T> with LocalHistoryRoute<T
/// A modal route that overlays a widget over the current route. /// A modal route that overlays a widget over the current route.
abstract class PopupRoute<T> extends ModalRoute<T> { abstract class PopupRoute<T> extends ModalRoute<T> {
/// Initializes the [PopupRoute].
PopupRoute({
RouteSettings settings,
}) : super(settings: settings);
@override @override
bool get opaque => false; bool get opaque => false;
@override @override
bool get maintainState => true; bool get maintainState => true;
@override
void didChangeNext(Route<dynamic> nextRoute) {
assert(nextRoute is! PageRoute<dynamic>);
super.didChangeNext(nextRoute);
}
} }
/// A [Navigator] observer that notifies [RouteAware]s of changes to the /// A [Navigator] observer that notifies [RouteAware]s of changes to the
/// state of their [Route]. /// state of their [Route].
/// ///
/// [RouteObserver] informs subscribers whenever a route of type `T` is pushed /// [RouteObserver] informs subscribers whenever a route of type `R` is pushed
/// on top of their own route of type `T` or popped from it. This is for example /// 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.i. a `RouteObserver<PageRoute>` /// useful to keep track of page transitions, e.g. a `RouteObserver<PageRoute>`
/// will inform subscribed [RouteAware]s whenever the user navigates away from /// will inform subscribed [RouteAware]s whenever the user navigates away from
/// the current page route to another page route. /// the current page route to another page route.
/// ///
/// If you want to be informed about route changes of any type, you should /// To be informed about route changes of any type, consider instantiating a
/// instantiate a `RouteObserver<Route>`. /// `RouteObserver<Route>`.
///
/// ## 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<PageRoute<dynamic>>` type may be used.
/// ///
/// ## Sample code /// ## Sample code
/// ///
...@@ -1119,15 +1178,15 @@ abstract class PopupRoute<T> extends ModalRoute<T> { ...@@ -1119,15 +1178,15 @@ abstract class PopupRoute<T> extends ModalRoute<T> {
/// ///
/// } /// }
/// ``` /// ```
class RouteObserver<T extends Route<dynamic>> extends NavigatorObserver { class RouteObserver<R extends Route<dynamic>> extends NavigatorObserver {
final Map<T, Set<RouteAware>> _listeners = <T, Set<RouteAware>>{}; final Map<R, Set<RouteAware>> _listeners = <R, Set<RouteAware>>{};
/// Subscribe [routeAware] to be informed about changes to [route]. /// Subscribe [routeAware] to be informed about changes to [route].
/// ///
/// Going forward, [routeAware] will be informed about qualifying changes /// Going forward, [routeAware] will be informed about qualifying changes
/// to [route], e.g. when [route] is covered by another route or when [route] /// to [route], e.g. when [route] is covered by another route or when [route]
/// is popped off the [Navigator] stack. /// is popped off the [Navigator] stack.
void subscribe(RouteAware routeAware, T route) { void subscribe(RouteAware routeAware, R route) {
assert(routeAware != null); assert(routeAware != null);
assert(route != null); assert(route != null);
final Set<RouteAware> subscribers = _listeners.putIfAbsent(route, () => new Set<RouteAware>()); final Set<RouteAware> subscribers = _listeners.putIfAbsent(route, () => new Set<RouteAware>());
...@@ -1142,7 +1201,7 @@ class RouteObserver<T extends Route<dynamic>> extends NavigatorObserver { ...@@ -1142,7 +1201,7 @@ class RouteObserver<T extends Route<dynamic>> extends NavigatorObserver {
/// subscribed to multiple types, this will unregister it (once) from each type. /// subscribed to multiple types, this will unregister it (once) from each type.
void unsubscribe(RouteAware routeAware) { void unsubscribe(RouteAware routeAware) {
assert(routeAware != null); assert(routeAware != null);
for (T route in _listeners.keys) { for (R route in _listeners.keys) {
final Set<RouteAware> subscribers = _listeners[route]; final Set<RouteAware> subscribers = _listeners[route];
subscribers?.remove(routeAware); subscribers?.remove(routeAware);
} }
...@@ -1150,7 +1209,7 @@ class RouteObserver<T extends Route<dynamic>> extends NavigatorObserver { ...@@ -1150,7 +1209,7 @@ class RouteObserver<T extends Route<dynamic>> extends NavigatorObserver {
@override @override
void didPop(Route<dynamic> route, Route<dynamic> previousRoute) { void didPop(Route<dynamic> route, Route<dynamic> previousRoute) {
if (route is T && previousRoute is T) { if (route is R && previousRoute is R) {
final List<RouteAware> previousSubscribers = _listeners[previousRoute]?.toList(); final List<RouteAware> previousSubscribers = _listeners[previousRoute]?.toList();
if (previousSubscribers != null) { if (previousSubscribers != null) {
...@@ -1171,7 +1230,7 @@ class RouteObserver<T extends Route<dynamic>> extends NavigatorObserver { ...@@ -1171,7 +1230,7 @@ class RouteObserver<T extends Route<dynamic>> extends NavigatorObserver {
@override @override
void didPush(Route<dynamic> route, Route<dynamic> previousRoute) { void didPush(Route<dynamic> route, Route<dynamic> previousRoute) {
if (route is T && previousRoute is T) { if (route is R && previousRoute is R) {
final Set<RouteAware> previousSubscribers = _listeners[previousRoute]; final Set<RouteAware> previousSubscribers = _listeners[previousRoute];
if (previousSubscribers != null) { if (previousSubscribers != null) {
......
...@@ -17,7 +17,7 @@ void main() { ...@@ -17,7 +17,7 @@ void main() {
builder: (BuildContext context) { builder: (BuildContext context) {
return new RaisedButton( return new RaisedButton(
onPressed: () { onPressed: () {
showDialog<Null>( showDialog<void>(
context: context, context: context,
builder: (BuildContext context) { builder: (BuildContext context) {
return new CupertinoAlertDialog( return new CupertinoAlertDialog(
...@@ -110,7 +110,7 @@ void main() { ...@@ -110,7 +110,7 @@ void main() {
child: new Builder(builder: (BuildContext context) { child: new Builder(builder: (BuildContext context) {
return new RaisedButton( return new RaisedButton(
onPressed: () { onPressed: () {
showDialog<Null>( showDialog<void>(
context: context, context: context,
builder: (BuildContext context) { builder: (BuildContext context) {
return new MediaQuery( return new MediaQuery(
......
...@@ -14,7 +14,7 @@ void main() { ...@@ -14,7 +14,7 @@ void main() {
new WidgetsApp( new WidgetsApp(
color: const Color(0xFFFFFFFF), color: const Color(0xFFFFFFFF),
onGenerateRoute: (RouteSettings settings) { onGenerateRoute: (RouteSettings settings) {
return new CupertinoPageRoute<Null>( return new CupertinoPageRoute<void>(
settings: settings, settings: settings,
builder: (BuildContext context) { builder: (BuildContext context) {
return const CupertinoNavigationBar( return const CupertinoNavigationBar(
...@@ -36,7 +36,7 @@ void main() { ...@@ -36,7 +36,7 @@ void main() {
new WidgetsApp( new WidgetsApp(
color: const Color(0xFFFFFFFF), color: const Color(0xFFFFFFFF),
onGenerateRoute: (RouteSettings settings) { onGenerateRoute: (RouteSettings settings) {
return new CupertinoPageRoute<Null>( return new CupertinoPageRoute<void>(
settings: settings, settings: settings,
builder: (BuildContext context) { builder: (BuildContext context) {
return const CupertinoNavigationBar( return const CupertinoNavigationBar(
...@@ -56,7 +56,7 @@ void main() { ...@@ -56,7 +56,7 @@ void main() {
new WidgetsApp( new WidgetsApp(
color: const Color(0xFFFFFFFF), color: const Color(0xFFFFFFFF),
onGenerateRoute: (RouteSettings settings) { onGenerateRoute: (RouteSettings settings) {
return new CupertinoPageRoute<Null>( return new CupertinoPageRoute<void>(
settings: settings, settings: settings,
builder: (BuildContext context) { builder: (BuildContext context) {
return const CupertinoNavigationBar( return const CupertinoNavigationBar(
...@@ -76,7 +76,7 @@ void main() { ...@@ -76,7 +76,7 @@ void main() {
new WidgetsApp( new WidgetsApp(
color: const Color(0xFFFFFFFF), color: const Color(0xFFFFFFFF),
onGenerateRoute: (RouteSettings settings) { onGenerateRoute: (RouteSettings settings) {
return new CupertinoPageRoute<Null>( return new CupertinoPageRoute<void>(
settings: settings, settings: settings,
builder: (BuildContext context) { builder: (BuildContext context) {
return const CupertinoNavigationBar( return const CupertinoNavigationBar(
...@@ -98,7 +98,7 @@ void main() { ...@@ -98,7 +98,7 @@ void main() {
new WidgetsApp( new WidgetsApp(
color: const Color(0xFFFFFFFF), color: const Color(0xFFFFFFFF),
onGenerateRoute: (RouteSettings settings) { onGenerateRoute: (RouteSettings settings) {
return new CupertinoPageRoute<Null>( return new CupertinoPageRoute<void>(
settings: settings, settings: settings,
builder: (BuildContext context) { builder: (BuildContext context) {
return const CupertinoPageScaffold( return const CupertinoPageScaffold(
...@@ -126,7 +126,7 @@ void main() { ...@@ -126,7 +126,7 @@ void main() {
new WidgetsApp( new WidgetsApp(
color: const Color(0xFFFFFFFF), color: const Color(0xFFFFFFFF),
onGenerateRoute: (RouteSettings settings) { onGenerateRoute: (RouteSettings settings) {
return new CupertinoPageRoute<Null>( return new CupertinoPageRoute<void>(
settings: settings, settings: settings,
builder: (BuildContext context) { builder: (BuildContext context) {
return new MediaQuery( return new MediaQuery(
...@@ -178,7 +178,7 @@ void main() { ...@@ -178,7 +178,7 @@ void main() {
new WidgetsApp( new WidgetsApp(
color: const Color(0xFFFFFFFF), color: const Color(0xFFFFFFFF),
onGenerateRoute: (RouteSettings settings) { onGenerateRoute: (RouteSettings settings) {
return new CupertinoPageRoute<Null>( return new CupertinoPageRoute<void>(
settings: settings, settings: settings,
builder: (BuildContext context) { builder: (BuildContext context) {
return new CupertinoPageScaffold( return new CupertinoPageScaffold(
...@@ -266,7 +266,7 @@ void main() { ...@@ -266,7 +266,7 @@ void main() {
new WidgetsApp( new WidgetsApp(
color: const Color(0xFFFFFFFF), color: const Color(0xFFFFFFFF),
onGenerateRoute: (RouteSettings settings) { onGenerateRoute: (RouteSettings settings) {
return new CupertinoPageRoute<Null>( return new CupertinoPageRoute<void>(
settings: settings, settings: settings,
builder: (BuildContext context) { builder: (BuildContext context) {
return new CupertinoPageScaffold( return new CupertinoPageScaffold(
...@@ -337,7 +337,7 @@ void main() { ...@@ -337,7 +337,7 @@ void main() {
new WidgetsApp( new WidgetsApp(
color: const Color(0xFFFFFFFF), color: const Color(0xFFFFFFFF),
onGenerateRoute: (RouteSettings settings) { onGenerateRoute: (RouteSettings settings) {
return new CupertinoPageRoute<Null>( return new CupertinoPageRoute<void>(
settings: settings, settings: settings,
builder: (BuildContext context) { builder: (BuildContext context) {
return const CupertinoNavigationBar( return const CupertinoNavigationBar(
...@@ -351,7 +351,7 @@ void main() { ...@@ -351,7 +351,7 @@ void main() {
expect(find.byType(CupertinoButton), findsNothing); expect(find.byType(CupertinoButton), findsNothing);
tester.state<NavigatorState>(find.byType(Navigator)).push(new CupertinoPageRoute<Null>( tester.state<NavigatorState>(find.byType(Navigator)).push(new CupertinoPageRoute<void>(
builder: (BuildContext context) { builder: (BuildContext context) {
return const CupertinoNavigationBar( return const CupertinoNavigationBar(
middle: const Text('Page 2'), middle: const Text('Page 2'),
...@@ -365,7 +365,7 @@ void main() { ...@@ -365,7 +365,7 @@ void main() {
expect(find.byType(CupertinoButton), findsOneWidget); expect(find.byType(CupertinoButton), findsOneWidget);
expect(find.byType(Icon), findsOneWidget); expect(find.byType(Icon), findsOneWidget);
tester.state<NavigatorState>(find.byType(Navigator)).push(new CupertinoPageRoute<Null>( tester.state<NavigatorState>(find.byType(Navigator)).push(new CupertinoPageRoute<void>(
fullscreenDialog: true, fullscreenDialog: true,
builder: (BuildContext context) { builder: (BuildContext context) {
return const CupertinoNavigationBar( return const CupertinoNavigationBar(
...@@ -401,7 +401,7 @@ void main() { ...@@ -401,7 +401,7 @@ void main() {
new WidgetsApp( new WidgetsApp(
color: const Color(0xFFFFFFFF), color: const Color(0xFFFFFFFF),
onGenerateRoute: (RouteSettings settings) { onGenerateRoute: (RouteSettings settings) {
return new CupertinoPageRoute<Null>( return new CupertinoPageRoute<void>(
settings: settings, settings: settings,
builder: (BuildContext context) { builder: (BuildContext context) {
return const CupertinoNavigationBar( return const CupertinoNavigationBar(
...@@ -428,7 +428,7 @@ void main() { ...@@ -428,7 +428,7 @@ void main() {
new WidgetsApp( new WidgetsApp(
color: const Color(0xFFFFFFFF), color: const Color(0xFFFFFFFF),
onGenerateRoute: (RouteSettings settings) { onGenerateRoute: (RouteSettings settings) {
return new CupertinoPageRoute<Null>( return new CupertinoPageRoute<void>(
settings: settings, settings: settings,
builder: (BuildContext context) { builder: (BuildContext context) {
return const CupertinoNavigationBar( return const CupertinoNavigationBar(
...@@ -462,7 +462,7 @@ void main() { ...@@ -462,7 +462,7 @@ void main() {
new WidgetsApp( new WidgetsApp(
color: const Color(0xFFFFFFFF), color: const Color(0xFFFFFFFF),
onGenerateRoute: (RouteSettings settings) { onGenerateRoute: (RouteSettings settings) {
return new CupertinoPageRoute<Null>( return new CupertinoPageRoute<void>(
settings: settings, settings: settings,
builder: (BuildContext context) { builder: (BuildContext context) {
return const CupertinoNavigationBar( return const CupertinoNavigationBar(
......
...@@ -11,7 +11,7 @@ void main() { ...@@ -11,7 +11,7 @@ void main() {
new WidgetsApp( new WidgetsApp(
color: const Color(0xFFFFFFFF), color: const Color(0xFFFFFFFF),
onGenerateRoute: (RouteSettings settings) { onGenerateRoute: (RouteSettings settings) {
return new CupertinoPageRoute<Null>( return new CupertinoPageRoute<void>(
settings: settings, settings: settings,
builder: (BuildContext context) { builder: (BuildContext context) {
final String pageNumber = settings.name == '/' ? '1' : '2'; final String pageNumber = settings.name == '/' ? '1' : '2';
...@@ -82,7 +82,7 @@ void main() { ...@@ -82,7 +82,7 @@ void main() {
], ],
color: const Color(0xFFFFFFFF), color: const Color(0xFFFFFFFF),
onGenerateRoute: (RouteSettings settings) { onGenerateRoute: (RouteSettings settings) {
return new CupertinoPageRoute<Null>( return new CupertinoPageRoute<void>(
settings: settings, settings: settings,
builder: (BuildContext context) { builder: (BuildContext context) {
final String pageNumber = settings.name == '/' ? '1' : '2'; final String pageNumber = settings.name == '/' ? '1' : '2';
...@@ -151,7 +151,7 @@ void main() { ...@@ -151,7 +151,7 @@ void main() {
new WidgetsApp( new WidgetsApp(
color: const Color(0xFFFFFFFF), color: const Color(0xFFFFFFFF),
onGenerateRoute: (RouteSettings settings) { onGenerateRoute: (RouteSettings settings) {
return new CupertinoPageRoute<Null>( return new CupertinoPageRoute<void>(
settings: settings, settings: settings,
builder: (BuildContext context) { builder: (BuildContext context) {
return const Center(child: const Text('Page 1')); return const Center(child: const Text('Page 1'));
...@@ -163,7 +163,7 @@ void main() { ...@@ -163,7 +163,7 @@ void main() {
final Offset widget1InitialTopLeft = tester.getTopLeft(find.text('Page 1')); final Offset widget1InitialTopLeft = tester.getTopLeft(find.text('Page 1'));
tester.state<NavigatorState>(find.byType(Navigator)).push(new CupertinoPageRoute<Null>( tester.state<NavigatorState>(find.byType(Navigator)).push(new CupertinoPageRoute<void>(
builder: (BuildContext context) { builder: (BuildContext context) {
return const Center(child: const Text('Page 2')); return const Center(child: const Text('Page 2'));
}, },
...@@ -219,7 +219,7 @@ void main() { ...@@ -219,7 +219,7 @@ void main() {
new WidgetsApp( new WidgetsApp(
color: const Color(0xFFFFFFFF), color: const Color(0xFFFFFFFF),
onGenerateRoute: (RouteSettings settings) { onGenerateRoute: (RouteSettings settings) {
return new CupertinoPageRoute<Null>( return new CupertinoPageRoute<void>(
settings: settings, settings: settings,
builder: (BuildContext context) { builder: (BuildContext context) {
final String pageNumber = settings.name == '/' ? '1' : '2'; final String pageNumber = settings.name == '/' ? '1' : '2';
...@@ -284,7 +284,7 @@ void main() { ...@@ -284,7 +284,7 @@ void main() {
], ],
color: const Color(0xFFFFFFFF), color: const Color(0xFFFFFFFF),
onGenerateRoute: (RouteSettings settings) { onGenerateRoute: (RouteSettings settings) {
return new CupertinoPageRoute<Null>( return new CupertinoPageRoute<void>(
settings: settings, settings: settings,
builder: (BuildContext context) { builder: (BuildContext context) {
final String pageNumber = settings.name == '/' ? '1' : '2'; final String pageNumber = settings.name == '/' ? '1' : '2';
......
...@@ -14,7 +14,7 @@ void main() { ...@@ -14,7 +14,7 @@ void main() {
new WidgetsApp( new WidgetsApp(
color: const Color(0xFFFFFFFF), color: const Color(0xFFFFFFFF),
onGenerateRoute: (RouteSettings settings) { onGenerateRoute: (RouteSettings settings) {
return new CupertinoPageRoute<Null>( return new CupertinoPageRoute<void>(
settings: settings, settings: settings,
builder: (BuildContext context) { builder: (BuildContext context) {
return const CupertinoPageScaffold( return const CupertinoPageScaffold(
...@@ -40,7 +40,7 @@ void main() { ...@@ -40,7 +40,7 @@ void main() {
new WidgetsApp( new WidgetsApp(
color: const Color(0xFFFFFFFF), color: const Color(0xFFFFFFFF),
onGenerateRoute: (RouteSettings settings) { onGenerateRoute: (RouteSettings settings) {
return new CupertinoPageRoute<Null>( return new CupertinoPageRoute<void>(
settings: settings, settings: settings,
builder: (BuildContext context) { builder: (BuildContext context) {
return new CupertinoTabScaffold( return new CupertinoTabScaffold(
...@@ -85,7 +85,7 @@ void main() { ...@@ -85,7 +85,7 @@ void main() {
new WidgetsApp( new WidgetsApp(
color: const Color(0xFFFFFFFF), color: const Color(0xFFFFFFFF),
onGenerateRoute: (RouteSettings settings) { onGenerateRoute: (RouteSettings settings) {
return new CupertinoPageRoute<Null>( return new CupertinoPageRoute<void>(
settings: settings, settings: settings,
builder: (BuildContext context) { builder: (BuildContext context) {
return new MediaQuery( return new MediaQuery(
...@@ -145,7 +145,7 @@ void main() { ...@@ -145,7 +145,7 @@ void main() {
new WidgetsApp( new WidgetsApp(
color: const Color(0xFFFFFFFF), color: const Color(0xFFFFFFFF),
onGenerateRoute: (RouteSettings settings) { onGenerateRoute: (RouteSettings settings) {
return new CupertinoPageRoute<Null>( return new CupertinoPageRoute<void>(
settings: settings, settings: settings,
builder: (BuildContext context) { builder: (BuildContext context) {
return new CupertinoTabScaffold( return new CupertinoTabScaffold(
...@@ -175,7 +175,7 @@ void main() { ...@@ -175,7 +175,7 @@ void main() {
child: const Text('Next'), child: const Text('Next'),
onPressed: () { onPressed: () {
Navigator.of(context).push( Navigator.of(context).push(
new CupertinoPageRoute<Null>( new CupertinoPageRoute<void>(
builder: (BuildContext context) { builder: (BuildContext context) {
return new CupertinoPageScaffold( return new CupertinoPageScaffold(
navigationBar: new CupertinoNavigationBar( navigationBar: new CupertinoNavigationBar(
...@@ -260,7 +260,7 @@ void main() { ...@@ -260,7 +260,7 @@ void main() {
new WidgetsApp( new WidgetsApp(
color: const Color(0xFFFFFFFF), color: const Color(0xFFFFFFFF),
onGenerateRoute: (RouteSettings settings) { onGenerateRoute: (RouteSettings settings) {
return new CupertinoPageRoute<Null>( return new CupertinoPageRoute<void>(
settings: settings, settings: settings,
builder: (BuildContext context) { builder: (BuildContext context) {
return const CupertinoPageScaffold( return const CupertinoPageScaffold(
...@@ -284,7 +284,7 @@ void main() { ...@@ -284,7 +284,7 @@ void main() {
new WidgetsApp( new WidgetsApp(
color: const Color(0xFFFFFFFF), color: const Color(0xFFFFFFFF),
onGenerateRoute: (RouteSettings settings) { onGenerateRoute: (RouteSettings settings) {
return new CupertinoPageRoute<Null>( return new CupertinoPageRoute<void>(
settings: settings, settings: settings,
builder: (BuildContext context) { builder: (BuildContext context) {
return const CupertinoPageScaffold( return const CupertinoPageScaffold(
......
...@@ -23,7 +23,7 @@ void main() { ...@@ -23,7 +23,7 @@ void main() {
new WidgetsApp( new WidgetsApp(
color: const Color(0xFFFFFFFF), color: const Color(0xFFFFFFFF),
onGenerateRoute: (RouteSettings settings) { onGenerateRoute: (RouteSettings settings) {
return new CupertinoPageRoute<Null>( return new CupertinoPageRoute<void>(
settings: settings, settings: settings,
builder: (BuildContext context) { builder: (BuildContext context) {
return new CupertinoTabScaffold( return new CupertinoTabScaffold(
...@@ -85,7 +85,7 @@ void main() { ...@@ -85,7 +85,7 @@ void main() {
new WidgetsApp( new WidgetsApp(
color: const Color(0xFFFFFFFF), color: const Color(0xFFFFFFFF),
onGenerateRoute: (RouteSettings settings) { onGenerateRoute: (RouteSettings settings) {
return new CupertinoPageRoute<Null>( return new CupertinoPageRoute<void>(
settings: settings, settings: settings,
builder: (BuildContext context) { builder: (BuildContext context) {
return new CupertinoTabScaffold( return new CupertinoTabScaffold(
...@@ -129,7 +129,7 @@ void main() { ...@@ -129,7 +129,7 @@ void main() {
new WidgetsApp( new WidgetsApp(
color: const Color(0xFFFFFFFF), color: const Color(0xFFFFFFFF),
onGenerateRoute: (RouteSettings settings) { onGenerateRoute: (RouteSettings settings) {
return new CupertinoPageRoute<Null>( return new CupertinoPageRoute<void>(
settings: settings, settings: settings,
builder: (BuildContext context) { builder: (BuildContext context) {
return new CupertinoTabScaffold( return new CupertinoTabScaffold(
...@@ -171,7 +171,7 @@ void main() { ...@@ -171,7 +171,7 @@ void main() {
new WidgetsApp( new WidgetsApp(
color: const Color(0xFFFFFFFF), color: const Color(0xFFFFFFFF),
onGenerateRoute: (RouteSettings settings) { onGenerateRoute: (RouteSettings settings) {
return new CupertinoPageRoute<Null>( return new CupertinoPageRoute<void>(
settings: settings, settings: settings,
builder: (BuildContext context) { builder: (BuildContext context) {
return new Material( return new Material(
......
...@@ -11,7 +11,7 @@ void main() { ...@@ -11,7 +11,7 @@ void main() {
new WidgetsApp( new WidgetsApp(
color: const Color(0xFFFFFFFF), color: const Color(0xFFFFFFFF),
onGenerateRoute: (RouteSettings settings) { onGenerateRoute: (RouteSettings settings) {
return new CupertinoPageRoute<Null>( return new CupertinoPageRoute<void>(
settings: settings, settings: settings,
builder: (BuildContext context) { builder: (BuildContext context) {
return new CupertinoTabView( return new CupertinoTabView(
...@@ -31,7 +31,7 @@ void main() { ...@@ -31,7 +31,7 @@ void main() {
new WidgetsApp( new WidgetsApp(
color: const Color(0xFFFFFFFF), color: const Color(0xFFFFFFFF),
onGenerateRoute: (RouteSettings settings) { onGenerateRoute: (RouteSettings settings) {
return new CupertinoPageRoute<Null>( return new CupertinoPageRoute<void>(
settings: settings, settings: settings,
builder: (BuildContext context) { builder: (BuildContext context) {
return new CupertinoTabView( return new CupertinoTabView(
...@@ -53,7 +53,7 @@ void main() { ...@@ -53,7 +53,7 @@ void main() {
new WidgetsApp( new WidgetsApp(
color: const Color(0xFFFFFFFF), color: const Color(0xFFFFFFFF),
onGenerateRoute: (RouteSettings settings) { onGenerateRoute: (RouteSettings settings) {
return new CupertinoPageRoute<Null>( return new CupertinoPageRoute<void>(
settings: settings, settings: settings,
builder: (BuildContext context) { builder: (BuildContext context) {
return new CupertinoTabView( return new CupertinoTabView(
...@@ -87,13 +87,13 @@ void main() { ...@@ -87,13 +87,13 @@ void main() {
new WidgetsApp( new WidgetsApp(
color: const Color(0xFFFFFFFF), color: const Color(0xFFFFFFFF),
onGenerateRoute: (RouteSettings settings) { onGenerateRoute: (RouteSettings settings) {
return new CupertinoPageRoute<Null>( return new CupertinoPageRoute<void>(
settings: settings, settings: settings,
builder: (BuildContext context) { builder: (BuildContext context) {
return new CupertinoTabView( return new CupertinoTabView(
onGenerateRoute: (RouteSettings settings) { onGenerateRoute: (RouteSettings settings) {
if (settings.name == Navigator.defaultRouteName) { if (settings.name == Navigator.defaultRouteName) {
return new CupertinoPageRoute<Null>( return new CupertinoPageRoute<void>(
settings: settings, settings: settings,
builder: (BuildContext context) { builder: (BuildContext context) {
return const Text('generated home'); return const Text('generated home');
...@@ -117,7 +117,7 @@ void main() { ...@@ -117,7 +117,7 @@ void main() {
new WidgetsApp( new WidgetsApp(
color: const Color(0xFFFFFFFF), color: const Color(0xFFFFFFFF),
onGenerateRoute: (RouteSettings settings) { onGenerateRoute: (RouteSettings settings) {
return new CupertinoPageRoute<Null>( return new CupertinoPageRoute<void>(
settings: settings, settings: settings,
builder: (BuildContext context) { builder: (BuildContext context) {
return new CupertinoTabView( return new CupertinoTabView(
......
...@@ -87,7 +87,7 @@ void main() { ...@@ -87,7 +87,7 @@ void main() {
expect(state2.marker, equals('original')); 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; int buildCounter = 0;
await tester.pumpWidget( await tester.pumpWidget(
new MaterialApp( new MaterialApp(
...@@ -128,10 +128,82 @@ void main() { ...@@ -128,10 +128,82 @@ void main() {
await tester.pump(const Duration(milliseconds: 10)); await tester.pump(const Duration(milliseconds: 10));
expect(buildCounter, 1); expect(buildCounter, 1);
await tester.pump(const Duration(seconds: 1)); await tester.pump(const Duration(seconds: 1));
expect(buildCounter, 2); expect(buildCounter, 1);
expect(find.text('Y'), findsOneWidget); 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<String, WidgetBuilder> routes = <String, WidgetBuilder>{
'/': (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 { testWidgets('Cannot pop the initial route', (WidgetTester tester) async {
await tester.pumpWidget(new MaterialApp(home: const Text('Home'))); await tester.pumpWidget(new MaterialApp(home: const Text('Home')));
......
...@@ -21,7 +21,7 @@ void main() { ...@@ -21,7 +21,7 @@ void main() {
child: new RaisedButton( child: new RaisedButton(
child: const Text('X'), child: const Text('X'),
onPressed: () { onPressed: () {
showDialog<Null>( showDialog<void>(
context: context, context: context,
builder: (BuildContext context) { builder: (BuildContext context) {
return new AlertDialog( return new AlertDialog(
...@@ -71,7 +71,7 @@ void main() { ...@@ -71,7 +71,7 @@ void main() {
child: new RaisedButton( child: new RaisedButton(
child: const Text('X'), child: const Text('X'),
onPressed: () { onPressed: () {
showDialog<Null>( showDialog<void>(
context: context, context: context,
builder: (BuildContext context) { builder: (BuildContext context) {
return const AlertDialog( return const AlertDialog(
...@@ -118,7 +118,7 @@ void main() { ...@@ -118,7 +118,7 @@ void main() {
final BuildContext context = tester.element(find.text('Go')); final BuildContext context = tester.element(find.text('Go'));
final Future<int> result = showDialog( final Future<int> result = showDialog<int>(
context: context, context: context,
builder: (BuildContext context) { builder: (BuildContext context) {
return new SimpleDialog( return new SimpleDialog(
...@@ -161,7 +161,7 @@ void main() { ...@@ -161,7 +161,7 @@ void main() {
final BuildContext context = tester.element(find.text('Go')); final BuildContext context = tester.element(find.text('Go'));
showDialog<Null>( showDialog<void>(
context: context, context: context,
builder: (BuildContext context) { builder: (BuildContext context) {
return new Container( return new Container(
...@@ -182,7 +182,7 @@ void main() { ...@@ -182,7 +182,7 @@ void main() {
await tester.pumpAndSettle(const Duration(seconds: 1)); await tester.pumpAndSettle(const Duration(seconds: 1));
expect(find.text('Dialog1'), findsNothing); expect(find.text('Dialog1'), findsNothing);
showDialog<Null>( showDialog<void>(
context: context, context: context,
barrierDismissible: false, barrierDismissible: false,
builder: (BuildContext context) { builder: (BuildContext context) {
...@@ -227,7 +227,7 @@ void main() { ...@@ -227,7 +227,7 @@ void main() {
final BuildContext context = tester.element(find.text(buttonText)); final BuildContext context = tester.element(find.text(buttonText));
const String alertText = 'A button in an overlay alert'; const String alertText = 'A button in an overlay alert';
showDialog<Null>( showDialog<void>(
context: context, context: context,
builder: (BuildContext context) { builder: (BuildContext context) {
return const AlertDialog(title: const Text(alertText)); return const AlertDialog(title: const Text(alertText));
...@@ -260,7 +260,7 @@ void main() { ...@@ -260,7 +260,7 @@ void main() {
), ),
child: new Navigator( child: new Navigator(
onGenerateRoute: (_) { onGenerateRoute: (_) {
return new PageRouteBuilder<Null>( return new PageRouteBuilder<void>(
pageBuilder: (BuildContext context, Animation<double> animation, Animation<double> secondaryAnimation) { pageBuilder: (BuildContext context, Animation<double> animation, Animation<double> secondaryAnimation) {
outerContext = context; outerContext = context;
return new Container(); return new Container();
...@@ -271,7 +271,7 @@ void main() { ...@@ -271,7 +271,7 @@ void main() {
), ),
)); ));
showDialog<Null>( showDialog<void>(
context: outerContext, context: outerContext,
barrierDismissible: false, barrierDismissible: false,
builder: (BuildContext context) { builder: (BuildContext context) {
......
...@@ -76,7 +76,7 @@ class _TestAppState extends State<TestApp> { ...@@ -76,7 +76,7 @@ class _TestAppState extends State<TestApp> {
child: new Navigator( child: new Navigator(
onGenerateRoute: (RouteSettings settings) { onGenerateRoute: (RouteSettings settings) {
assert(settings.name == '/'); assert(settings.name == '/');
return new MaterialPageRoute<dynamic>( return new MaterialPageRoute<void>(
settings: settings, settings: settings,
builder: (BuildContext context) => widget.child, builder: (BuildContext context) => widget.child,
); );
...@@ -159,7 +159,7 @@ void main() { ...@@ -159,7 +159,7 @@ void main() {
child: new Navigator( child: new Navigator(
initialRoute: '/', initialRoute: '/',
onGenerateRoute: (RouteSettings settings) { onGenerateRoute: (RouteSettings settings) {
return new MaterialPageRoute<Null>( return new MaterialPageRoute<void>(
settings: settings, settings: settings,
builder: (BuildContext context) { builder: (BuildContext context) {
return new Material( return new Material(
......
...@@ -84,7 +84,7 @@ void main() { ...@@ -84,7 +84,7 @@ void main() {
), ),
), ),
); );
Navigator.push(theContext, new PageRouteBuilder<Null>( Navigator.push(theContext, new PageRouteBuilder<void>(
pageBuilder: (BuildContext context, Animation<double> animation, Animation<double> secondaryAnimation) { pageBuilder: (BuildContext context, Animation<double> animation, Animation<double> secondaryAnimation) {
return const Placeholder(); return const Placeholder();
}, },
...@@ -107,7 +107,7 @@ void main() { ...@@ -107,7 +107,7 @@ void main() {
), ),
), ),
); );
Navigator.push(theContext, new PageRouteBuilder<Null>( Navigator.push(theContext, new PageRouteBuilder<void>(
pageBuilder: (BuildContext context, Animation<double> animation, Animation<double> secondaryAnimation) { pageBuilder: (BuildContext context, Animation<double> animation, Animation<double> secondaryAnimation) {
return const Placeholder(); return const Placeholder();
}, },
...@@ -131,7 +131,7 @@ void main() { ...@@ -131,7 +131,7 @@ void main() {
), ),
), ),
); );
Navigator.push(theContext, new PageRouteBuilder<Null>( Navigator.push(theContext, new PageRouteBuilder<void>(
pageBuilder: (BuildContext context, Animation<double> animation, Animation<double> secondaryAnimation) { pageBuilder: (BuildContext context, Animation<double> animation, Animation<double> secondaryAnimation) {
return const Placeholder(); return const Placeholder();
}, },
......
...@@ -27,7 +27,7 @@ void main() { ...@@ -27,7 +27,7 @@ void main() {
showModalBottomSheet<Null>( showModalBottomSheet<Null>(
context: savedContext, context: savedContext,
builder: (BuildContext context) => const Text('BottomSheet') builder: (BuildContext context) => const Text('BottomSheet')
).then<Null>((Null result) { ).then<void>((Null result) {
expectSync(result, isNull); expectSync(result, isNull);
showBottomSheetThenCalled = true; showBottomSheetThenCalled = true;
}); });
...@@ -49,7 +49,7 @@ void main() { ...@@ -49,7 +49,7 @@ void main() {
showModalBottomSheet<Null>( showModalBottomSheet<Null>(
context: savedContext, context: savedContext,
builder: (BuildContext context) => const Text('BottomSheet'), builder: (BuildContext context) => const Text('BottomSheet'),
).then<Null>((Null result) { ).then<void>((Null result) {
expectSync(result, isNull); expectSync(result, isNull);
showBottomSheetThenCalled = true; showBottomSheetThenCalled = true;
}); });
...@@ -172,7 +172,7 @@ void main() { ...@@ -172,7 +172,7 @@ void main() {
), ),
child: new Navigator( child: new Navigator(
onGenerateRoute: (_) { onGenerateRoute: (_) {
return new PageRouteBuilder<Null>( return new PageRouteBuilder<void>(
pageBuilder: (BuildContext context, Animation<double> animation, Animation<double> secondaryAnimation) { pageBuilder: (BuildContext context, Animation<double> animation, Animation<double> secondaryAnimation) {
outerContext = context; outerContext = context;
return new Container(); return new Container();
...@@ -184,7 +184,7 @@ void main() { ...@@ -184,7 +184,7 @@ void main() {
), ),
)); ));
showModalBottomSheet<Null>( showModalBottomSheet<void>(
context: outerContext, context: outerContext,
builder: (BuildContext context) { builder: (BuildContext context) {
innerContext = context; innerContext = context;
......
...@@ -156,7 +156,7 @@ void main() { ...@@ -156,7 +156,7 @@ void main() {
final Offset widget1InitialTopLeft = tester.getTopLeft(find.text('Page 1')); final Offset widget1InitialTopLeft = tester.getTopLeft(find.text('Page 1'));
tester.state<NavigatorState>(find.byType(Navigator)).push(new MaterialPageRoute<Null>( tester.state<NavigatorState>(find.byType(Navigator)).push(new MaterialPageRoute<void>(
builder: (BuildContext context) { builder: (BuildContext context) {
return const Material(child: const Text('Page 2')); return const Material(child: const Text('Page 2'));
}, },
...@@ -355,7 +355,7 @@ void main() { ...@@ -355,7 +355,7 @@ void main() {
) )
); );
tester.state<NavigatorState>(find.byType(Navigator)).push(new MaterialPageRoute<Null>( tester.state<NavigatorState>(find.byType(Navigator)).push(new MaterialPageRoute<void>(
builder: (BuildContext context) { builder: (BuildContext context) {
return const Scaffold(body: const Text('Page 2')); return const Scaffold(body: const Text('Page 2'));
}, },
......
...@@ -453,7 +453,7 @@ class _TestAppState extends State<TestApp> { ...@@ -453,7 +453,7 @@ class _TestAppState extends State<TestApp> {
child: new Navigator( child: new Navigator(
onGenerateRoute: (RouteSettings settings) { onGenerateRoute: (RouteSettings settings) {
assert(settings.name == '/'); assert(settings.name == '/');
return new MaterialPageRoute<dynamic>( return new MaterialPageRoute<void>(
settings: settings, settings: settings,
builder: (BuildContext context) => new Material( builder: (BuildContext context) => new Material(
child: widget.child, child: widget.child,
......
...@@ -419,7 +419,7 @@ void main() { ...@@ -419,7 +419,7 @@ void main() {
) )
); );
tester.state<NavigatorState>(find.byType(Navigator)).push(new MaterialPageRoute<Null>( tester.state<NavigatorState>(find.byType(Navigator)).push(new MaterialPageRoute<void>(
builder: (BuildContext context) { builder: (BuildContext context) {
return new Scaffold(appBar: new AppBar(), body: const Text('Page 2')); return new Scaffold(appBar: new AppBar(), body: const Text('Page 2'));
}, },
......
...@@ -108,7 +108,7 @@ void main() { ...@@ -108,7 +108,7 @@ void main() {
final BuildContext context = tester.element(find.byType(TextField)); final BuildContext context = tester.element(find.byType(TextField));
showDialog<Null>( showDialog<void>(
context: context, context: context,
builder: (BuildContext context) => const SimpleDialog(title: const Text('Dialog')), builder: (BuildContext context) => const SimpleDialog(title: const Text('Dialog')),
); );
......
...@@ -161,7 +161,7 @@ void main() { ...@@ -161,7 +161,7 @@ void main() {
builder: (BuildContext context) { builder: (BuildContext context) {
return new RaisedButton( return new RaisedButton(
onPressed: () { onPressed: () {
showModalBottomSheet<Null>( showModalBottomSheet<void>(
context: context, context: context,
builder: (BuildContext context) => const Text('bottomSheet'), builder: (BuildContext context) => const Text('bottomSheet'),
); );
...@@ -198,7 +198,7 @@ void main() { ...@@ -198,7 +198,7 @@ void main() {
builder: (BuildContext context) { builder: (BuildContext context) {
return new RaisedButton( return new RaisedButton(
onPressed: () { onPressed: () {
showDialog<Null>( showDialog<void>(
context: context, context: context,
builder: (BuildContext context) => const Text('dialog'), builder: (BuildContext context) => const Text('dialog'),
); );
...@@ -230,7 +230,7 @@ void main() { ...@@ -230,7 +230,7 @@ void main() {
builder: (BuildContext context) { builder: (BuildContext context) {
return new GestureDetector( return new GestureDetector(
onTap: () { onTap: () {
showDialog<Null>( showDialog<void>(
context: context, context: context,
builder: (BuildContext context) { builder: (BuildContext context) {
return const Scaffold( return const Scaffold(
......
...@@ -240,7 +240,7 @@ void _tests() { ...@@ -240,7 +240,7 @@ void _tests() {
textDirection: TextDirection.ltr, textDirection: TextDirection.ltr,
child: new Navigator( child: new Navigator(
onGenerateRoute: (RouteSettings settings) { onGenerateRoute: (RouteSettings settings) {
return new MaterialPageRoute<dynamic>(builder: (BuildContext context) { return new MaterialPageRoute<void>(builder: (BuildContext context) {
return new FlatButton( return new FlatButton(
onPressed: () { onPressed: () {
showTimePicker(context: context, initialTime: initialTime); showTimePicker(context: context, initialTime: initialTime);
......
...@@ -543,7 +543,7 @@ void main() { ...@@ -543,7 +543,7 @@ void main() {
textDirection: TextDirection.ltr, textDirection: TextDirection.ltr,
child: new Navigator( child: new Navigator(
onGenerateRoute: (RouteSettings settings) { onGenerateRoute: (RouteSettings settings) {
return new MaterialPageRoute<dynamic>( return new MaterialPageRoute<void>(
builder: (BuildContext context) { builder: (BuildContext context) {
return new Center( return new Center(
child: new Tooltip( child: new Tooltip(
......
...@@ -13,7 +13,7 @@ class SamplePage extends StatefulWidget { ...@@ -13,7 +13,7 @@ class SamplePage extends StatefulWidget {
} }
class SamplePageState extends State<SamplePage> { class SamplePageState extends State<SamplePage> {
ModalRoute<Null> _route; ModalRoute<void> _route;
Future<bool> _callback() async => willPopValue; Future<bool> _callback() async => willPopValue;
...@@ -66,7 +66,7 @@ class SampleForm extends StatelessWidget { ...@@ -66,7 +66,7 @@ class SampleForm extends StatelessWidget {
// Expose the protected hasScopedWillPopCallback getter // Expose the protected hasScopedWillPopCallback getter
class TestPageRoute<T> extends MaterialPageRoute<T> { class TestPageRoute<T> extends MaterialPageRoute<T> {
TestPageRoute({ WidgetBuilder builder }) TestPageRoute({ WidgetBuilder builder })
: super(builder: builder, maintainState: true, settings: const RouteSettings()); : super(builder: builder, maintainState: true);
bool get hasCallback => super.hasScopedWillPopCallback; bool get hasCallback => super.hasScopedWillPopCallback;
} }
...@@ -84,7 +84,7 @@ void main() { ...@@ -84,7 +84,7 @@ void main() {
child: new FlatButton( child: new FlatButton(
child: const Text('X'), child: const Text('X'),
onPressed: () { onPressed: () {
showDialog<Null>( showDialog<void>(
context: context, context: context,
builder: (BuildContext context) => new SamplePage(), builder: (BuildContext context) => new SamplePage(),
); );
...@@ -138,7 +138,7 @@ void main() { ...@@ -138,7 +138,7 @@ void main() {
child: new FlatButton( child: new FlatButton(
child: const Text('X'), child: const Text('X'),
onPressed: () { onPressed: () {
Navigator.of(context).push(new MaterialPageRoute<Null>( Navigator.of(context).push(new MaterialPageRoute<void>(
builder: (BuildContext context) { builder: (BuildContext context) {
return new SampleForm( return new SampleForm(
callback: () => new Future<bool>.value(willPopValue), callback: () => new Future<bool>.value(willPopValue),
...@@ -212,7 +212,7 @@ void main() { ...@@ -212,7 +212,7 @@ void main() {
child: new FlatButton( child: new FlatButton(
child: const Text('X'), child: const Text('X'),
onPressed: () { onPressed: () {
Navigator.of(context).push(new MaterialPageRoute<Null>( Navigator.of(context).push(new MaterialPageRoute<void>(
builder: (BuildContext context) { builder: (BuildContext context) {
return new SampleForm( return new SampleForm(
callback: () => showYesNoAlert(context), callback: () => showYesNoAlert(context),
......
...@@ -5,7 +5,7 @@ ...@@ -5,7 +5,7 @@
import 'package:flutter_test/flutter_test.dart'; import 'package:flutter_test/flutter_test.dart';
import 'package:flutter/widgets.dart'; import 'package:flutter/widgets.dart';
final RouteFactory generateRoute = (RouteSettings settings) => new PageRouteBuilder<Null>( final RouteFactory generateRoute = (RouteSettings settings) => new PageRouteBuilder<void>(
settings: settings, settings: settings,
pageBuilder: (BuildContext context, Animation<double> animation1, Animation<double> animation2) { pageBuilder: (BuildContext context, Animation<double> animation1, Animation<double> animation2) {
return const Placeholder(); return const Placeholder();
......
...@@ -19,7 +19,7 @@ Future<Null> pumpApp(WidgetTester tester, { GenerateAppTitle onGenerateTitle }) ...@@ -19,7 +19,7 @@ Future<Null> pumpApp(WidgetTester tester, { GenerateAppTitle onGenerateTitle })
color: kTitleColor, color: kTitleColor,
onGenerateTitle: onGenerateTitle, onGenerateTitle: onGenerateTitle,
onGenerateRoute: (RouteSettings settings) { onGenerateRoute: (RouteSettings settings) {
return new PageRouteBuilder<Null>( return new PageRouteBuilder<void>(
pageBuilder: (BuildContext context, Animation<double> animation, Animation<double> secondaryAnimation) { pageBuilder: (BuildContext context, Animation<double> animation, Animation<double> secondaryAnimation) {
return new Container(); return new Container();
} }
......
...@@ -81,7 +81,7 @@ final Map<String, WidgetBuilder> routes = <String, WidgetBuilder>{ ...@@ -81,7 +81,7 @@ final Map<String, WidgetBuilder> routes = <String, WidgetBuilder>{
}; };
class ThreeRoute extends MaterialPageRoute<Null> { class ThreeRoute extends MaterialPageRoute<void> {
ThreeRoute() : super(builder: (BuildContext context) { ThreeRoute() : super(builder: (BuildContext context) {
return new Material( return new Material(
key: routeThreeKey, key: routeThreeKey,
...@@ -96,7 +96,7 @@ class ThreeRoute extends MaterialPageRoute<Null> { ...@@ -96,7 +96,7 @@ class ThreeRoute extends MaterialPageRoute<Null> {
}); });
} }
class MutatingRoute extends MaterialPageRoute<Null> { class MutatingRoute extends MaterialPageRoute<void> {
MutatingRoute() : super(builder: (BuildContext context) { MutatingRoute() : super(builder: (BuildContext context) {
return new Hero(tag: 'a', child: const Text('MutatingRoute'), key: new UniqueKey()); return new Hero(tag: 'a', child: const Text('MutatingRoute'), key: new UniqueKey());
}); });
...@@ -363,7 +363,7 @@ void main() { ...@@ -363,7 +363,7 @@ void main() {
testWidgets('Popping on first frame does not cause hero observer to crash', (WidgetTester tester) async { testWidgets('Popping on first frame does not cause hero observer to crash', (WidgetTester tester) async {
await tester.pumpWidget(new MaterialApp( await tester.pumpWidget(new MaterialApp(
onGenerateRoute: (RouteSettings settings) { onGenerateRoute: (RouteSettings settings) {
return new MaterialPageRoute<Null>( return new MaterialPageRoute<void>(
settings: settings, settings: settings,
builder: (BuildContext context) => new Hero(tag: 'test', child: new Container()), builder: (BuildContext context) => new Hero(tag: 'test', child: new Container()),
); );
...@@ -389,7 +389,7 @@ void main() { ...@@ -389,7 +389,7 @@ void main() {
testWidgets('Overlapping starting and ending a hero transition works ok', (WidgetTester tester) async { testWidgets('Overlapping starting and ending a hero transition works ok', (WidgetTester tester) async {
await tester.pumpWidget(new MaterialApp( await tester.pumpWidget(new MaterialApp(
onGenerateRoute: (RouteSettings settings) { onGenerateRoute: (RouteSettings settings) {
return new MaterialPageRoute<Null>( return new MaterialPageRoute<void>(
settings: settings, settings: settings,
builder: (BuildContext context) => new Hero(tag: 'test', child: new Container()), builder: (BuildContext context) => new Hero(tag: 'test', child: new Container()),
); );
...@@ -432,7 +432,7 @@ void main() { ...@@ -432,7 +432,7 @@ void main() {
return new FlatButton( return new FlatButton(
child: const Text('push'), child: const Text('push'),
onPressed: () { onPressed: () {
Navigator.push(context, new PageRouteBuilder<Null>( Navigator.push(context, new PageRouteBuilder<void>(
pageBuilder: (BuildContext context, Animation<double> _, Animation<double> __) { pageBuilder: (BuildContext context, Animation<double> _, Animation<double> __) {
return const Text('fail'); return const Text('fail');
}, },
...@@ -569,9 +569,9 @@ void main() { ...@@ -569,9 +569,9 @@ void main() {
// After flying in the opposite direction for 50ms Hero 'a' will // After flying in the opposite direction for 50ms Hero 'a' will
// be smaller than it was, but bigger than its initial size. // be smaller than it was, but bigger than its initial size.
await tester.pump(const Duration(milliseconds: 50)); await tester.pump(const Duration(milliseconds: 50));
final double height100ms = tester.getSize(find.byKey(firstKey)).height; final double height200ms = tester.getSize(find.byKey(firstKey)).height;
expect(height100ms, greaterThan(height150ms)); expect(height200ms, greaterThan(height150ms));
expect(finalHeight, lessThan(height100ms)); expect(finalHeight, lessThan(height200ms));
// Hero a's return flight at 149ms. The outgoing (push) flight took // 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. // 150ms so we should be just about back to where Hero 'a' started.
...@@ -593,7 +593,7 @@ void main() { ...@@ -593,7 +593,7 @@ void main() {
StateSetter heroCardSetState; StateSetter heroCardSetState;
// Show a 200x200 Hero tagged 'H', with key routeHeroKey // Show a 200x200 Hero tagged 'H', with key routeHeroKey
final MaterialPageRoute<Null> route = new MaterialPageRoute<Null>( final MaterialPageRoute<void> route = new MaterialPageRoute<void>(
builder: (BuildContext context) { builder: (BuildContext context) {
return new Material( return new Material(
child: new ListView( child: new ListView(
...@@ -695,7 +695,7 @@ void main() { ...@@ -695,7 +695,7 @@ void main() {
const Key routeContainerKey = const Key('route hero container'); const Key routeContainerKey = const Key('route hero container');
// Show a 200x200 Hero tagged 'H', with key routeHeroKey // Show a 200x200 Hero tagged 'H', with key routeHeroKey
final MaterialPageRoute<Null> route = new MaterialPageRoute<Null>( final MaterialPageRoute<void> route = new MaterialPageRoute<void>(
builder: (BuildContext context) { builder: (BuildContext context) {
return new Material( return new Material(
child: new ListView( child: new ListView(
...@@ -777,7 +777,7 @@ void main() { ...@@ -777,7 +777,7 @@ void main() {
const Key routeContainerKey = const Key('route hero container'); const Key routeContainerKey = const Key('route hero container');
// Show a 200x200 Hero tagged 'H', with key routeHeroKey // Show a 200x200 Hero tagged 'H', with key routeHeroKey
final MaterialPageRoute<Null> route = new MaterialPageRoute<Null>( final MaterialPageRoute<void> route = new MaterialPageRoute<void>(
builder: (BuildContext context) { builder: (BuildContext context) {
return new Material( return new Material(
child: new ListView( child: new ListView(
...@@ -855,7 +855,7 @@ void main() { ...@@ -855,7 +855,7 @@ void main() {
const Key heroBCKey = const Key('BC hero'); const Key heroBCKey = const Key('BC hero');
// Show a 150x150 Hero tagged 'BC' // Show a 150x150 Hero tagged 'BC'
final MaterialPageRoute<Null> routeC = new MaterialPageRoute<Null>( final MaterialPageRoute<void> routeC = new MaterialPageRoute<void>(
builder: (BuildContext context) { builder: (BuildContext context) {
return new Material( return new Material(
child: new ListView( child: new ListView(
...@@ -872,7 +872,7 @@ void main() { ...@@ -872,7 +872,7 @@ void main() {
); );
// Show a height=200 Hero tagged 'AB' and a height=50 Hero tagged 'BC' // Show a height=200 Hero tagged 'AB' and a height=50 Hero tagged 'BC'
final MaterialPageRoute<Null> routeB = new MaterialPageRoute<Null>( final MaterialPageRoute<void> routeB = new MaterialPageRoute<void>(
builder: (BuildContext context) { builder: (BuildContext context) {
return new Material( return new Material(
child: new ListView( child: new ListView(
...@@ -956,7 +956,7 @@ void main() { ...@@ -956,7 +956,7 @@ void main() {
}); });
testWidgets('Stateful hero child state survives flight', (WidgetTester tester) async { testWidgets('Stateful hero child state survives flight', (WidgetTester tester) async {
final MaterialPageRoute<Null> route = new MaterialPageRoute<Null>( final MaterialPageRoute<void> route = new MaterialPageRoute<void>(
builder: (BuildContext context) { builder: (BuildContext context) {
return new Material( return new Material(
child: new ListView( child: new ListView(
......
...@@ -91,6 +91,7 @@ class TestObserver extends NavigatorObserver { ...@@ -91,6 +91,7 @@ class TestObserver extends NavigatorObserver {
OnObservation onPushed; OnObservation onPushed;
OnObservation onPopped; OnObservation onPopped;
OnObservation onRemoved; OnObservation onRemoved;
OnObservation onReplaced;
@override @override
void didPush(Route<dynamic> route, Route<dynamic> previousRoute) { void didPush(Route<dynamic> route, Route<dynamic> previousRoute) {
...@@ -111,6 +112,12 @@ class TestObserver extends NavigatorObserver { ...@@ -111,6 +112,12 @@ class TestObserver extends NavigatorObserver {
if (onRemoved != null) if (onRemoved != null)
onRemoved(route, previousRoute); onRemoved(route, previousRoute);
} }
@override
void didReplace({ Route<dynamic> oldRoute, Route<dynamic> newRoute }) {
if (onReplaced != null)
onReplaced(newRoute, oldRoute);
}
} }
void main() { void main() {
...@@ -193,19 +200,19 @@ void main() { ...@@ -193,19 +200,19 @@ void main() {
child: new Navigator( child: new Navigator(
onGenerateRoute: (RouteSettings settings) { onGenerateRoute: (RouteSettings settings) {
if (settings.isInitialRoute) { if (settings.isInitialRoute) {
return new MaterialPageRoute<Null>( return new MaterialPageRoute<void>(
builder: (BuildContext context) { builder: (BuildContext context) {
return new RaisedButton( return new RaisedButton(
child: const Text('Next'), child: const Text('Next'),
onPressed: () { onPressed: () {
Navigator.of(context).push( Navigator.of(context).push(
new MaterialPageRoute<Null>( new MaterialPageRoute<void>(
builder: (BuildContext context) { builder: (BuildContext context) {
return new RaisedButton( return new RaisedButton(
child: const Text('Inner page'), child: const Text('Inner page'),
onPressed: () { onPressed: () {
Navigator.of(context, rootNavigator: true).push( Navigator.of(context, rootNavigator: true).push(
new MaterialPageRoute<Null>( new MaterialPageRoute<void>(
builder: (BuildContext context) { builder: (BuildContext context) {
return const Text('Dialog'); return const Text('Dialog');
} }
...@@ -608,7 +615,7 @@ void main() { ...@@ -608,7 +615,7 @@ void main() {
testWidgets('remove a route whose value is awaited', (WidgetTester tester) async { testWidgets('remove a route whose value is awaited', (WidgetTester tester) async {
Future<String> pageValue; Future<String> pageValue;
final Map<String, WidgetBuilder> pageBuilders = <String, WidgetBuilder>{ final Map<String, WidgetBuilder> pageBuilders = <String, WidgetBuilder>{
'/': (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'); }), '/A': (BuildContext context) => new OnTapPage(id: 'A', onTap: () { Navigator.pop(context, 'A'); }),
}; };
final Map<String, Route<String>> routes = <String, Route<String>>{}; final Map<String, Route<String>> routes = <String, Route<String>>{};
...@@ -633,5 +640,133 @@ void main() { ...@@ -633,5 +640,133 @@ void main() {
navigator.removeRoute(routes['/A']); // stack becomes /, pageValue will not complete navigator.removeRoute(routes['/A']); // stack becomes /, pageValue will not complete
}); });
testWidgets('replacing route can be observed', (WidgetTester tester) async {
final GlobalKey<NavigatorState> key = new GlobalKey<NavigatorState>();
final List<String> log = <String>[];
final TestObserver observer = new TestObserver()
..onPushed = (Route<dynamic> route, Route<dynamic> previousRoute) {
log.add('pushed ${route.settings.name} (previous is ${previousRoute == null ? "<none>" : previousRoute.settings.name})');
}
..onPopped = (Route<dynamic> route, Route<dynamic> previousRoute) {
log.add('popped ${route.settings.name} (previous is ${previousRoute == null ? "<none>" : previousRoute.settings.name})');
}
..onRemoved = (Route<dynamic> route, Route<dynamic> previousRoute) {
log.add('removed ${route.settings.name} (previous is ${previousRoute == null ? "<none>" : previousRoute.settings.name})');
}
..onReplaced = (Route<dynamic> newRoute, Route<dynamic> oldRoute) {
log.add('replaced ${oldRoute.settings.name} with ${newRoute.settings.name}');
};
Route<void> routeB;
await tester.pumpWidget(new MaterialApp(
navigatorKey: key,
navigatorObservers: <NavigatorObserver>[observer],
home: new FlatButton(
child: const Text('A'),
onPressed: () {
key.currentState.push<void>(routeB = new MaterialPageRoute<void>(
settings: const RouteSettings(name: 'B'),
builder: (BuildContext context) {
return new FlatButton(
child: const Text('B'),
onPressed: () {
key.currentState.push<void>(new MaterialPageRoute<int>(
settings: const RouteSettings(name: 'C'),
builder: (BuildContext context) {
return new FlatButton(
child: const Text('C'),
onPressed: () {
key.currentState.replace(
oldRoute: routeB,
newRoute: new MaterialPageRoute<int>(
settings: const RouteSettings(name: 'D'),
builder: (BuildContext context) {
return const Text('D');
},
),
);
},
);
},
));
},
);
},
));
},
),
));
expect(log, <String>['pushed / (previous is <none>)']);
await tester.tap(find.text('A'));
await tester.pump();
await tester.pump(const Duration(seconds: 1));
expect(log, <String>['pushed / (previous is <none>)', 'pushed B (previous is /)']);
await tester.tap(find.text('B'));
await tester.pump();
await tester.pump(const Duration(seconds: 1));
expect(log, <String>['pushed / (previous is <none>)', '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, <String>['pushed / (previous is <none>)', '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<NavigatorState> key = new GlobalKey<NavigatorState>();
final List<String> log = <String>[];
Route<void> routeB;
await tester.pumpWidget(new MaterialApp(
navigatorKey: key,
home: new FlatButton(
child: const Text('A'),
onPressed: () {
key.currentState.push<void>(routeB = new MaterialPageRoute<void>(
settings: const RouteSettings(name: 'B'),
builder: (BuildContext context) {
log.add('building B');
return new FlatButton(
child: const Text('B'),
onPressed: () {
key.currentState.push<void>(new MaterialPageRoute<int>(
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<int>(
settings: const RouteSettings(name: 'D'),
builder: (BuildContext context) {
log.add('building D');
return const Text('D');
},
),
);
},
);
},
));
},
);
},
));
},
),
));
expect(log, <String>[]);
await tester.tap(find.text('A'));
await tester.pumpAndSettle(const Duration(milliseconds: 10));
expect(log, <String>['building B']);
await tester.tap(find.text('B'));
await tester.pumpAndSettle(const Duration(milliseconds: 10));
expect(log, <String>['building B', 'building C', 'found C']);
await tester.tap(find.text('C'));
await tester.pumpAndSettle(const Duration(milliseconds: 10));
expect(log, <String>['building B', 'building C', 'found C', 'building D']);
key.currentState.pop<void>();
await tester.pumpAndSettle(const Duration(milliseconds: 10));
expect(log, <String>['building B', 'building C', 'found C', 'building D', 'building C', 'found C']);
});
} }
...@@ -86,7 +86,7 @@ void main() { ...@@ -86,7 +86,7 @@ void main() {
child: new Builder( child: new Builder(
key: insideKey, key: insideKey,
builder: (BuildContext context) { builder: (BuildContext context) {
final PageRoute<Null> route = ModalRoute.of(context); final PageRoute<void> route = ModalRoute.of(context);
return new Column( return new Column(
children: <Widget>[ children: <Widget>[
new TestTransition( new TestTransition(
......
...@@ -5,7 +5,8 @@ ...@@ -5,7 +5,8 @@
import 'package:flutter_test/flutter_test.dart'; import 'package:flutter_test/flutter_test.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
class TestOverlayRoute extends OverlayRoute<Null> { class TestOverlayRoute extends OverlayRoute<void> {
TestOverlayRoute({ RouteSettings settings }) : super(settings: settings);
@override @override
Iterable<OverlayEntry> createOverlayEntries() sync* { Iterable<OverlayEntry> createOverlayEntries() sync* {
yield new OverlayEntry(builder: _build); yield new OverlayEntry(builder: _build);
...@@ -311,7 +312,7 @@ void main() { ...@@ -311,7 +312,7 @@ void main() {
await tester.pumpWidget(new MaterialApp(routes: routes)); await tester.pumpWidget(new MaterialApp(routes: routes));
final PageRoute<Null> route = new MaterialPageRoute<Null>( final PageRoute<void> route = new MaterialPageRoute<void>(
settings: const RouteSettings(name: '/page'), settings: const RouteSettings(name: '/page'),
builder: (BuildContext context) => const Center(child: const Text('page')), builder: (BuildContext context) => const Center(child: const Text('page')),
); );
......
...@@ -37,13 +37,13 @@ Future<Null> performTest(WidgetTester tester, bool maintainState) async { ...@@ -37,13 +37,13 @@ Future<Null> performTest(WidgetTester tester, bool maintainState) async {
key: navigatorKey, key: navigatorKey,
onGenerateRoute: (RouteSettings settings) { onGenerateRoute: (RouteSettings settings) {
if (settings.name == '/') { if (settings.name == '/') {
return new MaterialPageRoute<Null>( return new MaterialPageRoute<void>(
settings: settings, settings: settings,
builder: (_) => new Container(child: const ThePositiveNumbers(from: 0)), builder: (_) => new Container(child: const ThePositiveNumbers(from: 0)),
maintainState: maintainState, maintainState: maintainState,
); );
} else if (settings.name == '/second') { } else if (settings.name == '/second') {
return new MaterialPageRoute<Null>( return new MaterialPageRoute<void>(
settings: settings, settings: settings,
builder: (_) => new Container(child: const ThePositiveNumbers(from: 10000)), builder: (_) => new Container(child: const ThePositiveNumbers(from: 10000)),
maintainState: maintainState, maintainState: maintainState,
......
...@@ -45,9 +45,11 @@ class TestRoute extends LocalHistoryRoute<String> { ...@@ -45,9 +45,11 @@ class TestRoute extends LocalHistoryRoute<String> {
} }
@override @override
void didReplace(covariant TestRoute oldRoute) { void didReplace(Route<dynamic> oldRoute) {
log('didReplace ${oldRoute.name}'); expect(oldRoute, const isInstanceOf<TestRoute>());
super.didReplace(oldRoute); final TestRoute castRoute = oldRoute;
log('didReplace ${castRoute.name}');
super.didReplace(castRoute);
} }
@override @override
...@@ -60,15 +62,19 @@ class TestRoute extends LocalHistoryRoute<String> { ...@@ -60,15 +62,19 @@ class TestRoute extends LocalHistoryRoute<String> {
} }
@override @override
void didPopNext(covariant TestRoute nextRoute) { void didPopNext(Route<dynamic> nextRoute) {
log('didPopNext ${nextRoute.name}'); expect(nextRoute, const isInstanceOf<TestRoute>());
super.didPopNext(nextRoute); final TestRoute castRoute = nextRoute;
log('didPopNext ${castRoute.name}');
super.didPopNext(castRoute);
} }
@override @override
void didChangeNext(covariant TestRoute nextRoute) { void didChangeNext(Route<dynamic> nextRoute) {
log('didChangeNext ${nextRoute?.name}'); expect(nextRoute, anyOf(isNull, const isInstanceOf<TestRoute>()));
super.didChangeNext(nextRoute); final TestRoute castRoute = nextRoute;
log('didChangeNext ${castRoute?.name}');
super.didChangeNext(castRoute);
} }
@override @override
......
...@@ -54,7 +54,7 @@ Widget buildFrame({ ...@@ -54,7 +54,7 @@ Widget buildFrame({
localizationsDelegates: delegates, localizationsDelegates: delegates,
localeResolutionCallback: localeResolutionCallback, localeResolutionCallback: localeResolutionCallback,
onGenerateRoute: (RouteSettings settings) { onGenerateRoute: (RouteSettings settings) {
return new MaterialPageRoute<Null>( return new MaterialPageRoute<void>(
builder: (BuildContext context) { builder: (BuildContext context) {
return buildContent(context); return buildContent(context);
} }
......
...@@ -145,7 +145,7 @@ void main() { ...@@ -145,7 +145,7 @@ void main() {
textDirection: TextDirection.ltr, textDirection: TextDirection.ltr,
child: new Navigator( child: new Navigator(
onGenerateRoute: (RouteSettings settings) { onGenerateRoute: (RouteSettings settings) {
return new MaterialPageRoute<dynamic>(builder: (BuildContext context) { return new MaterialPageRoute<void>(builder: (BuildContext context) {
return new FlatButton( return new FlatButton(
onPressed: () { onPressed: () {
showTimePicker(context: context, initialTime: const TimeOfDay(hour: 7, minute: 0)); showTimePicker(context: context, initialTime: const TimeOfDay(hour: 7, minute: 0));
......
...@@ -153,7 +153,7 @@ Widget buildFrame({ ...@@ -153,7 +153,7 @@ Widget buildFrame({
localeResolutionCallback: localeResolutionCallback, localeResolutionCallback: localeResolutionCallback,
supportedLocales: supportedLocales, supportedLocales: supportedLocales,
onGenerateRoute: (RouteSettings settings) { onGenerateRoute: (RouteSettings settings) {
return new PageRouteBuilder<Null>( return new PageRouteBuilder<void>(
pageBuilder: (BuildContext context, Animation<double> _, Animation<double> __) { pageBuilder: (BuildContext context, Animation<double> _, Animation<double> __) {
return buildContent(context); return buildContent(context);
} }
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment