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