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.
This commit is contained in:
Ian Hickson 2018-03-22 13:21:07 -07:00 committed by GitHub
parent 3a2e0d9375
commit aba0379dcc
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
63 changed files with 1614 additions and 789 deletions

View File

@ -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'); },
),
],
),

View File

@ -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.

View File

@ -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')

View File

@ -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);
},
),
],

View File

@ -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();

View File

@ -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')

View File

@ -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);
},
),
],

View File

@ -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(

View File

@ -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),

View File

@ -54,7 +54,7 @@ class _PersistentBottomSheetDemoState extends State<PersistentBottomSheetDemo> {
}
void _showMessage() {
showDialog<Null>(
showDialog<void>(
context: context,
builder: (BuildContext context) {
return new AlertDialog(

View File

@ -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(

View File

@ -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);
}

View File

@ -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),

View File

@ -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

View File

@ -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);
});

View File

@ -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,

View File

@ -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);
@ -56,16 +56,19 @@ class UpdaterState extends State<Updater> {
content: new Text('A newer version is available.', style: dialogTextStyle),
actions: <Widget>[
new FlatButton(
child: const Text('NO THANKS'),
onPressed: () {
Navigator.pop(context, false);
}),
child: const Text('NO THANKS'),
onPressed: () {
Navigator.pop(context, false);
},
),
new FlatButton(
child: const Text('UPDATE'),
onPressed: () {
Navigator.pop(context, true);
}),
]);
child: const Text('UPDATE'),
onPressed: () {
Navigator.pop(context, true);
},
),
],
);
}
@override

View File

@ -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),
);

View File

@ -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(),
);

View File

@ -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); },
);
}
}

View File

@ -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
}

View File

@ -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,

View File

@ -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,

View File

@ -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);
},
);
}

View File

@ -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;

View File

@ -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;

View File

@ -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);
});

View File

@ -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),

View File

@ -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;

View File

@ -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

File diff suppressed because it is too large Load Diff

View File

@ -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),

View File

@ -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 didChangeDependencies() {
super.didChangeDependencies();
_page = null;
}
void addWillPopCallback(WillPopCallback callback) {
assert(mounted);
_willPopCallbacks.add(callback);
}
void removeWillPopCallback(WillPopCallback callback) {
assert(mounted);
_willPopCallbacks.remove(callback);
}
void _animationStatusChanged(AnimationStatus status) {
void _forceRebuildPage() {
setState(() {
// The animation's states are our build state, and they changed already.
_page = null;
});
}
// This should be called to wrap any changes to route.isCurrent, route.canPop,
// and route.offstage.
void _routeSetState(VoidCallback fn) {
setState(fn);
}
@override
Widget build(BuildContext context) {
return new FocusScope(
node: widget.route.focusScopeNode,
return new _ModalScopeStatus(
route: widget.route,
isCurrent: widget.route.isCurrent, // _routeSetState is called if this updates
canPop: widget.route.canPop, // _routeSetState is called if this updates
child: new Offstage(
offstage: widget.route.offstage,
child: new IgnorePointer(
ignoring: widget.route.animation?.status == AnimationStatus.reverse,
// Keep the transition between repaint boundaries so we don't trigger
// deep repaints of the render tree above or below the transition.
child: new RepaintBoundary(
child: widget.route.buildTransitions(
context,
widget.route.animation,
widget.route.secondaryAnimation,
new RepaintBoundary(
child: new PageStorage(
key: widget.route._subtreeKey,
bucket: widget.route._storageBucket,
child: new _ModalScopeStatus(
route: widget.route,
isCurrent: widget.route.isCurrent,
canPop: widget.route.canPop,
child: widget.page,
offstage: widget.route.offstage, // _routeSetState is called if this updates
child: new PageStorage(
bucket: widget.route._storageBucket, // immutable
child: new FocusScope(
node: widget.route.focusScopeNode, // immutable
child: new RepaintBoundary(
child: new AnimatedBuilder(
animation: _listenable, // immutable
builder: (BuildContext context, Widget child) {
return widget.route.buildTransitions(
context,
widget.route.animation,
widget.route.secondaryAnimation,
new IgnorePointer(
ignoring: widget.route.animation?.status == AnimationStatus.reverse,
child: child,
),
);
},
child: _page ??= new RepaintBoundary(
key: widget.route._subtreeKey, // immutable
child: new Builder(
builder: (BuildContext context) {
return widget.route.buildPage(
context,
widget.route.animation,
widget.route.secondaryAnimation,
);
},
),
),
),
@ -495,22 +513,20 @@ class _ModalScopeState extends State<_ModalScope> {
/// A route that blocks interaction with previous routes.
///
/// ModalRoutes cover the entire [Navigator]. They are not necessarily [opaque],
/// however; for example, a pop-up menu uses a ModalRoute but only shows the menu
/// in a small box overlapping the previous route.
/// [ModalRoute]s cover the entire [Navigator]. They are not necessarily
/// [opaque], however; for example, a pop-up menu uses a [ModalRoute] but only
/// shows the menu in a small box overlapping the previous route.
///
/// The `T` type argument is the return value of the route. If there is no
/// return value, consider using `void` as the return value.
abstract class ModalRoute<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) {

View File

@ -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(

View File

@ -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(

View File

@ -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';

View File

@ -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(

View File

@ -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(

View File

@ -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(

View File

@ -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')));

View File

@ -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) {

View File

@ -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(

View File

@ -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();
},

View File

@ -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;

View File

@ -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'));
},

View File

@ -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,

View File

@ -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'));
},

View File

@ -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')),
);

View File

@ -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(

View File

@ -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);

View File

@ -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(

View File

@ -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),

View File

@ -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();

View File

@ -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();
}

View File

@ -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(

View File

@ -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');
}
@ -608,7 +615,7 @@ void main() {
testWidgets('remove a route whose value is awaited', (WidgetTester tester) async {
Future<String> pageValue;
final Map<String, WidgetBuilder> pageBuilders = <String, WidgetBuilder>{
'/': (BuildContext context) => new OnTapPage(id: '/', onTap: () { pageValue = Navigator.pushNamed(context, '/A'); }),
'/': (BuildContext context) => new OnTapPage(id: '/', onTap: () { pageValue = Navigator.pushNamed(context, '/A'); }),
'/A': (BuildContext context) => new OnTapPage(id: 'A', onTap: () { Navigator.pop(context, 'A'); }),
};
final Map<String, Route<String>> routes = <String, Route<String>>{};
@ -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']);
});
}

View File

@ -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(

View File

@ -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')),
);

View File

@ -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,

View File

@ -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

View File

@ -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);
}

View File

@ -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));

View File

@ -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);
}