Implement Router widget and widgets app api (#60299)

This commit is contained in:
chunhtai 2020-08-07 20:26:05 -07:00 committed by GitHub
parent 12b8d9db80
commit f9fd71bc78
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
16 changed files with 2997 additions and 187 deletions

View File

@ -104,6 +104,50 @@ class CupertinoApp extends StatefulWidget {
assert(checkerboardOffscreenLayers != null), assert(checkerboardOffscreenLayers != null),
assert(showSemanticsDebugger != null), assert(showSemanticsDebugger != null),
assert(debugShowCheckedModeBanner != null), assert(debugShowCheckedModeBanner != null),
routeInformationProvider = null,
routeInformationParser = null,
routerDelegate = null,
backButtonDispatcher = null,
super(key: key);
/// Creates a [CupertinoApp] that uses the [Router] instead of a [Navigator].
const CupertinoApp.router({
Key key,
this.routeInformationProvider,
@required this.routeInformationParser,
@required this.routerDelegate,
this.backButtonDispatcher,
this.theme,
this.builder,
this.title = '',
this.onGenerateTitle,
this.color,
this.locale,
this.localizationsDelegates,
this.localeListResolutionCallback,
this.localeResolutionCallback,
this.supportedLocales = const <Locale>[Locale('en', 'US')],
this.showPerformanceOverlay = false,
this.checkerboardRasterCacheImages = false,
this.checkerboardOffscreenLayers = false,
this.showSemanticsDebugger = false,
this.debugShowCheckedModeBanner = true,
this.shortcuts,
this.actions,
}) : assert(title != null),
assert(showPerformanceOverlay != null),
assert(checkerboardRasterCacheImages != null),
assert(checkerboardOffscreenLayers != null),
assert(showSemanticsDebugger != null),
assert(debugShowCheckedModeBanner != null),
navigatorObservers = null,
navigatorKey = null,
onGenerateRoute = null,
home = null,
onGenerateInitialRoutes = null,
onUnknownRoute = null,
routes = null,
initialRoute = null,
super(key: key); super(key: key);
/// {@macro flutter.widgets.widgetsApp.navigatorKey} /// {@macro flutter.widgets.widgetsApp.navigatorKey}
@ -143,6 +187,18 @@ class CupertinoApp extends StatefulWidget {
/// {@macro flutter.widgets.widgetsApp.navigatorObservers} /// {@macro flutter.widgets.widgetsApp.navigatorObservers}
final List<NavigatorObserver> navigatorObservers; final List<NavigatorObserver> navigatorObservers;
/// {@macro flutter.widgets.widgetsApp.routeInformationProvider}
final RouteInformationProvider routeInformationProvider;
/// {@macro flutter.widgets.widgetsApp.routeInformationParser}
final RouteInformationParser<Object> routeInformationParser;
/// {@macro flutter.widgets.widgetsApp.routerDelegate}
final RouterDelegate<Object> routerDelegate;
/// {@macro flutter.widgets.widgetsApp.backButtonDispatcher}
final BackButtonDispatcher backButtonDispatcher;
/// {@macro flutter.widgets.widgetsApp.builder} /// {@macro flutter.widgets.widgetsApp.builder}
final TransitionBuilder builder; final TransitionBuilder builder;
@ -286,6 +342,7 @@ class _AlwaysCupertinoScrollBehavior extends ScrollBehavior {
class _CupertinoAppState extends State<CupertinoApp> { class _CupertinoAppState extends State<CupertinoApp> {
HeroController _heroController; HeroController _heroController;
bool get _usesRouter => widget.routerDelegate != null;
@override @override
void initState() { void initState() {
@ -304,37 +361,34 @@ class _CupertinoAppState extends State<CupertinoApp> {
yield DefaultCupertinoLocalizations.delegate; yield DefaultCupertinoLocalizations.delegate;
} }
@override Widget _inspectorSelectButtonBuilder(BuildContext context, VoidCallback onPressed) {
Widget build(BuildContext context) { return CupertinoButton.filled(
final CupertinoThemeData effectiveThemeData = widget.theme ?? const CupertinoThemeData(); child: const Icon(
CupertinoIcons.search,
size: 28.0,
color: CupertinoColors.white,
),
padding: EdgeInsets.zero,
onPressed: onPressed,
);
}
return ScrollConfiguration( WidgetsApp _buildWidgetApp(BuildContext context) {
behavior: _AlwaysCupertinoScrollBehavior(), final CupertinoThemeData effectiveThemeData = CupertinoTheme.of(context);
child: CupertinoUserInterfaceLevel( final Color color = CupertinoDynamicColor.resolve(widget.color ?? effectiveThemeData.primaryColor, context);
data: CupertinoUserInterfaceLevelData.base,
child: CupertinoTheme( if (_usesRouter) {
data: effectiveThemeData, return WidgetsApp.router(
child: Builder(
builder: (BuildContext context) {
return HeroControllerScope(
controller: _heroController,
child: WidgetsApp(
key: GlobalObjectKey(this), key: GlobalObjectKey(this),
navigatorKey: widget.navigatorKey, routeInformationProvider: widget.routeInformationProvider,
navigatorObservers: widget.navigatorObservers, routeInformationParser: widget.routeInformationParser,
pageRouteBuilder: <T>(RouteSettings settings, WidgetBuilder builder) => routerDelegate: widget.routerDelegate,
CupertinoPageRoute<T>(settings: settings, builder: builder), backButtonDispatcher: widget.backButtonDispatcher,
home: widget.home,
routes: widget.routes,
initialRoute: widget.initialRoute,
onGenerateRoute: widget.onGenerateRoute,
onGenerateInitialRoutes: widget.onGenerateInitialRoutes,
onUnknownRoute: widget.onUnknownRoute,
builder: widget.builder, builder: widget.builder,
title: widget.title, title: widget.title,
onGenerateTitle: widget.onGenerateTitle, onGenerateTitle: widget.onGenerateTitle,
textStyle: CupertinoTheme.of(context).textTheme.textStyle, textStyle: effectiveThemeData.textTheme.textStyle,
color: CupertinoDynamicColor.resolve(widget.color ?? effectiveThemeData.primaryColor, context), color: color,
locale: widget.locale, locale: widget.locale,
localizationsDelegates: _localizationsDelegates, localizationsDelegates: _localizationsDelegates,
localeResolutionCallback: widget.localeResolutionCallback, localeResolutionCallback: widget.localeResolutionCallback,
@ -345,22 +399,60 @@ class _CupertinoAppState extends State<CupertinoApp> {
checkerboardOffscreenLayers: widget.checkerboardOffscreenLayers, checkerboardOffscreenLayers: widget.checkerboardOffscreenLayers,
showSemanticsDebugger: widget.showSemanticsDebugger, showSemanticsDebugger: widget.showSemanticsDebugger,
debugShowCheckedModeBanner: widget.debugShowCheckedModeBanner, debugShowCheckedModeBanner: widget.debugShowCheckedModeBanner,
inspectorSelectButtonBuilder: (BuildContext context, VoidCallback onPressed) { inspectorSelectButtonBuilder: _inspectorSelectButtonBuilder,
return CupertinoButton.filled(
child: const Icon(
CupertinoIcons.search,
size: 28.0,
color: CupertinoColors.white,
),
padding: EdgeInsets.zero,
onPressed: onPressed,
);
},
shortcuts: widget.shortcuts, shortcuts: widget.shortcuts,
actions: widget.actions, actions: widget.actions,
),
); );
}
return WidgetsApp(
key: GlobalObjectKey(this),
navigatorKey: widget.navigatorKey,
navigatorObservers: widget.navigatorObservers,
pageRouteBuilder: <T>(RouteSettings settings, WidgetBuilder builder) {
return CupertinoPageRoute<T>(settings: settings, builder: builder);
}, },
home: widget.home,
routes: widget.routes,
initialRoute: widget.initialRoute,
onGenerateRoute: widget.onGenerateRoute,
onGenerateInitialRoutes: widget.onGenerateInitialRoutes,
onUnknownRoute: widget.onUnknownRoute,
builder: widget.builder,
title: widget.title,
onGenerateTitle: widget.onGenerateTitle,
textStyle: effectiveThemeData.textTheme.textStyle,
color: color,
locale: widget.locale,
localizationsDelegates: _localizationsDelegates,
localeResolutionCallback: widget.localeResolutionCallback,
localeListResolutionCallback: widget.localeListResolutionCallback,
supportedLocales: widget.supportedLocales,
showPerformanceOverlay: widget.showPerformanceOverlay,
checkerboardRasterCacheImages: widget.checkerboardRasterCacheImages,
checkerboardOffscreenLayers: widget.checkerboardOffscreenLayers,
showSemanticsDebugger: widget.showSemanticsDebugger,
debugShowCheckedModeBanner: widget.debugShowCheckedModeBanner,
inspectorSelectButtonBuilder: _inspectorSelectButtonBuilder,
shortcuts: widget.shortcuts,
actions: widget.actions,
);
}
@override
Widget build(BuildContext context) {
final CupertinoThemeData effectiveThemeData = widget.theme ?? const CupertinoThemeData();
return ScrollConfiguration(
behavior: _AlwaysCupertinoScrollBehavior(),
child: CupertinoUserInterfaceLevel(
data: CupertinoUserInterfaceLevelData.base,
child: CupertinoTheme(
data: effectiveThemeData,
child: HeroControllerScope(
controller: _heroController,
child: Builder(
builder: _buildWidgetApp,
),
), ),
), ),
), ),

View File

@ -7,6 +7,7 @@
import 'dart:ui' as ui; import 'dart:ui' as ui;
import 'package:flutter/cupertino.dart'; import 'package:flutter/cupertino.dart';
import 'package:flutter/foundation.dart';
import 'package:flutter/rendering.dart'; import 'package:flutter/rendering.dart';
import 'package:flutter/widgets.dart'; import 'package:flutter/widgets.dart';
@ -205,6 +206,58 @@ class MaterialApp extends StatefulWidget {
assert(checkerboardOffscreenLayers != null), assert(checkerboardOffscreenLayers != null),
assert(showSemanticsDebugger != null), assert(showSemanticsDebugger != null),
assert(debugShowCheckedModeBanner != null), assert(debugShowCheckedModeBanner != null),
routeInformationProvider = null,
routeInformationParser = null,
routerDelegate = null,
backButtonDispatcher = null,
super(key: key);
/// Creates a [MaterialApp] that uses the [Router] instead of a [Navigator].
const MaterialApp.router({
Key key,
this.routeInformationProvider,
@required this.routeInformationParser,
@required this.routerDelegate,
this.backButtonDispatcher,
this.builder,
this.title = '',
this.onGenerateTitle,
this.color,
this.theme,
this.darkTheme,
this.highContrastTheme,
this.highContrastDarkTheme,
this.themeMode = ThemeMode.system,
this.locale,
this.localizationsDelegates,
this.localeListResolutionCallback,
this.localeResolutionCallback,
this.supportedLocales = const <Locale>[Locale('en', 'US')],
this.debugShowMaterialGrid = false,
this.showPerformanceOverlay = false,
this.checkerboardRasterCacheImages = false,
this.checkerboardOffscreenLayers = false,
this.showSemanticsDebugger = false,
this.debugShowCheckedModeBanner = true,
this.shortcuts,
this.actions,
}) : assert(routeInformationParser != null),
assert(routerDelegate != null),
assert(title != null),
assert(debugShowMaterialGrid != null),
assert(showPerformanceOverlay != null),
assert(checkerboardRasterCacheImages != null),
assert(checkerboardOffscreenLayers != null),
assert(showSemanticsDebugger != null),
assert(debugShowCheckedModeBanner != null),
navigatorObservers = null,
navigatorKey = null,
onGenerateRoute = null,
home = null,
onGenerateInitialRoutes = null,
onUnknownRoute = null,
routes = null,
initialRoute = null,
super(key: key); super(key: key);
/// {@macro flutter.widgets.widgetsApp.navigatorKey} /// {@macro flutter.widgets.widgetsApp.navigatorKey}
@ -238,6 +291,18 @@ class MaterialApp extends StatefulWidget {
/// {@macro flutter.widgets.widgetsApp.navigatorObservers} /// {@macro flutter.widgets.widgetsApp.navigatorObservers}
final List<NavigatorObserver> navigatorObservers; final List<NavigatorObserver> navigatorObservers;
/// {@macro flutter.widgets.widgetsApp.routeInformationProvider}
final RouteInformationProvider routeInformationProvider;
/// {@macro flutter.widgets.widgetsApp.routeInformationParser}
final RouteInformationParser<Object> routeInformationParser;
/// {@macro flutter.widgets.widgetsApp.routerDelegate}
final RouterDelegate<Object> routerDelegate;
/// {@macro flutter.widgets.widgetsApp.backButtonDispatcher}
final BackButtonDispatcher backButtonDispatcher;
/// {@macro flutter.widgets.widgetsApp.builder} /// {@macro flutter.widgets.widgetsApp.builder}
/// ///
/// Material specific features such as [showDialog] and [showMenu], and widgets /// Material specific features such as [showDialog] and [showMenu], and widgets
@ -611,6 +676,8 @@ class _MaterialScrollBehavior extends ScrollBehavior {
class _MaterialAppState extends State<MaterialApp> { class _MaterialAppState extends State<MaterialApp> {
HeroController _heroController; HeroController _heroController;
bool get _usesRouter => widget.routerDelegate != null;
@override @override
void initState() { void initState() {
super.initState(); super.initState();
@ -629,24 +696,15 @@ class _MaterialAppState extends State<MaterialApp> {
yield DefaultCupertinoLocalizations.delegate; yield DefaultCupertinoLocalizations.delegate;
} }
@override Widget _inspectorSelectButtonBuilder(BuildContext context, VoidCallback onPressed) {
Widget build(BuildContext context) { return FloatingActionButton(
Widget result = HeroControllerScope( child: const Icon(Icons.search),
controller: _heroController, onPressed: onPressed,
child: WidgetsApp( mini: true,
key: GlobalObjectKey(this), );
navigatorKey: widget.navigatorKey, }
navigatorObservers: widget.navigatorObservers,
pageRouteBuilder: <T>(RouteSettings settings, WidgetBuilder builder) { Widget _materialBuilder(BuildContext context, Widget child) {
return MaterialPageRoute<T>(settings: settings, builder: builder);
},
home: widget.home,
routes: widget.routes,
initialRoute: widget.initialRoute,
onGenerateRoute: widget.onGenerateRoute,
onGenerateInitialRoutes: widget.onGenerateInitialRoutes,
onUnknownRoute: widget.onUnknownRoute,
builder: (BuildContext context, Widget child) {
// Resolve which theme to use based on brightness and high contrast. // Resolve which theme to use based on brightness and high contrast.
final ThemeMode mode = widget.themeMode ?? ThemeMode.system; final ThemeMode mode = widget.themeMode ?? ThemeMode.system;
final Brightness platformBrightness = MediaQuery.platformBrightnessOf(context); final Brightness platformBrightness = MediaQuery.platformBrightnessOf(context);
@ -686,18 +744,29 @@ class _MaterialAppState extends State<MaterialApp> {
) )
: child, : child,
); );
}, }
title: widget.title,
onGenerateTitle: widget.onGenerateTitle, Widget _buildWidgetApp(BuildContext context) {
textStyle: _errorTextStyle,
// The color property is always pulled from the light theme, even if dark // The color property is always pulled from the light theme, even if dark
// mode is activated. This was done to simplify the technical details // mode is activated. This was done to simplify the technical details
// of switching themes and it was deemed acceptable because this color // of switching themes and it was deemed acceptable because this color
// property is only used on old Android OSes to color the app bar in // property is only used on old Android OSes to color the app bar in
// Android's switcher UI. // Android's switcher UI.
// //
// blue is the primary color of the default theme // blue is the primary color of the default theme.
color: widget.color ?? widget.theme?.primaryColor ?? Colors.blue, final Color materialColor = widget.color ?? widget.theme?.primaryColor ?? Colors.blue;
if (_usesRouter) {
return WidgetsApp.router(
key: GlobalObjectKey(this),
routeInformationProvider: widget.routeInformationProvider,
routeInformationParser: widget.routeInformationParser,
routerDelegate: widget.routerDelegate,
backButtonDispatcher: widget.backButtonDispatcher,
builder: _materialBuilder,
title: widget.title,
onGenerateTitle: widget.onGenerateTitle,
textStyle: _errorTextStyle,
color: materialColor,
locale: widget.locale, locale: widget.locale,
localizationsDelegates: _localizationsDelegates, localizationsDelegates: _localizationsDelegates,
localeResolutionCallback: widget.localeResolutionCallback, localeResolutionCallback: widget.localeResolutionCallback,
@ -708,17 +777,49 @@ class _MaterialAppState extends State<MaterialApp> {
checkerboardOffscreenLayers: widget.checkerboardOffscreenLayers, checkerboardOffscreenLayers: widget.checkerboardOffscreenLayers,
showSemanticsDebugger: widget.showSemanticsDebugger, showSemanticsDebugger: widget.showSemanticsDebugger,
debugShowCheckedModeBanner: widget.debugShowCheckedModeBanner, debugShowCheckedModeBanner: widget.debugShowCheckedModeBanner,
inspectorSelectButtonBuilder: (BuildContext context, VoidCallback onPressed) { inspectorSelectButtonBuilder: _inspectorSelectButtonBuilder,
return FloatingActionButton(
child: const Icon(Icons.search),
onPressed: onPressed,
mini: true,
);
},
shortcuts: widget.shortcuts, shortcuts: widget.shortcuts,
actions: widget.actions, actions: widget.actions,
),
); );
}
return WidgetsApp(
key: GlobalObjectKey(this),
navigatorKey: widget.navigatorKey,
navigatorObservers: widget.navigatorObservers,
pageRouteBuilder: <T>(RouteSettings settings, WidgetBuilder builder) {
return MaterialPageRoute<T>(settings: settings, builder: builder);
},
home: widget.home,
routes: widget.routes,
initialRoute: widget.initialRoute,
onGenerateRoute: widget.onGenerateRoute,
onGenerateInitialRoutes: widget.onGenerateInitialRoutes,
onUnknownRoute: widget.onUnknownRoute,
builder: _materialBuilder,
title: widget.title,
onGenerateTitle: widget.onGenerateTitle,
textStyle: _errorTextStyle,
color: materialColor,
locale: widget.locale,
localizationsDelegates: _localizationsDelegates,
localeResolutionCallback: widget.localeResolutionCallback,
localeListResolutionCallback: widget.localeListResolutionCallback,
supportedLocales: widget.supportedLocales,
showPerformanceOverlay: widget.showPerformanceOverlay,
checkerboardRasterCacheImages: widget.checkerboardRasterCacheImages,
checkerboardOffscreenLayers: widget.checkerboardOffscreenLayers,
showSemanticsDebugger: widget.showSemanticsDebugger,
debugShowCheckedModeBanner: widget.debugShowCheckedModeBanner,
inspectorSelectButtonBuilder: _inspectorSelectButtonBuilder,
shortcuts: widget.shortcuts,
actions: widget.actions,
);
}
@override
Widget build(BuildContext context) {
Widget result = _buildWidgetApp(context);
assert(() { assert(() {
if (widget.debugShowMaterialGrid) { if (widget.debugShowMaterialGrid) {
@ -735,7 +836,10 @@ class _MaterialAppState extends State<MaterialApp> {
return ScrollConfiguration( return ScrollConfiguration(
behavior: _MaterialScrollBehavior(), behavior: _MaterialScrollBehavior(),
child: HeroControllerScope(
controller: _heroController,
child: result, child: result,
)
); );
} }
} }

View File

@ -26,16 +26,18 @@ class SystemChannels {
/// * `pushRoute`, which is called with a single string argument when the /// * `pushRoute`, which is called with a single string argument when the
/// operating system instructs the application to open a particular page. /// operating system instructs the application to open a particular page.
/// ///
/// * `pushRouteInformation`, which is called with a map, which contains a
/// location string and a state object, when the operating system instructs
/// the application to open a particular page. These parameters are stored
/// under the key `location` and `state` in the map.
///
/// The following methods are used for the opposite direction data flow. The /// The following methods are used for the opposite direction data flow. The
/// framework notifies the engine about the route changes. /// framework notifies the engine about the route changes.
/// ///
/// * `routePushed`, which is called when a route is pushed. (e.g. A modal /// * `routeUpdated`, which is called when current route has changed.
/// replaces the entire screen.)
/// ///
/// * `routePopped`, which is called when a route is popped. (e.g. A dialog, /// * `routeInformationUpdated`, which is called by the [Router] when the
/// such as time picker is closed.) /// application navigate to a new location.
///
/// * `routeReplaced`, which is called when a route is replaced.
/// ///
/// See also: /// See also:
/// ///
@ -46,7 +48,7 @@ class SystemChannels {
/// [Navigator.push], [Navigator.pushReplacement], [Navigator.pop] and /// [Navigator.push], [Navigator.pushReplacement], [Navigator.pop] and
/// [Navigator.replace], utilize this channel's methods to send route /// [Navigator.replace], utilize this channel's methods to send route
/// change information from framework to engine. /// change information from framework to engine.
static const MethodChannel navigation = MethodChannel( static const MethodChannel navigation = OptionalMethodChannel(
'flutter/navigation', 'flutter/navigation',
JSONMethodCodec(), JSONMethodCodec(),
); );

View File

@ -35,4 +35,37 @@ class SystemNavigator {
static Future<void> pop({bool? animated}) async { static Future<void> pop({bool? animated}) async {
await SystemChannels.platform.invokeMethod<void>('SystemNavigator.pop', animated); await SystemChannels.platform.invokeMethod<void>('SystemNavigator.pop', animated);
} }
/// Notifies the platform for a route information change.
///
/// On Web, creates a new browser history entry and update URL with the route
/// information.
static void routeInformationUpdated({
required String location,
Object? state
}) {
SystemChannels.navigation.invokeMethod<void>(
'routeInformationUpdated',
<String, dynamic>{
'location': location,
'state': state,
},
);
}
/// Notifies the platform of a route change.
///
/// On Web, updates the URL bar with the [routeName].
static void routeUpdated({
String? routeName,
String? previousRouteName
}) {
SystemChannels.navigation.invokeMethod<void>(
'routeUpdated',
<String, dynamic>{
'previousRouteName': previousRouteName,
'routeName': routeName,
},
);
}
} }

View File

@ -22,6 +22,7 @@ import 'media_query.dart';
import 'navigator.dart'; import 'navigator.dart';
import 'pages.dart'; import 'pages.dart';
import 'performance_overlay.dart'; import 'performance_overlay.dart';
import 'router.dart';
import 'scrollable.dart'; import 'scrollable.dart';
import 'semantics_debugger.dart'; import 'semantics_debugger.dart';
import 'shortcuts.dart'; import 'shortcuts.dart';
@ -260,6 +261,62 @@ class WidgetsApp extends StatefulWidget {
assert(showSemanticsDebugger != null), assert(showSemanticsDebugger != null),
assert(debugShowCheckedModeBanner != null), assert(debugShowCheckedModeBanner != null),
assert(debugShowWidgetInspector != null), assert(debugShowWidgetInspector != null),
routeInformationProvider = null,
routeInformationParser = null,
routerDelegate = null,
backButtonDispatcher = null,
super(key: key);
/// Creates a [WidgetsApp] that uses the [Router] instead of a [Navigator].
WidgetsApp.router({
Key key,
this.routeInformationProvider,
@required this.routeInformationParser,
@required this.routerDelegate,
BackButtonDispatcher backButtonDispatcher,
this.builder,
this.title = '',
this.onGenerateTitle,
this.textStyle,
@required this.color,
this.locale,
this.localizationsDelegates,
this.localeListResolutionCallback,
this.localeResolutionCallback,
this.supportedLocales = const <Locale>[Locale('en', 'US')],
this.showPerformanceOverlay = false,
this.checkerboardRasterCacheImages = false,
this.checkerboardOffscreenLayers = false,
this.showSemanticsDebugger = false,
this.debugShowWidgetInspector = false,
this.debugShowCheckedModeBanner = true,
this.inspectorSelectButtonBuilder,
this.shortcuts,
this.actions,
}) : assert(
routeInformationParser != null &&
routerDelegate != null,
'The routeInformationParser and routerDelegate cannot be null.'
),
assert(title != null),
assert(color != null),
assert(supportedLocales != null && supportedLocales.isNotEmpty),
assert(showPerformanceOverlay != null),
assert(checkerboardRasterCacheImages != null),
assert(checkerboardOffscreenLayers != null),
assert(showSemanticsDebugger != null),
assert(debugShowCheckedModeBanner != null),
assert(debugShowWidgetInspector != null),
navigatorObservers = null,
backButtonDispatcher = backButtonDispatcher ?? RootBackButtonDispatcher(),
navigatorKey = null,
onGenerateRoute = null,
pageRouteBuilder = null,
home = null,
onGenerateInitialRoutes = null,
onUnknownRoute = null,
routes = null,
initialRoute = null,
super(key: key); super(key: key);
/// {@template flutter.widgets.widgetsApp.navigatorKey} /// {@template flutter.widgets.widgetsApp.navigatorKey}
@ -321,6 +378,71 @@ class WidgetsApp extends StatefulWidget {
/// or a [CupertinoPageRoute] should be used for building page transitions. /// or a [CupertinoPageRoute] should be used for building page transitions.
final PageRouteFactory pageRouteBuilder; final PageRouteFactory pageRouteBuilder;
/// {@template flutter.widgets.widgetsApp.routeInformationParser}
/// A delegate to parse the route information from the
/// [routeInformationProvider] into a generic data type to be processed by
/// the [routerDelegate] at a later stage.
///
/// This object will be used by the underlying [Router].
///
/// The generic type `T` must match the generic type of the [routerDelegate].
///
/// See also:
///
/// * [Router.routeInformationParser]: which receives this object when this
/// widget builds the [Router].
/// {@endtemplate}
final RouteInformationParser<Object> routeInformationParser;
/// {@template flutter.widgets.widgetsApp.routerDelegate}
/// A delegate that configures a widget, typically a [Navigator], with
/// parsed result from the [routeInformationParser].
///
/// This object will be used by the underlying [Router].
///
/// The generic type `T` must match the generic type of the
/// [routeInformationParser].
///
/// See also:
///
/// * [Router.routerDelegate]: which receives this object when this widget
/// builds the [Router].
/// {@endtemplate}
final RouterDelegate<Object> routerDelegate;
/// {@template flutter.widgets.widgetsApp.backButtonDispatcher}
/// A delegate that decide whether to handle the Android back button intent.
///
/// This object will be used by the underlying [Router].
///
/// If this is not provided, the widgets app will create a
/// [RootBackButtonDispatcher] by default.
///
/// See also:
///
/// * [Router.backButtonDispatcher]: which receives this object when this
/// widget builds the [Router].
/// {@endtemplate}
final BackButtonDispatcher backButtonDispatcher;
/// {@template flutter.widgets.widgetsApp.routeInformationProvider}
/// A object that provides route information through the
/// [RouteInformationProvider.value] and notifies its listener when its value
/// changes.
///
/// This object will be used by the underlying [Router].
///
/// If this is not provided, the widgets app will create a
/// [PlatformRouteInformationProvider] with initial route name equals to
/// the [Window.defaultRouteName] by default.
///
/// See also:
///
/// * [Router.routeInformationProvider]: which receives this object when this
/// widget builds the [Router].
/// {@endtemplate}
final RouteInformationProvider routeInformationProvider;
/// {@template flutter.widgets.widgetsApp.home} /// {@template flutter.widgets.widgetsApp.home}
/// The widget for the default route of the app ([Navigator.defaultRouteName], /// The widget for the default route of the app ([Navigator.defaultRouteName],
/// which is `/`). /// which is `/`).
@ -960,10 +1082,21 @@ class WidgetsApp extends StatefulWidget {
class _WidgetsAppState extends State<WidgetsApp> with WidgetsBindingObserver { class _WidgetsAppState extends State<WidgetsApp> with WidgetsBindingObserver {
// STATE LIFECYCLE // STATE LIFECYCLE
// If window.defaultRouteName isn't '/', we should assume it was set
// intentionally via `setInitialRoute`, and should override whatever is in
// [widget.initialRoute].
String get _initialRouteName => WidgetsBinding.instance.window.defaultRouteName != Navigator.defaultRouteName
? WidgetsBinding.instance.window.defaultRouteName
: widget.initialRoute ?? WidgetsBinding.instance.window.defaultRouteName;
@override @override
void initState() { void initState() {
super.initState(); super.initState();
if (_usesRouter) {
_updateRouter();
} else {
_updateNavigator(); _updateNavigator();
}
_locale = _resolveLocales(WidgetsBinding.instance.window.locales, widget.supportedLocales); _locale = _resolveLocales(WidgetsBinding.instance.window.locales, widget.supportedLocales);
WidgetsBinding.instance.addObserver(this); WidgetsBinding.instance.addObserver(this);
} }
@ -971,16 +1104,37 @@ class _WidgetsAppState extends State<WidgetsApp> with WidgetsBindingObserver {
@override @override
void didUpdateWidget(WidgetsApp oldWidget) { void didUpdateWidget(WidgetsApp oldWidget) {
super.didUpdateWidget(oldWidget); super.didUpdateWidget(oldWidget);
if (widget.navigatorKey != oldWidget.navigatorKey) if (oldWidget.routeInformationProvider != widget.routeInformationProvider) {
_updateRouter();
}
if (widget.navigatorKey != oldWidget.navigatorKey) {
_updateNavigator(); _updateNavigator();
} }
}
@override @override
void dispose() { void dispose() {
WidgetsBinding.instance.removeObserver(this); WidgetsBinding.instance.removeObserver(this);
_defaultRouteInformationProvider?.dispose();
super.dispose(); super.dispose();
} }
bool get _usesRouter => widget.routerDelegate != null;
// ROUTER
RouteInformationProvider get _effectiveRouteInformationProvider => widget.routeInformationProvider ?? _defaultRouteInformationProvider;
PlatformRouteInformationProvider _defaultRouteInformationProvider;
void _updateRouter() {
_defaultRouteInformationProvider?.dispose();
if (widget.routeInformationProvider == null)
_defaultRouteInformationProvider = PlatformRouteInformationProvider(
initialRouteInformation: RouteInformation(
location: _initialRouteName,
),
);
}
// NAVIGATOR // NAVIGATOR
GlobalKey<NavigatorState> _navigator; GlobalKey<NavigatorState> _navigator;
@ -1050,6 +1204,11 @@ class _WidgetsAppState extends State<WidgetsApp> with WidgetsBindingObserver {
@override @override
Future<bool> didPopRoute() async { Future<bool> didPopRoute() async {
assert(mounted); assert(mounted);
// The back button dispatcher should handle the pop route if we use a
// router.
if (_usesRouter)
return false;
final NavigatorState navigator = _navigator?.currentState; final NavigatorState navigator = _navigator?.currentState;
if (navigator == null) if (navigator == null)
return false; return false;
@ -1059,6 +1218,11 @@ class _WidgetsAppState extends State<WidgetsApp> with WidgetsBindingObserver {
@override @override
Future<bool> didPushRoute(String route) async { Future<bool> didPushRoute(String route) async {
assert(mounted); assert(mounted);
// The route name provider should handle the push route if we uses a
// router.
if (_usesRouter)
return false;
final NavigatorState navigator = _navigator?.currentState; final NavigatorState navigator = _navigator?.currentState;
if (navigator == null) if (navigator == null)
return false; return false;
@ -1291,16 +1455,20 @@ class _WidgetsAppState extends State<WidgetsApp> with WidgetsBindingObserver {
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
Widget navigator; Widget routing;
if (_navigator != null) { if (_usesRouter) {
navigator = Navigator( assert(_effectiveRouteInformationProvider != null);
routing = Router<dynamic>(
routeInformationProvider: _effectiveRouteInformationProvider,
routeInformationParser: widget.routeInformationParser,
routerDelegate: widget.routerDelegate,
backButtonDispatcher: widget.backButtonDispatcher,
);
} else {
assert(_navigator != null);
routing = Navigator(
key: _navigator, key: _navigator,
// If window.defaultRouteName isn't '/', we should assume it was set initialRoute: _initialRouteName,
// intentionally via `setInitialRoute`, and should override whatever
// is in [widget.initialRoute].
initialRoute: WidgetsBinding.instance.window.defaultRouteName != Navigator.defaultRouteName
? WidgetsBinding.instance.window.defaultRouteName
: widget.initialRoute ?? WidgetsBinding.instance.window.defaultRouteName,
onGenerateRoute: _onGenerateRoute, onGenerateRoute: _onGenerateRoute,
onGenerateInitialRoutes: widget.onGenerateInitialRoutes == null onGenerateInitialRoutes: widget.onGenerateInitialRoutes == null
? Navigator.defaultGenerateInitialRoutes ? Navigator.defaultGenerateInitialRoutes
@ -1317,12 +1485,12 @@ class _WidgetsAppState extends State<WidgetsApp> with WidgetsBindingObserver {
if (widget.builder != null) { if (widget.builder != null) {
result = Builder( result = Builder(
builder: (BuildContext context) { builder: (BuildContext context) {
return widget.builder(context, navigator); return widget.builder(context, routing);
}, },
); );
} else { } else {
assert(navigator != null); assert(routing != null);
result = navigator; result = routing;
} }
if (widget.textStyle != null) { if (widget.textStyle != null) {

View File

@ -18,6 +18,7 @@ import 'app.dart';
import 'debug.dart'; import 'debug.dart';
import 'focus_manager.dart'; import 'focus_manager.dart';
import 'framework.dart'; import 'framework.dart';
import 'router.dart';
import 'widget_inspector.dart'; import 'widget_inspector.dart';
export 'dart:ui' show AppLifecycleState, Locale; export 'dart:ui' show AppLifecycleState, Locale;
@ -95,7 +96,7 @@ abstract class WidgetsBindingObserver {
/// [SystemChannels.navigation]. /// [SystemChannels.navigation].
Future<bool> didPopRoute() => Future<bool>.value(false); Future<bool> didPopRoute() => Future<bool>.value(false);
/// Called when the host tells the app to push a new route onto the /// Called when the host tells the application to push a new route onto the
/// navigator. /// navigator.
/// ///
/// Observers are expected to return true if they were able to /// Observers are expected to return true if they were able to
@ -106,6 +107,22 @@ abstract class WidgetsBindingObserver {
/// [SystemChannels.navigation]. /// [SystemChannels.navigation].
Future<bool> didPushRoute(String route) => Future<bool>.value(false); Future<bool> didPushRoute(String route) => Future<bool>.value(false);
/// Called when the host tells the application to push a new
/// [RouteInformation] and a restoration state onto the router.
///
/// Observers are expected to return true if they were able to
/// handle the notification. Observers are notified in registration
/// order until one returns true.
///
/// This method exposes the `pushRouteInformation` notification from
/// [SystemChannels.navigation].
///
/// The default implementation is to call the [didPushRoute] directly with the
/// [RouteInformation.location].
Future<bool> didPushRouteInformation(RouteInformation routeInformation) {
return didPushRoute(routeInformation.location);
}
/// Called when the application's dimensions change. For example, /// Called when the application's dimensions change. For example,
/// when a phone is rotated. /// when a phone is rotated.
/// ///
@ -654,12 +671,28 @@ mixin WidgetsBinding on BindingBase, ServicesBinding, SchedulerBinding, GestureB
} }
} }
Future<void> _handlePushRouteInformation(Map<dynamic, dynamic> routeArguments) async {
for (final WidgetsBindingObserver observer in List<WidgetsBindingObserver>.from(_observers)) {
if (
await observer.didPushRouteInformation(
RouteInformation(
location: routeArguments['location'] as String,
state: routeArguments['state'] as Object,
)
)
)
return;
}
}
Future<dynamic> _handleNavigationInvocation(MethodCall methodCall) { Future<dynamic> _handleNavigationInvocation(MethodCall methodCall) {
switch (methodCall.method) { switch (methodCall.method) {
case 'popRoute': case 'popRoute':
return handlePopRoute(); return handlePopRoute();
case 'pushRoute': case 'pushRoute':
return handlePushRoute(methodCall.arguments as String); return handlePushRoute(methodCall.arguments as String);
case 'pushRouteInformation':
return _handlePushRouteInformation(methodCall.arguments as Map<dynamic, dynamic>);
} }
return Future<dynamic>.value(); return Future<dynamic>.value();
} }

View File

@ -21,7 +21,6 @@ import 'focus_scope.dart';
import 'framework.dart'; import 'framework.dart';
import 'heroes.dart'; import 'heroes.dart';
import 'overlay.dart'; import 'overlay.dart';
import 'route_notification_messages.dart';
import 'routes.dart'; import 'routes.dart';
import 'ticker_provider.dart'; import 'ticker_provider.dart';
@ -3308,8 +3307,10 @@ class NavigatorState extends State<Navigator> with TickerProviderStateMixin {
_RouteEntry.isPresentPredicate, orElse: () => null); _RouteEntry.isPresentPredicate, orElse: () => null);
final String routeName = lastEntry?.route?.settings?.name; final String routeName = lastEntry?.route?.settings?.name;
if (routeName != _lastAnnouncedRouteName) { if (routeName != _lastAnnouncedRouteName) {
RouteNotificationMessages.maybeNotifyRouteChange( SystemNavigator.routeUpdated(
routeName, _lastAnnouncedRouteName); routeName: routeName,
previousRouteName: _lastAnnouncedRouteName
);
_lastAnnouncedRouteName = routeName; _lastAnnouncedRouteName = routeName;
} }
} }

View File

@ -1,41 +0,0 @@
// Copyright 2014 The Flutter Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
// @dart = 2.8
import 'package:flutter/foundation.dart';
import 'package:flutter/services.dart';
/// Messages for route change notifications.
class RouteNotificationMessages {
// This class is not meant to be instantiated or extended; this constructor
// prevents instantiation and extension.
// ignore: unused_element
RouteNotificationMessages._();
/// When the engine is Web notify the platform for a route change.
static void maybeNotifyRouteChange(String routeName, String previousRouteName) {
if(kIsWeb) {
_notifyRouteChange(routeName, previousRouteName);
} else {
// No op.
}
}
/// Notifies the platform of a route change.
///
/// See also:
///
/// * [SystemChannels.navigation], which handles subsequent navigation
/// requests.
static void _notifyRouteChange(String routeName, String previousRouteName) {
SystemChannels.navigation.invokeMethod<void>(
'routeUpdated',
<String, dynamic>{
'previousRouteName': previousRouteName,
'routeName': routeName,
},
);
}
}

File diff suppressed because it is too large Load Diff

View File

@ -85,6 +85,7 @@ export 'src/widgets/primary_scroll_controller.dart';
export 'src/widgets/raw_keyboard_listener.dart'; export 'src/widgets/raw_keyboard_listener.dart';
export 'src/widgets/restoration.dart'; export 'src/widgets/restoration.dart';
export 'src/widgets/restoration_properties.dart'; export 'src/widgets/restoration_properties.dart';
export 'src/widgets/router.dart';
export 'src/widgets/routes.dart'; export 'src/widgets/routes.dart';
export 'src/widgets/safe_area.dart'; export 'src/widgets/safe_area.dart';
export 'src/widgets/scroll_activity.dart'; export 'src/widgets/scroll_activity.dart';

View File

@ -6,6 +6,8 @@
import 'package:flutter_test/flutter_test.dart'; import 'package:flutter_test/flutter_test.dart';
import 'package:flutter/cupertino.dart'; import 'package:flutter/cupertino.dart';
import 'package:flutter/foundation.dart';
import 'package:flutter/services.dart';
void main() { void main() {
testWidgets('Heroes work', (WidgetTester tester) async { testWidgets('Heroes work', (WidgetTester tester) async {
@ -147,4 +149,101 @@ void main() {
expect(key2.currentState, isA<NavigatorState>()); expect(key2.currentState, isA<NavigatorState>());
expect(key1.currentState, isNull); expect(key1.currentState, isNull);
}); });
testWidgets('CupertinoApp.router works', (WidgetTester tester) async {
final PlatformRouteInformationProvider provider = PlatformRouteInformationProvider(
initialRouteInformation: const RouteInformation(
location: 'initial',
),
);
final SimpleNavigatorRouterDelegate delegate = SimpleNavigatorRouterDelegate(
builder: (BuildContext context, RouteInformation information) {
return Text(information.location);
},
onPopPage: (Route<void> route, void result, SimpleNavigatorRouterDelegate delegate) {
delegate.routeInformation = const RouteInformation(
location: 'popped',
);
return route.didPop(result);
}
);
await tester.pumpWidget(CupertinoApp.router(
routeInformationProvider: provider,
routeInformationParser: SimpleRouteInformationParser(),
routerDelegate: delegate,
));
expect(find.text('initial'), findsOneWidget);
// Simulate android back button intent.
final ByteData message = const JSONMethodCodec().encodeMethodCall(const MethodCall('popRoute'));
await ServicesBinding.instance.defaultBinaryMessenger.handlePlatformMessage('flutter/navigation', message, (_) { });
await tester.pumpAndSettle();
expect(find.text('popped'), findsOneWidget);
});
}
typedef SimpleRouterDelegateBuilder = Widget Function(BuildContext, RouteInformation);
typedef SimpleNavigatorRouterDelegatePopPage<T> = bool Function(Route<T> route, T result, SimpleNavigatorRouterDelegate delegate);
class SimpleRouteInformationParser extends RouteInformationParser<RouteInformation> {
SimpleRouteInformationParser();
@override
Future<RouteInformation> parseRouteInformation(RouteInformation information) {
return SynchronousFuture<RouteInformation>(information);
}
@override
RouteInformation restoreRouteInformation(RouteInformation configuration) {
return configuration;
}
}
class SimpleNavigatorRouterDelegate extends RouterDelegate<RouteInformation> with PopNavigatorRouterDelegateMixin<RouteInformation>, ChangeNotifier {
SimpleNavigatorRouterDelegate({
@required this.builder,
this.onPopPage,
});
@override
GlobalKey<NavigatorState> navigatorKey = GlobalKey<NavigatorState>();
RouteInformation get routeInformation => _routeInformation;
RouteInformation _routeInformation;
set routeInformation(RouteInformation newValue) {
_routeInformation = newValue;
notifyListeners();
}
SimpleRouterDelegateBuilder builder;
SimpleNavigatorRouterDelegatePopPage<void> onPopPage;
@override
Future<void> setNewRoutePath(RouteInformation configuration) {
_routeInformation = configuration;
return SynchronousFuture<void>(null);
}
bool _handlePopPage(Route<void> route, void data) {
return onPopPage(route, data, this);
}
@override
Widget build(BuildContext context) {
return Navigator(
key: navigatorKey,
onPopPage: _handlePopPage,
pages: <Page<void>>[
// We need at least two pages for the pop to propagate through.
// Otherwise, the navigator will bubble the pop to the system navigator.
CupertinoPage<void>(
builder: (BuildContext context) => const Text('base'),
),
CupertinoPage<void>(
key: ValueKey<String>(routeInformation?.location),
builder: (BuildContext context) => builder(context, routeInformation),
)
],
);
}
} }

View File

@ -4,7 +4,9 @@
// @dart = 2.8 // @dart = 2.8
import 'package:flutter/foundation.dart';
import 'package:flutter/semantics.dart'; import 'package:flutter/semantics.dart';
import 'package:flutter/services.dart';
import 'package:flutter_test/flutter_test.dart'; import 'package:flutter_test/flutter_test.dart';
import 'package:flutter/cupertino.dart'; import 'package:flutter/cupertino.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
@ -947,6 +949,37 @@ void main() {
expect(key2.currentState, isA<NavigatorState>()); expect(key2.currentState, isA<NavigatorState>());
expect(key1.currentState, isNull); expect(key1.currentState, isNull);
}); });
testWidgets('MaterialApp.router works', (WidgetTester tester) async {
final PlatformRouteInformationProvider provider = PlatformRouteInformationProvider(
initialRouteInformation: const RouteInformation(
location: 'initial',
),
);
final SimpleNavigatorRouterDelegate delegate = SimpleNavigatorRouterDelegate(
builder: (BuildContext context, RouteInformation information) {
return Text(information.location);
},
onPopPage: (Route<void> route, void result, SimpleNavigatorRouterDelegate delegate) {
delegate.routeInformation = const RouteInformation(
location: 'popped',
);
return route.didPop(result);
}
);
await tester.pumpWidget(MaterialApp.router(
routeInformationProvider: provider,
routeInformationParser: SimpleRouteInformationParser(),
routerDelegate: delegate,
));
expect(find.text('initial'), findsOneWidget);
// Simulate android back button intent.
final ByteData message = const JSONMethodCodec().encodeMethodCall(const MethodCall('popRoute'));
await ServicesBinding.instance.defaultBinaryMessenger.handlePlatformMessage('flutter/navigation', message, (_) { });
await tester.pumpAndSettle();
expect(find.text('popped'), findsOneWidget);
});
} }
class MockAccessibilityFeature implements AccessibilityFeatures { class MockAccessibilityFeature implements AccessibilityFeatures {
@ -968,3 +1001,69 @@ class MockAccessibilityFeature implements AccessibilityFeatures {
@override @override
bool get reduceMotion => true; bool get reduceMotion => true;
} }
typedef SimpleRouterDelegateBuilder = Widget Function(BuildContext, RouteInformation);
typedef SimpleNavigatorRouterDelegatePopPage<T> = bool Function(Route<T> route, T result, SimpleNavigatorRouterDelegate delegate);
class SimpleRouteInformationParser extends RouteInformationParser<RouteInformation> {
SimpleRouteInformationParser();
@override
Future<RouteInformation> parseRouteInformation(RouteInformation information) {
return SynchronousFuture<RouteInformation>(information);
}
@override
RouteInformation restoreRouteInformation(RouteInformation configuration) {
return configuration;
}
}
class SimpleNavigatorRouterDelegate extends RouterDelegate<RouteInformation> with PopNavigatorRouterDelegateMixin<RouteInformation>, ChangeNotifier {
SimpleNavigatorRouterDelegate({
@required this.builder,
this.onPopPage,
});
@override
GlobalKey<NavigatorState> navigatorKey = GlobalKey<NavigatorState>();
RouteInformation get routeInformation => _routeInformation;
RouteInformation _routeInformation;
set routeInformation(RouteInformation newValue) {
_routeInformation = newValue;
notifyListeners();
}
SimpleRouterDelegateBuilder builder;
SimpleNavigatorRouterDelegatePopPage<void> onPopPage;
@override
Future<void> setNewRoutePath(RouteInformation configuration) {
_routeInformation = configuration;
return SynchronousFuture<void>(null);
}
bool _handlePopPage(Route<void> route, void data) {
return onPopPage(route, data, this);
}
@override
Widget build(BuildContext context) {
return Navigator(
key: navigatorKey,
onPopPage: _handlePopPage,
pages: <Page<void>>[
// We need at least two pages for the pop to propagate through.
// Otherwise, the navigator will bubble the pop to the system navigator.
MaterialPage<void>(
builder: (BuildContext context) => const Text('base'),
),
MaterialPage<void>(
key: ValueKey<String>(routeInformation?.location),
builder: (BuildContext context) => builder(context, routeInformation),
)
],
);
}
}

View File

@ -264,4 +264,116 @@ void main() {
expect(find.text('non-regular page one'), findsOneWidget); expect(find.text('non-regular page one'), findsOneWidget);
expect(find.text('regular page'), findsNothing); expect(find.text('regular page'), findsNothing);
}); });
testWidgets('WidgetsApp.router works', (WidgetTester tester) async {
final PlatformRouteInformationProvider provider = PlatformRouteInformationProvider(
initialRouteInformation: const RouteInformation(
location: 'initial',
),
);
final SimpleNavigatorRouterDelegate delegate = SimpleNavigatorRouterDelegate(
builder: (BuildContext context, RouteInformation information) {
return Text(information.location);
},
onPopPage: (Route<void> route, void result, SimpleNavigatorRouterDelegate delegate) {
delegate.routeInformation = const RouteInformation(
location: 'popped',
);
return route.didPop(result);
}
);
await tester.pumpWidget(WidgetsApp.router(
routeInformationProvider: provider,
routeInformationParser: SimpleRouteInformationParser(),
routerDelegate: delegate,
color: const Color(0xFF123456),
));
expect(find.text('initial'), findsOneWidget);
// Simulate android back button intent.
final ByteData message = const JSONMethodCodec().encodeMethodCall(const MethodCall('popRoute'));
await ServicesBinding.instance.defaultBinaryMessenger.handlePlatformMessage('flutter/navigation', message, (_) { });
await tester.pumpAndSettle();
expect(find.text('popped'), findsOneWidget);
});
testWidgets('WidgetsApp.router has correct default', (WidgetTester tester) async {
final SimpleNavigatorRouterDelegate delegate = SimpleNavigatorRouterDelegate(
builder: (BuildContext context, RouteInformation information) {
return Text(information.location);
},
);
await tester.pumpWidget(WidgetsApp.router(
routeInformationParser: SimpleRouteInformationParser(),
routerDelegate: delegate,
color: const Color(0xFF123456),
));
expect(find.text('/'), findsOneWidget);
});
}
typedef SimpleRouterDelegateBuilder = Widget Function(BuildContext, RouteInformation);
typedef SimpleNavigatorRouterDelegatePopPage<T> = bool Function(Route<T> route, T result, SimpleNavigatorRouterDelegate delegate);
class SimpleRouteInformationParser extends RouteInformationParser<RouteInformation> {
SimpleRouteInformationParser();
@override
Future<RouteInformation> parseRouteInformation(RouteInformation information) {
return SynchronousFuture<RouteInformation>(information);
}
@override
RouteInformation restoreRouteInformation(RouteInformation configuration) {
return configuration;
}
}
class SimpleNavigatorRouterDelegate extends RouterDelegate<RouteInformation> with PopNavigatorRouterDelegateMixin<RouteInformation>, ChangeNotifier {
SimpleNavigatorRouterDelegate({
@required this.builder,
this.onPopPage,
});
@override
GlobalKey<NavigatorState> navigatorKey = GlobalKey<NavigatorState>();
RouteInformation get routeInformation => _routeInformation;
RouteInformation _routeInformation;
set routeInformation(RouteInformation newValue) {
_routeInformation = newValue;
notifyListeners();
}
SimpleRouterDelegateBuilder builder;
SimpleNavigatorRouterDelegatePopPage<void> onPopPage;
@override
Future<void> setNewRoutePath(RouteInformation configuration) {
_routeInformation = configuration;
return SynchronousFuture<void>(null);
}
bool _handlePopPage(Route<void> route, void data) {
return onPopPage(route, data, this);
}
@override
Widget build(BuildContext context) {
return Navigator(
key: navigatorKey,
onPopPage: _handlePopPage,
pages: <Page<void>>[
// We need at least two pages for the pop to propagate through.
// Otherwise, the navigator will bubble the pop to the system navigator.
MaterialPage<void>(
builder: (BuildContext context) => const Text('base'),
),
MaterialPage<void>(
key: ValueKey<String>(routeInformation?.location),
builder: (BuildContext context) => builder(context, routeInformation),
)
],
);
}
} }

View File

@ -40,6 +40,16 @@ class PushRouteObserver with WidgetsBindingObserver {
} }
} }
class PushRouteInformationObserver with WidgetsBindingObserver {
RouteInformation pushedRouteInformation;
@override
Future<bool> didPushRouteInformation(RouteInformation routeInformation) async {
pushedRouteInformation = routeInformation;
return true;
}
}
void main() { void main() {
setUp(() { setUp(() {
WidgetsFlutterBinding.ensureInitialized(); WidgetsFlutterBinding.ensureInitialized();
@ -90,6 +100,38 @@ void main() {
WidgetsBinding.instance.removeObserver(observer); WidgetsBinding.instance.removeObserver(observer);
}); });
testWidgets('didPushRouteInformation calls didPushRoute by default', (WidgetTester tester) async {
final PushRouteObserver observer = PushRouteObserver();
WidgetsBinding.instance.addObserver(observer);
const Map<String, dynamic> testRouteInformation = <String, dynamic>{
'location': 'testRouteName',
'state': 'state',
'restorationData': <dynamic, dynamic>{'test': 'config'}
};
final ByteData message = const JSONMethodCodec().encodeMethodCall(
const MethodCall('pushRouteInformation', testRouteInformation));
await ServicesBinding.instance.defaultBinaryMessenger.handlePlatformMessage('flutter/navigation', message, (_) { });
expect(observer.pushedRoute, 'testRouteName');
WidgetsBinding.instance.removeObserver(observer);
});
testWidgets('didPushRouteInformation callback', (WidgetTester tester) async {
final PushRouteInformationObserver observer = PushRouteInformationObserver();
WidgetsBinding.instance.addObserver(observer);
const Map<String, dynamic> testRouteInformation = <String, dynamic>{
'location': 'testRouteName',
'state': 'state',
};
final ByteData message = const JSONMethodCodec().encodeMethodCall(
const MethodCall('pushRouteInformation', testRouteInformation));
await ServicesBinding.instance.defaultBinaryMessenger.handlePlatformMessage('flutter/navigation', message, (_) { });
expect(observer.pushedRouteInformation.location, 'testRouteName');
expect(observer.pushedRouteInformation.state, 'state');
WidgetsBinding.instance.removeObserver(observer);
});
testWidgets('Application lifecycle affects frame scheduling', (WidgetTester tester) async { testWidgets('Application lifecycle affects frame scheduling', (WidgetTester tester) async {
final BinaryMessenger defaultBinaryMessenger = ServicesBinding.instance.defaultBinaryMessenger; final BinaryMessenger defaultBinaryMessenger = ServicesBinding.instance.defaultBinaryMessenger;
ByteData message; ByteData message;

View File

@ -36,6 +36,13 @@ class OnTapPage extends StatelessWidget {
} }
} }
Map<String, dynamic> convertRouteInformationToMap(RouteInformation routeInformation) {
return <String, dynamic>{
'location': routeInformation.location,
'state': routeInformation.state,
};
}
void main() { void main() {
testWidgets('Push and Pop should send platform messages', (WidgetTester tester) async { testWidgets('Push and Pop should send platform messages', (WidgetTester tester) async {
final Map<String, WidgetBuilder> routes = <String, WidgetBuilder>{ final Map<String, WidgetBuilder> routes = <String, WidgetBuilder>{
@ -258,4 +265,109 @@ void main() {
}), }),
); );
}); });
testWidgets('PlatformRouteInformationProvider reports URL', (WidgetTester tester) async {
final List<MethodCall> log = <MethodCall>[];
SystemChannels.navigation.setMockMethodCallHandler((MethodCall methodCall) async {
log.add(methodCall);
});
final PlatformRouteInformationProvider provider = PlatformRouteInformationProvider(
initialRouteInformation: const RouteInformation(
location: 'initial',
),
);
final SimpleRouterDelegate delegate = SimpleRouterDelegate(
reportConfiguration: true,
builder: (BuildContext context, RouteInformation information) {
return Text(information.location);
}
);
await tester.pumpWidget(MaterialApp.router(
routeInformationProvider: provider,
routeInformationParser: SimpleRouteInformationParser(),
routerDelegate: delegate,
));
expect(find.text('initial'), findsOneWidget);
// Triggers a router rebuild and verify the route information is reported
// to the web engine.
delegate.routeInformation = const RouteInformation(
location: 'update',
state: 'state',
);
await tester.pump();
expect(find.text('update'), findsOneWidget);
expect(log, hasLength(1));
// TODO(chunhtai): check routeInformationUpdated instead once the engine
// side is done.
expect(
log.last,
isMethodCall('routeInformationUpdated', arguments: <String, dynamic>{
'location': 'update',
'state': 'state',
}),
);
});
}
typedef SimpleRouterDelegateBuilder = Widget Function(BuildContext, RouteInformation);
typedef SimpleRouterDelegatePopRoute = Future<bool> Function();
class SimpleRouteInformationParser extends RouteInformationParser<RouteInformation> {
SimpleRouteInformationParser();
@override
Future<RouteInformation> parseRouteInformation(RouteInformation information) {
return SynchronousFuture<RouteInformation>(information);
}
@override
RouteInformation restoreRouteInformation(RouteInformation configuration) {
return configuration;
}
}
class SimpleRouterDelegate extends RouterDelegate<RouteInformation> with ChangeNotifier {
SimpleRouterDelegate({
@required this.builder,
this.onPopRoute,
this.reportConfiguration = false,
});
RouteInformation get routeInformation => _routeInformation;
RouteInformation _routeInformation;
set routeInformation(RouteInformation newValue) {
_routeInformation = newValue;
notifyListeners();
}
SimpleRouterDelegateBuilder builder;
SimpleRouterDelegatePopRoute onPopRoute;
final bool reportConfiguration;
@override
RouteInformation get currentConfiguration {
if (reportConfiguration)
return routeInformation;
return null;
}
@override
Future<void> setNewRoutePath(RouteInformation configuration) {
_routeInformation = configuration;
return SynchronousFuture<void>(null);
}
@override
Future<bool> popRoute() {
if (onPopRoute != null)
return onPopRoute();
return SynchronousFuture<bool>(true);
}
@override
Widget build(BuildContext context) => builder(context, routeInformation);
} }

View File

@ -0,0 +1,705 @@
// Copyright 2014 The Flutter Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
// @dart = 2.8
import 'package:flutter/foundation.dart';
import 'package:flutter_test/flutter_test.dart';
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
void main() {
testWidgets('Simple router basic functionality - synchronized', (WidgetTester tester) async {
final SimpleRouteInformationProvider provider = SimpleRouteInformationProvider();
provider.value = const RouteInformation(
location: 'initial',
);
await tester.pumpWidget(buildBoilerPlate(
Router<RouteInformation>(
routeInformationProvider: provider,
routeInformationParser: SimpleRouteInformationParser(),
routerDelegate: SimpleRouterDelegate(
builder: (BuildContext context, RouteInformation information) {
return Text(information.location);
}
),
)
));
expect(find.text('initial'), findsOneWidget);
provider.value = const RouteInformation(
location: 'update',
);
await tester.pump();
expect(find.text('initial'), findsNothing);
expect(find.text('update'), findsOneWidget);
});
testWidgets('Simple router basic functionality - asynchronized', (WidgetTester tester) async {
final SimpleRouteInformationProvider provider = SimpleRouteInformationProvider();
provider.value = const RouteInformation(
location: 'initial',
);
final SimpleAsyncRouteInformationParser parser = SimpleAsyncRouteInformationParser();
final SimpleAsyncRouterDelegate delegate = SimpleAsyncRouterDelegate(
builder: (BuildContext context, RouteInformation information) {
if (information == null)
return const Text('waiting');
return Text(information.location);
}
);
await tester.runAsync(() async {
await tester.pumpWidget(buildBoilerPlate(
Router<RouteInformation>(
routeInformationProvider: provider,
routeInformationParser: parser,
routerDelegate: delegate,
)
));
// Future has not yet completed.
expect(find.text('waiting'), findsOneWidget);
await parser.parsingFuture;
await delegate.setNewRouteFuture;
await tester.pump();
expect(find.text('initial'), findsOneWidget);
provider.value = const RouteInformation(
location: 'update',
);
await tester.pump();
// Future has not yet completed.
expect(find.text('initial'), findsOneWidget);
await parser.parsingFuture;
await delegate.setNewRouteFuture;
await tester.pump();
expect(find.text('update'), findsOneWidget);
});
});
testWidgets('Simple router can handle pop route', (WidgetTester tester) async {
final SimpleRouteInformationProvider provider = SimpleRouteInformationProvider();
provider.value = const RouteInformation(
location: 'initial',
);
final BackButtonDispatcher dispatcher = RootBackButtonDispatcher();
await tester.pumpWidget(buildBoilerPlate(
Router<RouteInformation>(
routeInformationProvider: provider,
routeInformationParser: SimpleRouteInformationParser(),
routerDelegate: SimpleRouterDelegate(
builder: (BuildContext context, RouteInformation information) {
return Text(information.location);
},
onPopRoute: () {
provider.value = const RouteInformation(
location: 'popped',
);
return SynchronousFuture<bool>(true);
}
),
backButtonDispatcher: dispatcher,
)
));
expect(find.text('initial'), findsOneWidget);
bool result = false;
// SynchronousFuture should complete immediately.
dispatcher.invokeCallback(SynchronousFuture<bool>(false))
.then((bool data) {
result = data;
});
expect(result, isTrue);
await tester.pump();
expect(find.text('popped'), findsOneWidget);
});
testWidgets('PopNavigatorRouterDelegateMixin works', (WidgetTester tester) async {
final SimpleRouteInformationProvider provider = SimpleRouteInformationProvider();
provider.value = const RouteInformation(
location: 'initial',
);
final BackButtonDispatcher dispatcher = RootBackButtonDispatcher();
final SimpleNavigatorRouterDelegate delegate = SimpleNavigatorRouterDelegate(
builder: (BuildContext context, RouteInformation information) {
return Text(information.location);
},
onPopPage: (Route<void> route, void result) {
provider.value = const RouteInformation(
location: 'popped',
);
return route.didPop(result);
}
);
await tester.pumpWidget(buildBoilerPlate(
Router<RouteInformation>(
routeInformationProvider: provider,
routeInformationParser: SimpleRouteInformationParser(),
routerDelegate: delegate,
backButtonDispatcher: dispatcher,
)
));
expect(find.text('initial'), findsOneWidget);
// Pushes a nameless route.
showDialog<void>(
useRootNavigator: false,
context: delegate.navigatorKey.currentContext,
builder: (BuildContext context) => const Text('dialog')
);
await tester.pumpAndSettle();
expect(find.text('dialog'), findsOneWidget);
// Pops the nameless route and makes sure the initial page is shown.
bool result = false;
result = await dispatcher.invokeCallback(SynchronousFuture<bool>(false));
expect(result, isTrue);
await tester.pumpAndSettle();
expect(find.text('initial'), findsOneWidget);
expect(find.text('dialog'), findsNothing);
// Pops one more time.
result = false;
result = await dispatcher.invokeCallback(SynchronousFuture<bool>(false));
expect(result, isTrue);
await tester.pumpAndSettle();
expect(find.text('popped'), findsOneWidget);
});
testWidgets('Nested routers back button dispatcher works', (WidgetTester tester) async {
final SimpleRouteInformationProvider provider = SimpleRouteInformationProvider();
provider.value = const RouteInformation(
location: 'initial',
);
final BackButtonDispatcher outerDispatcher = RootBackButtonDispatcher();
await tester.pumpWidget(buildBoilerPlate(
Router<RouteInformation>(
backButtonDispatcher: outerDispatcher,
routeInformationProvider: provider,
routeInformationParser: SimpleRouteInformationParser(),
routerDelegate: SimpleRouterDelegate(
builder: (BuildContext context, RouteInformation information) {
final BackButtonDispatcher innerDispatcher = ChildBackButtonDispatcher(outerDispatcher);
innerDispatcher.takePriority();
// Creates the sub-router.
return Router<RouteInformation>(
backButtonDispatcher: innerDispatcher,
routerDelegate: SimpleRouterDelegate(
builder: (BuildContext context, RouteInformation innerInformation) {
return Text(information.location);
},
onPopRoute: () {
provider.value = const RouteInformation(
location: 'popped inner',
);
return SynchronousFuture<bool>(true);
},
),
);
},
onPopRoute: () {
provider.value = const RouteInformation(
location: 'popped outter',
);
return SynchronousFuture<bool>(true);
}
),
)
));
expect(find.text('initial'), findsOneWidget);
// The outer dispatcher should trigger the pop on the inner router.
bool result = false;
result = await outerDispatcher.invokeCallback(SynchronousFuture<bool>(false));
expect(result, isTrue);
await tester.pump();
expect(find.text('popped inner'), findsOneWidget);
});
testWidgets('Nested router back button dispatcher works for multiple children', (WidgetTester tester) async {
final SimpleRouteInformationProvider provider = SimpleRouteInformationProvider();
provider.value = const RouteInformation(
location: 'initial',
);
final BackButtonDispatcher outerDispatcher = RootBackButtonDispatcher();
final BackButtonDispatcher innerDispatcher1 = ChildBackButtonDispatcher(outerDispatcher);
final BackButtonDispatcher innerDispatcher2 = ChildBackButtonDispatcher(outerDispatcher);
await tester.pumpWidget(buildBoilerPlate(
Router<RouteInformation>(
backButtonDispatcher: outerDispatcher,
routeInformationProvider: provider,
routeInformationParser: SimpleRouteInformationParser(),
routerDelegate: SimpleRouterDelegate(
builder: (BuildContext context, RouteInformation information) {
// Creates the sub-router.
return Column(
children: <Widget>[
Text(information.location),
Router<RouteInformation>(
backButtonDispatcher: innerDispatcher1,
routerDelegate: SimpleRouterDelegate(
builder: (BuildContext context, RouteInformation innerInformation) {
return Container();
},
onPopRoute: () {
provider.value = const RouteInformation(
location: 'popped inner1',
);
return SynchronousFuture<bool>(true);
},
),
),
Router<RouteInformation>(
backButtonDispatcher: innerDispatcher2,
routerDelegate: SimpleRouterDelegate(
builder: (BuildContext context, RouteInformation innerInformation) {
return Container();
},
onPopRoute: () {
provider.value = const RouteInformation(
location: 'popped inner2',
);
return SynchronousFuture<bool>(true);
},
),
),
],
);
},
onPopRoute: () {
provider.value = const RouteInformation(
location: 'popped outter',
);
return SynchronousFuture<bool>(true);
}
),
)
));
expect(find.text('initial'), findsOneWidget);
// If none of the children have taken the priority, the root router handles
// the pop.
bool result = false;
result = await outerDispatcher.invokeCallback(SynchronousFuture<bool>(false));
expect(result, isTrue);
await tester.pump();
expect(find.text('popped outter'), findsOneWidget);
innerDispatcher1.takePriority();
result = false;
result = await outerDispatcher.invokeCallback(SynchronousFuture<bool>(false));
expect(result, isTrue);
await tester.pump();
expect(find.text('popped inner1'), findsOneWidget);
// The last child dispatcher that took priority handles the pop.
innerDispatcher2.takePriority();
result = false;
result = await outerDispatcher.invokeCallback(SynchronousFuture<bool>(false));
expect(result, isTrue);
await tester.pump();
expect(find.text('popped inner2'), findsOneWidget);
});
testWidgets('router does report URL change correctly', (WidgetTester tester) async {
RouteInformation reportedRouteInformation;
final SimpleRouteInformationProvider provider = SimpleRouteInformationProvider(
onRouterReport: (RouteInformation information) {
// Makes sure we only report once after manually cleaning up.
expect(reportedRouteInformation, isNull);
reportedRouteInformation = information;
}
);
final SimpleRouterDelegate delegate = SimpleRouterDelegate(
reportConfiguration: true,
builder: (BuildContext context, RouteInformation information) {
return Text(information.location);
}
);
delegate.onPopRoute = () {
delegate.routeInformation = const RouteInformation(
location: 'popped',
);
return SynchronousFuture<bool>(true);
};
final BackButtonDispatcher outerDispatcher = RootBackButtonDispatcher();
provider.value = const RouteInformation(
location: 'initial',
);
await tester.pumpWidget(buildBoilerPlate(
Router<RouteInformation>(
backButtonDispatcher: outerDispatcher,
routeInformationProvider: provider,
routeInformationParser: SimpleRouteInformationParser(),
routerDelegate: delegate,
)
));
expect(find.text('initial'), findsOneWidget);
expect(reportedRouteInformation, isNull);
delegate.routeInformation = const RouteInformation(
location: 'update',
);
await tester.pump();
expect(find.text('initial'), findsNothing);
expect(find.text('update'), findsOneWidget);
expect(reportedRouteInformation.location, 'update');
// The router should not report if only state changes.
reportedRouteInformation = null;
delegate.routeInformation = const RouteInformation(
location: 'update',
state: 'another state',
);
await tester.pump();
expect(find.text('update'), findsOneWidget);
expect(reportedRouteInformation, isNull);
reportedRouteInformation = null;
bool result = false;
result = await outerDispatcher.invokeCallback(SynchronousFuture<bool>(false));
expect(result, isTrue);
await tester.pump();
expect(find.text('popped'), findsOneWidget);
expect(reportedRouteInformation.location, 'popped');
});
testWidgets('router can be forced to recognize or ignore navigating events', (WidgetTester tester) async {
RouteInformation reportedRouteInformation;
bool isNavigating = false;
RouteInformation nextRouteInformation;
final SimpleRouteInformationProvider provider = SimpleRouteInformationProvider(
onRouterReport: (RouteInformation information) {
// Makes sure we only report once after manually cleaning up.
expect(reportedRouteInformation, isNull);
reportedRouteInformation = information;
}
);
provider.value = const RouteInformation(
location: 'initial',
);
final SimpleRouterDelegate delegate = SimpleRouterDelegate(reportConfiguration: true);
delegate.builder = (BuildContext context, RouteInformation information) {
return ElevatedButton(
child: Text(information.location),
onPressed: () {
if (isNavigating) {
Router.navigate(context, () {
if (delegate.routeInformation != nextRouteInformation)
delegate.routeInformation = nextRouteInformation;
});
} else {
Router.neglect(context, () {
if (delegate.routeInformation != nextRouteInformation)
delegate.routeInformation = nextRouteInformation;
});
}
},
);
};
final BackButtonDispatcher outerDispatcher = RootBackButtonDispatcher();
await tester.pumpWidget(buildBoilerPlate(
Router<RouteInformation>(
backButtonDispatcher: outerDispatcher,
routeInformationProvider: provider,
routeInformationParser: SimpleRouteInformationParser(),
routerDelegate: delegate,
)
));
expect(find.text('initial'), findsOneWidget);
expect(reportedRouteInformation, isNull);
nextRouteInformation = const RouteInformation(
location: 'update',
);
await tester.tap(find.byType(ElevatedButton));
await tester.pump();
expect(find.text('initial'), findsNothing);
expect(find.text('update'), findsOneWidget);
expect(reportedRouteInformation, isNull);
isNavigating = true;
// This should not trigger any real navigating event because the
// nextRouteInformation does not change. However, the router should still
// report a route information because isNavigating = true.
await tester.tap(find.byType(ElevatedButton));
await tester.pump();
expect(reportedRouteInformation.location, 'update');
});
testWidgets('PlatformRouteInformationProvider works', (WidgetTester tester) async {
final RouteInformationProvider provider = PlatformRouteInformationProvider(
initialRouteInformation: const RouteInformation(
location: 'initial',
),
);
final SimpleRouterDelegate delegate = SimpleRouterDelegate(
builder: (BuildContext context, RouteInformation information) {
final List<Widget> children = <Widget>[];
if (information.location != null)
children.add(Text(information.location));
if (information.state != null)
children.add(Text(information.state.toString()));
return Column(
children: children,
);
}
);
await tester.pumpWidget(MaterialApp.router(
routeInformationProvider: provider,
routeInformationParser: SimpleRouteInformationParser(),
routerDelegate: delegate,
));
expect(find.text('initial'), findsOneWidget);
// Pushes through the `pushRouteInformation` in the navigation method channel.
const Map<String, dynamic> testRouteInformation = <String, dynamic>{
'location': 'testRouteName',
'state': 'state',
};
final ByteData routerMessage = const JSONMethodCodec().encodeMethodCall(
const MethodCall('pushRouteInformation', testRouteInformation)
);
await ServicesBinding.instance.defaultBinaryMessenger.handlePlatformMessage('flutter/navigation', routerMessage, (_) { });
await tester.pump();
expect(find.text('testRouteName'), findsOneWidget);
expect(find.text('state'), findsOneWidget);
// Pushes through the `pushRoute` in the navigation method channel.
const String testRouteName = 'newTestRouteName';
final ByteData message = const JSONMethodCodec().encodeMethodCall(
const MethodCall('pushRoute', testRouteName));
await ServicesBinding.instance.defaultBinaryMessenger.handlePlatformMessage('flutter/navigation', message, (_) { });
await tester.pump();
expect(find.text('newTestRouteName'), findsOneWidget);
});
testWidgets('RootBackButtonDispatcher works', (WidgetTester tester) async {
final BackButtonDispatcher outerDispatcher = RootBackButtonDispatcher();
final RouteInformationProvider provider = PlatformRouteInformationProvider(
initialRouteInformation: const RouteInformation(
location: 'initial',
),
);
final SimpleRouterDelegate delegate = SimpleRouterDelegate(
reportConfiguration: true,
builder: (BuildContext context, RouteInformation information) {
return Text(information.location);
}
);
delegate.onPopRoute = () {
delegate.routeInformation = const RouteInformation(
location: 'popped',
);
return SynchronousFuture<bool>(true);
};
await tester.pumpWidget(MaterialApp.router(
backButtonDispatcher: outerDispatcher,
routeInformationProvider: provider,
routeInformationParser: SimpleRouteInformationParser(),
routerDelegate: delegate,
));
expect(find.text('initial'), findsOneWidget);
// Pop route through the message channel.
final ByteData message = const JSONMethodCodec().encodeMethodCall(const MethodCall('popRoute'));
await ServicesBinding.instance.defaultBinaryMessenger.handlePlatformMessage('flutter/navigation', message, (_) { });
await tester.pump();
expect(find.text('popped'), findsOneWidget);
});
}
Widget buildBoilerPlate(Widget child) {
return MaterialApp(
home: Scaffold(
body: child,
),
);
}
typedef SimpleRouterDelegateBuilder = Widget Function(BuildContext, RouteInformation);
typedef SimpleRouterDelegatePopRoute = Future<bool> Function();
typedef SimpleNavigatorRouterDelegatePopPage<T> = bool Function(Route<T> route, T result);
typedef RouterReportRouterInformation = void Function(RouteInformation);
class SimpleRouteInformationParser extends RouteInformationParser<RouteInformation> {
SimpleRouteInformationParser();
@override
Future<RouteInformation> parseRouteInformation(RouteInformation information) {
return SynchronousFuture<RouteInformation>(information);
}
@override
RouteInformation restoreRouteInformation(RouteInformation configuration) {
return configuration;
}
}
class SimpleRouterDelegate extends RouterDelegate<RouteInformation> with ChangeNotifier {
SimpleRouterDelegate({
this.builder,
this.onPopRoute,
this.reportConfiguration = false,
});
RouteInformation get routeInformation => _routeInformation;
RouteInformation _routeInformation;
set routeInformation(RouteInformation newValue) {
_routeInformation = newValue;
notifyListeners();
}
SimpleRouterDelegateBuilder builder;
SimpleRouterDelegatePopRoute onPopRoute;
final bool reportConfiguration;
@override
RouteInformation get currentConfiguration {
if (reportConfiguration)
return routeInformation;
return null;
}
@override
Future<void> setNewRoutePath(RouteInformation configuration) {
_routeInformation = configuration;
return SynchronousFuture<void>(null);
}
@override
Future<bool> popRoute() {
if (onPopRoute != null)
return onPopRoute();
return SynchronousFuture<bool>(true);
}
@override
Widget build(BuildContext context) => builder(context, routeInformation);
}
class SimpleNavigatorRouterDelegate extends RouterDelegate<RouteInformation> with PopNavigatorRouterDelegateMixin<RouteInformation>, ChangeNotifier {
SimpleNavigatorRouterDelegate({
@required this.builder,
this.onPopPage,
});
@override
GlobalKey<NavigatorState> navigatorKey = GlobalKey<NavigatorState>();
RouteInformation get routeInformation => _routeInformation;
RouteInformation _routeInformation;
set routeInformation(RouteInformation newValue) {
_routeInformation = newValue;
notifyListeners();
}
SimpleRouterDelegateBuilder builder;
SimpleNavigatorRouterDelegatePopPage<void> onPopPage;
@override
Future<void> setNewRoutePath(RouteInformation configuration) {
_routeInformation = configuration;
return SynchronousFuture<void>(null);
}
bool _handlePopPage(Route<void> route, void data) {
return onPopPage(route, data);
}
@override
Widget build(BuildContext context) {
return Navigator(
key: navigatorKey,
onPopPage: _handlePopPage,
pages: <Page<void>>[
// We need at least two pages for the pop to propagate through.
// Otherwise, the navigator will bubble the pop to the system navigator.
MaterialPage<void>(
builder: (BuildContext context) => const Text('base'),
),
MaterialPage<void>(
key: ValueKey<String>(routeInformation?.location),
builder: (BuildContext context) => builder(context, routeInformation),
)
],
);
}
}
class SimpleRouteInformationProvider extends RouteInformationProvider with ChangeNotifier {
SimpleRouteInformationProvider({
this.onRouterReport
});
RouterReportRouterInformation onRouterReport;
@override
RouteInformation get value => _value;
RouteInformation _value;
set value(RouteInformation newValue) {
_value = newValue;
notifyListeners();
}
@override
void routerReportsNewRouteInformation(RouteInformation routeInformation) {
if (onRouterReport != null)
onRouterReport(routeInformation);
}
}
class SimpleAsyncRouteInformationParser extends RouteInformationParser<RouteInformation> {
SimpleAsyncRouteInformationParser();
Future<RouteInformation> parsingFuture;
@override
Future<RouteInformation> parseRouteInformation(RouteInformation information) {
return parsingFuture = Future<RouteInformation>.value(information);
}
@override
RouteInformation restoreRouteInformation(RouteInformation configuration) {
return configuration;
}
}
class SimpleAsyncRouterDelegate extends RouterDelegate<RouteInformation> with ChangeNotifier{
SimpleAsyncRouterDelegate({
@required this.builder,
});
RouteInformation get routeInformation => _routeInformation;
RouteInformation _routeInformation;
set routeInformation(RouteInformation newValue) {
_routeInformation = newValue;
notifyListeners();
}
SimpleRouterDelegateBuilder builder;
Future<void> setNewRouteFuture;
@override
Future<void> setNewRoutePath(RouteInformation configuration) {
_routeInformation = configuration;
return setNewRouteFuture = Future<void>.value();
}
@override
Future<bool> popRoute() {
return Future<bool>.value(true);
}
@override
Widget build(BuildContext context) => builder(context, routeInformation);
}