Unverified Commit f9fd71bc authored by chunhtai's avatar chunhtai Committed by GitHub

Implement Router widget and widgets app api (#60299)

parent 12b8d9db
......@@ -104,6 +104,50 @@ class CupertinoApp extends StatefulWidget {
assert(checkerboardOffscreenLayers != null),
assert(showSemanticsDebugger != 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);
/// {@macro flutter.widgets.widgetsApp.navigatorKey}
......@@ -143,6 +187,18 @@ class CupertinoApp extends StatefulWidget {
/// {@macro flutter.widgets.widgetsApp.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}
final TransitionBuilder builder;
......@@ -286,6 +342,7 @@ class _AlwaysCupertinoScrollBehavior extends ScrollBehavior {
class _CupertinoAppState extends State<CupertinoApp> {
HeroController _heroController;
bool get _usesRouter => widget.routerDelegate != null;
@override
void initState() {
......@@ -304,6 +361,83 @@ class _CupertinoAppState extends State<CupertinoApp> {
yield DefaultCupertinoLocalizations.delegate;
}
Widget _inspectorSelectButtonBuilder(BuildContext context, VoidCallback onPressed) {
return CupertinoButton.filled(
child: const Icon(
CupertinoIcons.search,
size: 28.0,
color: CupertinoColors.white,
),
padding: EdgeInsets.zero,
onPressed: onPressed,
);
}
WidgetsApp _buildWidgetApp(BuildContext context) {
final CupertinoThemeData effectiveThemeData = CupertinoTheme.of(context);
final Color color = CupertinoDynamicColor.resolve(widget.color ?? effectiveThemeData.primaryColor, context);
if (_usesRouter) {
return WidgetsApp.router(
key: GlobalObjectKey(this),
routeInformationProvider: widget.routeInformationProvider,
routeInformationParser: widget.routeInformationParser,
routerDelegate: widget.routerDelegate,
backButtonDispatcher: widget.backButtonDispatcher,
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,
);
}
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();
......@@ -314,53 +448,11 @@ class _CupertinoAppState extends State<CupertinoApp> {
data: CupertinoUserInterfaceLevelData.base,
child: CupertinoTheme(
data: effectiveThemeData,
child: Builder(
builder: (BuildContext context) {
return HeroControllerScope(
controller: _heroController,
child: WidgetsApp(
key: GlobalObjectKey(this),
navigatorKey: widget.navigatorKey,
navigatorObservers: widget.navigatorObservers,
pageRouteBuilder: <T>(RouteSettings settings, WidgetBuilder builder) =>
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: CupertinoTheme.of(context).textTheme.textStyle,
color: CupertinoDynamicColor.resolve(widget.color ?? effectiveThemeData.primaryColor, context),
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: (BuildContext context, VoidCallback onPressed) {
return CupertinoButton.filled(
child: const Icon(
CupertinoIcons.search,
size: 28.0,
color: CupertinoColors.white,
),
padding: EdgeInsets.zero,
onPressed: onPressed,
);
},
shortcuts: widget.shortcuts,
actions: widget.actions,
),
);
},
child: HeroControllerScope(
controller: _heroController,
child: Builder(
builder: _buildWidgetApp,
),
),
),
),
......
......@@ -7,6 +7,7 @@
import 'dart:ui' as ui;
import 'package:flutter/cupertino.dart';
import 'package:flutter/foundation.dart';
import 'package:flutter/rendering.dart';
import 'package:flutter/widgets.dart';
......@@ -205,6 +206,58 @@ class MaterialApp extends StatefulWidget {
assert(checkerboardOffscreenLayers != null),
assert(showSemanticsDebugger != 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);
/// {@macro flutter.widgets.widgetsApp.navigatorKey}
......@@ -238,6 +291,18 @@ class MaterialApp extends StatefulWidget {
/// {@macro flutter.widgets.widgetsApp.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}
///
/// Material specific features such as [showDialog] and [showMenu], and widgets
......@@ -611,6 +676,8 @@ class _MaterialScrollBehavior extends ScrollBehavior {
class _MaterialAppState extends State<MaterialApp> {
HeroController _heroController;
bool get _usesRouter => widget.routerDelegate != null;
@override
void initState() {
super.initState();
......@@ -629,75 +696,77 @@ class _MaterialAppState extends State<MaterialApp> {
yield DefaultCupertinoLocalizations.delegate;
}
@override
Widget build(BuildContext context) {
Widget result = HeroControllerScope(
controller: _heroController,
child: WidgetsApp(
Widget _inspectorSelectButtonBuilder(BuildContext context, VoidCallback onPressed) {
return FloatingActionButton(
child: const Icon(Icons.search),
onPressed: onPressed,
mini: true,
);
}
Widget _materialBuilder(BuildContext context, Widget child) {
// Resolve which theme to use based on brightness and high contrast.
final ThemeMode mode = widget.themeMode ?? ThemeMode.system;
final Brightness platformBrightness = MediaQuery.platformBrightnessOf(context);
final bool useDarkTheme = mode == ThemeMode.dark
|| (mode == ThemeMode.system && platformBrightness == ui.Brightness.dark);
final bool highContrast = MediaQuery.highContrastOf(context);
ThemeData theme;
if (useDarkTheme && highContrast && widget.highContrastDarkTheme != null) {
theme = widget.highContrastDarkTheme;
} else if (useDarkTheme && widget.darkTheme != null) {
theme = widget.darkTheme;
} else if (highContrast && widget.highContrastTheme != null) {
theme = widget.highContrastTheme;
}
theme ??= widget.theme ?? ThemeData.light();
return AnimatedTheme(
data: theme,
isMaterialAppTheme: true,
child: widget.builder != null
? Builder(
builder: (BuildContext context) {
// Why are we surrounding a builder with a builder?
//
// The widget.builder may contain code that invokes
// Theme.of(), which should return the theme we selected
// above in AnimatedTheme. However, if we invoke
// widget.builder() directly as the child of AnimatedTheme
// then there is no Context separating them, and the
// widget.builder() will not find the theme. Therefore, we
// surround widget.builder with yet another builder so that
// a context separates them and Theme.of() correctly
// resolves to the theme we passed to AnimatedTheme.
return widget.builder(context, child);
},
)
: child,
);
}
Widget _buildWidgetApp(BuildContext context) {
// The color property is always pulled from the light theme, even if dark
// mode is activated. This was done to simplify the technical details
// 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
// Android's switcher UI.
//
// blue is the primary color of the default theme.
final Color materialColor = widget.color ?? widget.theme?.primaryColor ?? Colors.blue;
if (_usesRouter) {
return WidgetsApp.router(
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: (BuildContext context, Widget child) {
// Resolve which theme to use based on brightness and high contrast.
final ThemeMode mode = widget.themeMode ?? ThemeMode.system;
final Brightness platformBrightness = MediaQuery.platformBrightnessOf(context);
final bool useDarkTheme = mode == ThemeMode.dark
|| (mode == ThemeMode.system && platformBrightness == ui.Brightness.dark);
final bool highContrast = MediaQuery.highContrastOf(context);
ThemeData theme;
if (useDarkTheme && highContrast && widget.highContrastDarkTheme != null) {
theme = widget.highContrastDarkTheme;
} else if (useDarkTheme && widget.darkTheme != null) {
theme = widget.darkTheme;
} else if (highContrast && widget.highContrastTheme != null) {
theme = widget.highContrastTheme;
}
theme ??= widget.theme ?? ThemeData.light();
return AnimatedTheme(
data: theme,
isMaterialAppTheme: true,
child: widget.builder != null
? Builder(
builder: (BuildContext context) {
// Why are we surrounding a builder with a builder?
//
// The widget.builder may contain code that invokes
// Theme.of(), which should return the theme we selected
// above in AnimatedTheme. However, if we invoke
// widget.builder() directly as the child of AnimatedTheme
// then there is no Context separating them, and the
// widget.builder() will not find the theme. Therefore, we
// surround widget.builder with yet another builder so that
// a context separates them and Theme.of() correctly
// resolves to the theme we passed to AnimatedTheme.
return widget.builder(context, child);
},
)
: child,
);
},
routeInformationProvider: widget.routeInformationProvider,
routeInformationParser: widget.routeInformationParser,
routerDelegate: widget.routerDelegate,
backButtonDispatcher: widget.backButtonDispatcher,
builder: _materialBuilder,
title: widget.title,
onGenerateTitle: widget.onGenerateTitle,
textStyle: _errorTextStyle,
// The color property is always pulled from the light theme, even if dark
// mode is activated. This was done to simplify the technical details
// 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
// Android's switcher UI.
//
// blue is the primary color of the default theme
color: widget.color ?? widget.theme?.primaryColor ?? Colors.blue,
color: materialColor,
locale: widget.locale,
localizationsDelegates: _localizationsDelegates,
localeResolutionCallback: widget.localeResolutionCallback,
......@@ -708,17 +777,49 @@ class _MaterialAppState extends State<MaterialApp> {
checkerboardOffscreenLayers: widget.checkerboardOffscreenLayers,
showSemanticsDebugger: widget.showSemanticsDebugger,
debugShowCheckedModeBanner: widget.debugShowCheckedModeBanner,
inspectorSelectButtonBuilder: (BuildContext context, VoidCallback onPressed) {
return FloatingActionButton(
child: const Icon(Icons.search),
onPressed: onPressed,
mini: true,
);
},
inspectorSelectButtonBuilder: _inspectorSelectButtonBuilder,
shortcuts: widget.shortcuts,
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(() {
if (widget.debugShowMaterialGrid) {
......@@ -735,7 +836,10 @@ class _MaterialAppState extends State<MaterialApp> {
return ScrollConfiguration(
behavior: _MaterialScrollBehavior(),
child: result,
child: HeroControllerScope(
controller: _heroController,
child: result,
)
);
}
}
......@@ -26,16 +26,18 @@ class SystemChannels {
/// * `pushRoute`, which is called with a single string argument when the
/// 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
/// framework notifies the engine about the route changes.
///
/// * `routePushed`, which is called when a route is pushed. (e.g. A modal
/// replaces the entire screen.)
///
/// * `routePopped`, which is called when a route is popped. (e.g. A dialog,
/// such as time picker is closed.)
/// * `routeUpdated`, which is called when current route has changed.
///
/// * `routeReplaced`, which is called when a route is replaced.
/// * `routeInformationUpdated`, which is called by the [Router] when the
/// application navigate to a new location.
///
/// See also:
///
......@@ -46,7 +48,7 @@ class SystemChannels {
/// [Navigator.push], [Navigator.pushReplacement], [Navigator.pop] and
/// [Navigator.replace], utilize this channel's methods to send route
/// change information from framework to engine.
static const MethodChannel navigation = MethodChannel(
static const MethodChannel navigation = OptionalMethodChannel(
'flutter/navigation',
JSONMethodCodec(),
);
......
......@@ -35,4 +35,37 @@ class SystemNavigator {
static Future<void> pop({bool? animated}) async {
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,
},
);
}
}
......@@ -22,6 +22,7 @@ import 'media_query.dart';
import 'navigator.dart';
import 'pages.dart';
import 'performance_overlay.dart';
import 'router.dart';
import 'scrollable.dart';
import 'semantics_debugger.dart';
import 'shortcuts.dart';
......@@ -260,6 +261,62 @@ class WidgetsApp extends StatefulWidget {
assert(showSemanticsDebugger != null),
assert(debugShowCheckedModeBanner != 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);
/// {@template flutter.widgets.widgetsApp.navigatorKey}
......@@ -321,6 +378,71 @@ class WidgetsApp extends StatefulWidget {
/// or a [CupertinoPageRoute] should be used for building page transitions.
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}
/// The widget for the default route of the app ([Navigator.defaultRouteName],
/// which is `/`).
......@@ -960,10 +1082,21 @@ class WidgetsApp extends StatefulWidget {
class _WidgetsAppState extends State<WidgetsApp> with WidgetsBindingObserver {
// 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
void initState() {
super.initState();
_updateNavigator();
if (_usesRouter) {
_updateRouter();
} else {
_updateNavigator();
}
_locale = _resolveLocales(WidgetsBinding.instance.window.locales, widget.supportedLocales);
WidgetsBinding.instance.addObserver(this);
}
......@@ -971,16 +1104,37 @@ class _WidgetsAppState extends State<WidgetsApp> with WidgetsBindingObserver {
@override
void didUpdateWidget(WidgetsApp oldWidget) {
super.didUpdateWidget(oldWidget);
if (widget.navigatorKey != oldWidget.navigatorKey)
if (oldWidget.routeInformationProvider != widget.routeInformationProvider) {
_updateRouter();
}
if (widget.navigatorKey != oldWidget.navigatorKey) {
_updateNavigator();
}
}
@override
void dispose() {
WidgetsBinding.instance.removeObserver(this);
_defaultRouteInformationProvider?.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
GlobalKey<NavigatorState> _navigator;
......@@ -1050,6 +1204,11 @@ class _WidgetsAppState extends State<WidgetsApp> with WidgetsBindingObserver {
@override
Future<bool> didPopRoute() async {
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;
if (navigator == null)
return false;
......@@ -1059,6 +1218,11 @@ class _WidgetsAppState extends State<WidgetsApp> with WidgetsBindingObserver {
@override
Future<bool> didPushRoute(String route) async {
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;
if (navigator == null)
return false;
......@@ -1291,16 +1455,20 @@ class _WidgetsAppState extends State<WidgetsApp> with WidgetsBindingObserver {
@override
Widget build(BuildContext context) {
Widget navigator;
if (_navigator != null) {
navigator = Navigator(
Widget routing;
if (_usesRouter) {
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,
// If window.defaultRouteName isn't '/', we should assume it was set
// 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,
initialRoute: _initialRouteName,
onGenerateRoute: _onGenerateRoute,
onGenerateInitialRoutes: widget.onGenerateInitialRoutes == null
? Navigator.defaultGenerateInitialRoutes
......@@ -1317,12 +1485,12 @@ class _WidgetsAppState extends State<WidgetsApp> with WidgetsBindingObserver {
if (widget.builder != null) {
result = Builder(
builder: (BuildContext context) {
return widget.builder(context, navigator);
return widget.builder(context, routing);
},
);
} else {
assert(navigator != null);
result = navigator;
assert(routing != null);
result = routing;
}
if (widget.textStyle != null) {
......
......@@ -18,6 +18,7 @@ import 'app.dart';
import 'debug.dart';
import 'focus_manager.dart';
import 'framework.dart';
import 'router.dart';
import 'widget_inspector.dart';
export 'dart:ui' show AppLifecycleState, Locale;
......@@ -95,7 +96,7 @@ abstract class WidgetsBindingObserver {
/// [SystemChannels.navigation].
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.
///
/// Observers are expected to return true if they were able to
......@@ -106,6 +107,22 @@ abstract class WidgetsBindingObserver {
/// [SystemChannels.navigation].
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,
/// 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) {
switch (methodCall.method) {
case 'popRoute':
return handlePopRoute();
case 'pushRoute':
return handlePushRoute(methodCall.arguments as String);
case 'pushRouteInformation':
return _handlePushRouteInformation(methodCall.arguments as Map<dynamic, dynamic>);
}
return Future<dynamic>.value();
}
......
......@@ -21,7 +21,6 @@ import 'focus_scope.dart';
import 'framework.dart';
import 'heroes.dart';
import 'overlay.dart';
import 'route_notification_messages.dart';
import 'routes.dart';
import 'ticker_provider.dart';
......@@ -3308,8 +3307,10 @@ class NavigatorState extends State<Navigator> with TickerProviderStateMixin {
_RouteEntry.isPresentPredicate, orElse: () => null);
final String routeName = lastEntry?.route?.settings?.name;
if (routeName != _lastAnnouncedRouteName) {
RouteNotificationMessages.maybeNotifyRouteChange(
routeName, _lastAnnouncedRouteName);
SystemNavigator.routeUpdated(
routeName: routeName,
previousRouteName: _lastAnnouncedRouteName
);
_lastAnnouncedRouteName = routeName;
}
}
......
// 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,
},
);
}
}
// 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 'dart:async';
import 'dart:collection';
import 'package:flutter/foundation.dart';
import 'package:flutter/scheduler.dart';
import 'package:flutter/services.dart';
import 'basic.dart';
import 'binding.dart';
import 'framework.dart';
import 'navigator.dart';
/// A piece of routing information.
///
/// The route information consists of a location string of the application and
/// a state object that configures the application in that location.
///
/// This information flows two ways, from the [RouteInformationProvider] to the
/// [Router] or from the [Router] to [RouteInformationProvider].
///
/// In the former case, the [RouteInformationProvider] notifies the [Router]
/// widget when a new [RouteInformation] is available. The [Router] widget takes
/// these information and navigates accordingly.
///
/// The latter case should only happen in a web application where the [Router]
/// reports route change back to web engine.
class RouteInformation {
/// Creates a route information.
const RouteInformation({this.location, this.state});
/// The location of the application.
///
/// The string is usually in the format of multiple string identifiers with
/// slashes in between. ex: `/`, `/path`, `/path/to/the/app`.
///
/// It is equivalent to the URL in a web application.
final String location;
/// The state of the application in the [location].
///
/// The app can have different states even in the same location. For example
/// the text inside a [TextField] or the scroll position in a [ScrollView],
/// these widget states can be stored in the [state].
///
/// It's only used in the web application currently. In a web application,
/// this property is stored into browser history entry when the [Router]
/// report this route information back to the web engine through the
/// [PlatformRouteInformationProvider], so we can get the url along with state
/// back when the user click the forward or backward buttons.
///
/// The state must be serializable.
final Object state;
}
/// The dispatcher for opening and closing pages of an application.
///
/// This widget listens for routing information from the operating system (e.g.
/// an initial route provided on app startup, a new route obtained when an
/// intent is received, or a notification that the user hit the system back
/// button), parses route information into data of type `T`, and then converts
/// that data into [Page] objects that it passes to a [Navigator].
///
/// Additionally, every single part of that previous sentence can be overridden
/// and configured as desired.
///
/// The [routeInformationProvider] can be overridden to change how the name of
/// the route is obtained. the [RouteInformationProvider.value] when the
/// [Router] is first created is used as the initial route, and subsequent
/// notifications from the [RouteInformationProvider] to its listeners are
/// treated as notifications that the route information has changed.
///
/// The [backButtonDispatcher] can be overridden to change how back button
/// notifications are received. This must be a [BackButtonDispatcher], which is
/// an object where callbacks can be registered, and which can be chained
/// so that back button presses are delegated to subsidiary routers. The
/// callbacks are invoked to indicate that the user is trying to close the
/// current route (by pressing the system back button); the [Router] ensures
/// that when this callback is invoked, the message is passed to the
/// [routerDelegate] and its result is provided back to the
/// [backButtonDispatcher]. Some platforms don't have back buttons and on those
/// platforms it is completely normal that this notification is never sent. The
/// common [backButtonDispatcher] for root router is an instance of
/// [RootBackButtonDispatcher], which uses a [WidgetsBindingObserver] to listen
/// to the `popRoute` notifications from [SystemChannels.navigation]. A
/// common alternative is [ChildBackButtonDispatcher], which must be provided
/// the [BackButtonDispatcher] of its ancestor [Router] (available via
/// [Router.of]).
///
/// The [routeInformationParser] can be overridden to change how names obtained
/// from the [routeInformationProvider] are interpreted. It must implement the
/// [RouteInformationParser] interface, specialized with the same type as the
/// [Router] itself. This type, `T`, represents the data type that the
/// [routeInformationParser] will generate.
///
/// The [routerDelegate] can be overridden to change how the output of the
/// [routeInformationParser] is interpreted. It must implement the
/// [RouterDelegate] interface, also specialized with `T`; it takes as input
/// the data (of type `T`) from the [routeInformationParser], and is responsible
/// for providing a navigating widget to insert into the widget tree. The
/// [RouterDelegate] interface is also [Listenable]; notifications are taken
/// to mean that the [Router] needs to rebuild.
///
/// ## Concerns regarding asynchrony
///
/// Some of the APIs (notably those involving [RouteInformationParser] and
/// [RouterDelegate]) are asynchronous.
///
/// When developing objects implementing these APIs, if the work can be done
/// entirely synchronously, then consider using [SynchronousFuture] for the
/// future returned from the relevant methods. This will allow the [Router] to
/// proceed in a completely synchronous way, which removes a number of
/// complications.
///
/// Using asynchronous computation is entirely reasonable, however, and the API
/// is designed to support it. For example, maybe a set of images need to be
/// loaded before a route can be shown; waiting for those images to be loaded
/// before [RouterDelegate.setNewRoutePath] returns is a reasonable approach to
/// handle this case.
///
/// If an asynchronous operation is ongoing when a new one is to be started, the
/// precise behavior will depend on the exact circumstances, as follows:
///
/// If the active operation is a [routeInformationParser] parsing a new route information:
/// that operation's result, if it ever completes, will be discarded.
///
/// If the active operation is a [routerDelegate] handling a pop request:
/// the previous pop is immediately completed with "false", claiming that the
/// previous pop was not handled (this may cause the application to close).
///
/// If the active operation is a [routerDelegate] handling an initial route
/// or a pushed route, the result depends on the new operation. If the new
/// operation is a pop request, then the original operation's result, if it ever
/// completes, will be discarded. If the new operation is a push request,
/// however, the [routeInformationParser] will be requested to start the parsing, and
/// only if that finishes before the original [routerDelegate] request
/// completes will that original request's result be discarded.
///
/// If the identity of the [Router] widget's delegates change while an
/// asynchronous operation is in progress, to keep matters simple, all active
/// asynchronous operations will have their results discarded. It is generally
/// considered unusual for these delegates to change during the lifetime of the
/// [Router].
///
/// If the [Router] itself is disposed while an an asynchronous operation is in
/// progress, all active asynchronous operations will have their results
/// discarded also.
///
/// No explicit signals are provided to the [routeInformationParser] or
/// [routerDelegate] to indicate when any of the above happens, so it is
/// strongly recommended that [RouteInformationParser] and [RouterDelegate]
/// implementations not perform extensive computation.
///
/// ## Application architectural design
///
/// An application can have zero, one, or many [Router] widgets, depending on
/// its needs.
///
/// An application might have no [Router] widgets if it has only one "screen",
/// or if the facilities provided by [Navigator] are sufficient.
///
/// A particularly elaborate application might have multiple [Router] widgets,
/// in a tree configuration, with the first handling the entire route parsing
/// and making the result available for routers in the subtree. The routers in
/// the subtree do not participate in route information parsing but merely take the
/// result from the first router to build their sub routes.
///
/// Most applications only need a single [Router].
///
/// ## URL updates for web applications
///
/// In the web platform, it is important to keeps the URL up to date with the
/// app state. This ensures the browser constructs its history entry
/// correctly so that its forward and backward buttons continue to work.
///
/// If the [routeInformationProvider] is a [PlatformRouteInformationProvider]
/// and a app state change leads to [Router] rebuilds, the [Router] will detect
/// such a event and retrieve the new route information from the
/// [RouterDelegate.currentConfiguration] and the
/// [RouteInformationParser.restoreRouteInformation]. If the location in the
/// new route information is different from the current location, the router
/// sends the new route information to the engine through the
/// [PlatformRouteInformationProvider.routerReportsNewRouteInformation].
///
/// By Providing implementations of these two methods in the subclasses and using
/// the [PlatformRouteInformationProvider], you can enable the [Router] widget to
/// update the URL in the browser automatically.
///
/// You can force the [Router] to report the new route information back to the
/// engine even if the [RouteInformation.location] has not changed. By calling
/// the [Router.navigate], the [Router] will be forced to report the route
/// information back to the engine after running the callback. This is useful
/// when you want to support the browser backward and forward buttons without
/// changing the URL. For example, the scroll position of a scroll view may be
/// saved in the [RouteInformation.state]. If you use the [Router.navigate] to
/// update the scroll position, the browser will create a new history entry with
/// the [RouteInformation.state] that stores the new scroll position. when the
/// users click the backward button, the browser will go back to previous scroll
/// position without changing the url bar.
///
/// You can also force the [Router] to ignore a one time route information
/// update by providing a one time app state update in a callback and pass it
/// into the [Router.neglect]. The [Router] will not report any route
/// information even if it detects location change as a result of running the
/// callback. This is particularly useful when you don't want the browser to
/// create a browser history entry for this app state update.
///
/// You can also choose to opt out of URL updates entirely. Simply ignore the
/// [RouterDelegate.currentConfiguration] and the
/// [RouteInformationParser.restoreRouteInformation] without providing the
/// implementations will prevent the [Router] from reporting the URL back to the
/// web engine. This is not recommended in general, but You may decide to opt
/// out in these cases:
///
/// * If you are not writing a web application.
///
/// * If you have multiple router widgets in your app, then only one router
/// widget should update the URL (Usually the top-most one created by the
/// [WidgetsApp.router]/[MaterialApp.router]/[CupertinoApp.router]).
///
/// * If your app does not care about the in-app navigation using the browser's
/// forward and backward buttons.
///
/// Otherwise, we strongly recommend implementing the
/// [RouterDelegate.currentConfiguration] and the
/// [RouteInformationParser.restoreRouteInformation] to provide optimal
/// user experience in the web application.
class Router<T> extends StatefulWidget {
/// Creates a router.
///
/// The [routeInformationProvider] and [routeInformationParser] can be null if this
/// router does not depend on route information. A common example is a sub router
/// that builds its content completely relies on the app state.
///
/// If the [routeInformationProvider] is not null, the [routeInformationParser] must
/// also not be null.
///
/// The [routerDelegate] must not be null.
const Router({
Key key,
this.routeInformationProvider,
this.routeInformationParser,
@required this.routerDelegate,
this.backButtonDispatcher,
}) : assert(routeInformationProvider == null || routeInformationParser != null),
assert(routerDelegate != null),
super(key: key);
/// The route information provider for the router.
///
/// The value at the time of first build will be used as the initial route.
/// The [Router] listens to this provider and rebuilds with new names when
/// it notifies.
///
/// This can be null if this router does not rely on the route information
/// to build its content. In such case, the [routeInformationParser] can also be
/// null.
final RouteInformationProvider routeInformationProvider;
/// The route information parser for the router.
///
/// When the [Router] gets a new route information from the [routeInformationProvider],
/// the [Router] uses this delegate to parse the route information and produce a
/// configuration. The configuration will be used by [routerDelegate] and
/// eventually rebuilds the [Router] widget.
///
/// Since this delegate is the primary consumer of the [routeInformationProvider],
/// it must not be null if [routeInformationProvider] is not null.
final RouteInformationParser<T> routeInformationParser;
/// The router delegate for the router.
///
/// This delegate consumes the configuration from [routeInformationParser] and
/// builds a navigating widget for the [Router].
///
/// It is also the primary respondent for the [backButtonDispatcher]. The
/// [Router] relies on the [RouterDelegate.popRoute] to handles the back
/// button intends.
///
/// If the [RouterDelegate.currentConfiguration] returns a non-null object,
/// this [Router] will opt for URL updates.
final RouterDelegate<T> routerDelegate;
/// The back button dispatcher for the router.
///
/// The two common alternatives are the [RootBackButtonDispatcher] for root
/// router, or the [ChildBackButtonDispatcher] for other routers.
final BackButtonDispatcher backButtonDispatcher;
/// Retrieves the immediate [Router] ancestor from the given context.
///
/// Use this method when you need to access the delegates in the [Router].
/// For example, you need to access the [backButtonDispatcher] of the parent
/// router to create a [ChildBackButtonDispatcher] for a nested router.
/// Another use case may be updating the value in [routeInformationProvider]
/// to navigate to a new route.
static Router<dynamic> of(BuildContext context) {
final _RouterScope scope = context.dependOnInheritedWidgetOfExactType<_RouterScope>();
assert(scope != null);
return scope.routerState.widget;
}
/// Forces the [Router] to run the [callback] and reports the route
/// information back to the engine.
///
/// The web application relies on the [Router] to report new route information
/// in order to create browser history entry. The [Router] will only report
/// them if it detects the [RouteInformation.location] changes. Use this
/// method if you want the [Router] to report the route information even if
/// the location does not change. This can be useful when you want to
/// support the browser backward and forward button without changing the URL.
///
/// For example, you can store certain state such as the scroll position into
/// the [RouteInformation.state]. If you use this method to update the
/// scroll position multiple times with the same URL, the browser will create
/// a stack of new history entries with the same URL but different
/// [RouteInformation.state]s that store the new scroll positions. If the user
/// click the backward button in the browser, the browser will restore the
/// scroll positions saved in history entries without changing the URL.
///
/// See also:
///
/// * [Router]: see the "URL updates for web applications" section for more
/// information about route information reporting.
/// * [neglect]: which forces the [Router] to not report the route
/// information even if location does change.
static void navigate(BuildContext context, VoidCallback callback) {
final _RouterScope scope = context
.getElementForInheritedWidgetOfExactType<_RouterScope>()
.widget as _RouterScope;
scope.routerState._setStateWithExplicitReportStatus(_IntentionToReportRouteInformation.must, callback);
}
/// Forces the [Router] to to run the [callback] without reporting the route
/// information back to the engine.
///
/// Use this method if you don't want the [Router] to report the new route
/// information even if it detects changes as a result of running the
/// [callback].
///
/// The web application relies on the [Router] to report new route information
/// in order to create browser history entry. The [Router] will report them
/// automatically if it detects the [RouteInformation.location] changes. You
/// can use this method if you want to navigate to a new route without
/// creating the browser history entry.
///
/// See also:
///
/// * [Router]: see the "URL updates for web applications" section for more
/// information about route information reporting.
/// * [navigate]: which forces the [Router] to report the route information
/// even if location does not change.
static void neglect(BuildContext context, VoidCallback callback) {
final _RouterScope scope = context
.getElementForInheritedWidgetOfExactType<_RouterScope>()
.widget as _RouterScope;
scope.routerState._setStateWithExplicitReportStatus(_IntentionToReportRouteInformation.ignore, callback);
}
@override
State<Router<T>> createState() => _RouterState<T>();
}
typedef _AsyncPassthrough<Q> = Future<Q> Function(Q);
// Whether to report the route information in this build cycle.
enum _IntentionToReportRouteInformation {
// We haven't receive any signal on whether to report.
none,
// Report if route information changes.
maybe,
// Report regardless of route information changes.
must,
// Don't report regardless of route information changes.
ignore,
}
class _RouterState<T> extends State<Router<T>> {
Object _currentRouteInformationParserTransaction;
Object _currentRouterDelegateTransaction;
_IntentionToReportRouteInformation _currentIntentionToReport;
@override
void initState() {
super.initState();
widget.routeInformationProvider?.addListener(_handleRouteInformationProviderNotification);
widget.backButtonDispatcher?.addCallback(_handleBackButtonDispatcherNotification);
widget.routerDelegate.addListener(_handleRouterDelegateNotification);
_currentIntentionToReport = _IntentionToReportRouteInformation.none;
if (widget.routeInformationProvider != null) {
_processInitialRoute();
}
_lastSeenLocation = widget.routeInformationProvider?.value?.location;
}
bool _routeInformationReportingTaskScheduled = false;
String _lastSeenLocation;
void _scheduleRouteInformationReportingTask() {
if (_routeInformationReportingTaskScheduled)
return;
assert(_currentIntentionToReport != _IntentionToReportRouteInformation.none);
_routeInformationReportingTaskScheduled = true;
SchedulerBinding.instance.addPostFrameCallback(_reportRouteInformation);
}
void _reportRouteInformation(Duration timestamp) {
assert(_routeInformationReportingTaskScheduled);
_routeInformationReportingTaskScheduled = false;
switch (_currentIntentionToReport) {
case _IntentionToReportRouteInformation.none:
assert(false);
return;
case _IntentionToReportRouteInformation.ignore:
// In the ignore case, we still want to update the _lastSeenLocation.
final RouteInformation routeInformation = _retrieveNewRouteInformation();
if (routeInformation != null) {
_lastSeenLocation = routeInformation.location;
}
_currentIntentionToReport = _IntentionToReportRouteInformation.none;
return;
case _IntentionToReportRouteInformation.maybe:
final RouteInformation routeInformation = _retrieveNewRouteInformation();
if (routeInformation != null) {
if (_lastSeenLocation != routeInformation.location) {
widget.routeInformationProvider.routerReportsNewRouteInformation(routeInformation);
_lastSeenLocation = routeInformation.location;
}
}
_currentIntentionToReport = _IntentionToReportRouteInformation.none;
return;
case _IntentionToReportRouteInformation.must:
final RouteInformation routeInformation = _retrieveNewRouteInformation();
if (routeInformation != null) {
widget.routeInformationProvider.routerReportsNewRouteInformation(routeInformation);
_lastSeenLocation = routeInformation.location;
}
_currentIntentionToReport = _IntentionToReportRouteInformation.none;
return;
}
}
RouteInformation _retrieveNewRouteInformation() {
final T configuration = widget.routerDelegate.currentConfiguration;
if (configuration == null)
return null;
final RouteInformation routeInformation = widget.routeInformationParser.restoreRouteInformation(configuration);
assert((){
if (routeInformation == null) {
FlutterError.reportError(
const FlutterErrorDetails(
exception:
'Router.routeInformationParser returns a null RouteInformation. '
'If you opt for route information reporting, the '
'routeInformationParser must not report null for a given '
'configuration.'
),
);
}
return true;
}());
return routeInformation;
}
void _setStateWithExplicitReportStatus(
_IntentionToReportRouteInformation status,
VoidCallback fn,
) {
assert(status != null);
assert(status.index >= _IntentionToReportRouteInformation.must.index);
assert(() {
if (_currentIntentionToReport.index >= _IntentionToReportRouteInformation.must.index &&
_currentIntentionToReport != status) {
FlutterError.reportError(
const FlutterErrorDetails(
exception:
'Both Router.navigate and Router.neglect have been called in this '
'build cycle, and the Router cannot decide whether to report the '
'route information. Please make sure only one of them is called '
'within the same build cycle.'
),
);
}
return true;
}());
_currentIntentionToReport = status;
_scheduleRouteInformationReportingTask();
fn();
}
void _maybeNeedToReportRouteInformation() {
_currentIntentionToReport = _currentIntentionToReport != _IntentionToReportRouteInformation.none
? _currentIntentionToReport
: _IntentionToReportRouteInformation.maybe;
_scheduleRouteInformationReportingTask();
}
@override
void didChangeDependencies() {
super.didChangeDependencies();
_maybeNeedToReportRouteInformation();
}
@override
void didUpdateWidget(Router<T> oldWidget) {
super.didUpdateWidget(oldWidget);
if (widget.routeInformationProvider != oldWidget.routeInformationProvider ||
widget.backButtonDispatcher != oldWidget.backButtonDispatcher ||
widget.routeInformationParser != oldWidget.routeInformationParser ||
widget.routerDelegate != oldWidget.routerDelegate) {
_currentRouteInformationParserTransaction = Object();
_currentRouterDelegateTransaction = Object();
}
if (widget.routeInformationProvider != oldWidget.routeInformationProvider) {
oldWidget.routeInformationProvider?.removeListener(_handleRouteInformationProviderNotification);
widget.routeInformationProvider?.addListener(_handleRouteInformationProviderNotification);
if (oldWidget.routeInformationProvider?.value != widget.routeInformationProvider?.value) {
_handleRouteInformationProviderNotification();
}
}
if (widget.backButtonDispatcher != oldWidget.backButtonDispatcher) {
oldWidget.backButtonDispatcher?.removeCallback(_handleBackButtonDispatcherNotification);
widget.backButtonDispatcher?.addCallback(_handleBackButtonDispatcherNotification);
}
if (widget.routerDelegate != oldWidget.routerDelegate) {
oldWidget.routerDelegate.removeListener(_handleRouterDelegateNotification);
widget.routerDelegate.addListener(_handleRouterDelegateNotification);
_maybeNeedToReportRouteInformation();
}
}
@override
void dispose() {
widget.routeInformationProvider?.removeListener(_handleRouteInformationProviderNotification);
widget.backButtonDispatcher?.removeCallback(_handleBackButtonDispatcherNotification);
widget.routerDelegate.removeListener(_handleRouterDelegateNotification);
_currentRouteInformationParserTransaction = null;
_currentRouterDelegateTransaction = null;
super.dispose();
}
void _processInitialRoute() {
_currentRouteInformationParserTransaction = Object();
_currentRouterDelegateTransaction = Object();
widget.routeInformationParser
.parseRouteInformation(widget.routeInformationProvider.value)
.then<T>(_verifyRouteInformationParserStillCurrent(_currentRouteInformationParserTransaction, widget))
.then<void>(widget.routerDelegate.setInitialRoutePath)
.then<void>(_verifyRouterDelegatePushStillCurrent(_currentRouterDelegateTransaction, widget))
.then<void>(_rebuild);
}
void _handleRouteInformationProviderNotification() {
_currentRouteInformationParserTransaction = Object();
_currentRouterDelegateTransaction = Object();
widget.routeInformationParser
.parseRouteInformation(widget.routeInformationProvider.value)
.then<T>(_verifyRouteInformationParserStillCurrent(_currentRouteInformationParserTransaction, widget))
.then<void>(widget.routerDelegate.setNewRoutePath)
.then<void>(_verifyRouterDelegatePushStillCurrent(_currentRouterDelegateTransaction, widget))
.then<void>(_rebuild);
}
Future<bool> _handleBackButtonDispatcherNotification() {
_currentRouteInformationParserTransaction = Object();
_currentRouterDelegateTransaction = Object();
return widget.routerDelegate
.popRoute()
.then<bool>(_verifyRouterDelegatePopStillCurrent(_currentRouterDelegateTransaction, widget))
.then<bool>((bool data) {
_rebuild();
_maybeNeedToReportRouteInformation();
return SynchronousFuture<bool>(data);
});
}
static final Future<dynamic> _never = Completer<dynamic>().future; // won't ever complete
_AsyncPassthrough<T> _verifyRouteInformationParserStillCurrent(Object transaction, Router<T> originalWidget) {
return (T data) {
if (transaction == _currentRouteInformationParserTransaction &&
widget.routeInformationProvider == originalWidget.routeInformationProvider &&
widget.backButtonDispatcher == originalWidget.backButtonDispatcher &&
widget.routeInformationParser == originalWidget.routeInformationParser &&
widget.routerDelegate == originalWidget.routerDelegate) {
return SynchronousFuture<T>(data);
}
return _never as Future<T>;
};
}
_AsyncPassthrough<void> _verifyRouterDelegatePushStillCurrent(Object transaction, Router<T> originalWidget) {
return (void data) {
if (transaction == _currentRouterDelegateTransaction &&
widget.routeInformationProvider == originalWidget.routeInformationProvider &&
widget.backButtonDispatcher == originalWidget.backButtonDispatcher &&
widget.routeInformationParser == originalWidget.routeInformationParser &&
widget.routerDelegate == originalWidget.routerDelegate)
return SynchronousFuture<void>(data);
return _never;
};
}
_AsyncPassthrough<bool> _verifyRouterDelegatePopStillCurrent(Object transaction, Router<T> originalWidget) {
return (bool data) {
if (transaction == _currentRouterDelegateTransaction &&
widget.routeInformationProvider == originalWidget.routeInformationProvider &&
widget.backButtonDispatcher == originalWidget.backButtonDispatcher &&
widget.routeInformationParser == originalWidget.routeInformationParser &&
widget.routerDelegate == originalWidget.routerDelegate) {
return SynchronousFuture<bool>(data);
}
// A rebuilt was trigger from a different source. Returns true to
// prevent bubbling.
return SynchronousFuture<bool>(true);
};
}
Future<void> _rebuild([void value]) {
setState(() {/* routerDelegate is ready to rebuild */});
return SynchronousFuture<void>(value);
}
void _handleRouterDelegateNotification() {
setState(() {/* routerDelegate wants to rebuild */});
_maybeNeedToReportRouteInformation();
}
@override
Widget build(BuildContext context) {
return _RouterScope(
routeInformationProvider: widget.routeInformationProvider,
backButtonDispatcher: widget.backButtonDispatcher,
routeInformationParser: widget.routeInformationParser,
routerDelegate: widget.routerDelegate,
routerState: this,
child: Builder(
// We use a Builder so that the build method below
// will have a BuildContext that contains the _RouterScope.
builder: widget.routerDelegate.build,
),
);
}
}
class _RouterScope extends InheritedWidget {
const _RouterScope({
Key key,
@required this.routeInformationProvider,
@required this.backButtonDispatcher,
@required this.routeInformationParser,
@required this.routerDelegate,
@required this.routerState,
@required Widget child,
}) : assert(routeInformationProvider == null || routeInformationParser != null),
assert(routerDelegate != null),
assert(routerState != null),
super(key: key, child: child);
final ValueListenable<RouteInformation> routeInformationProvider;
final BackButtonDispatcher backButtonDispatcher;
final RouteInformationParser<dynamic> routeInformationParser;
final RouterDelegate<dynamic> routerDelegate;
final _RouterState<dynamic> routerState;
@override
bool updateShouldNotify(_RouterScope oldWidget) {
return routeInformationProvider != oldWidget.routeInformationProvider ||
backButtonDispatcher != oldWidget.backButtonDispatcher ||
routeInformationParser != oldWidget.routeInformationParser ||
routerDelegate != oldWidget.routerDelegate ||
routerState != oldWidget.routerState;
}
}
/// A class that can be extended or mixed in that invokes a single callback,
/// which then returns a value.
///
/// While multiple callbacks can be registered, when a notification is
/// dispatched there must be only a single callback. The return values of
/// multiple callbacks are not aggregated.
///
/// `T` is the return value expected from the callback.
///
/// See also:
///
/// * [Listenable] and its subclasses, which provide a similar mechanism for
/// one-way signalling.
class _CallbackHookProvider<T> {
final ObserverList<ValueGetter<T>> _callbacks = ObserverList<ValueGetter<T>>();
/// Whether a callback is currently registered.
@protected
bool get hasCallbacks => _callbacks.isNotEmpty;
/// Register the callback to be called when the object changes.
///
/// If other callbacks have already been registered, they must be removed
/// (with [removeCallback]) before the callback is next called.
void addCallback(ValueGetter<T> callback) => _callbacks.add(callback);
/// Remove a previously registered callback.
///
/// If the given callback is not registered, the call is ignored.
void removeCallback(ValueGetter<T> callback) => _callbacks.remove(callback);
/// Calls the (single) registered callback and returns its result.
///
/// If no callback is registered, or if the callback throws, returns
/// `defaultValue`.
///
/// Call this method whenever the callback is to be invoked. If there is more
/// than one callback registered, this method will throw a [StateError].
///
/// Exceptions thrown by callbacks will be caught and reported using
/// [FlutterError.reportError].
@protected
T invokeCallback(T defaultValue) {
if (_callbacks.isEmpty)
return defaultValue;
try {
return _callbacks.single();
} catch (exception, stack) {
FlutterError.reportError(FlutterErrorDetails(
exception: exception,
stack: stack,
library: 'widget library',
context: ErrorDescription('while invoking the callback for $runtimeType'),
informationCollector: () sync* {
yield DiagnosticsProperty<_CallbackHookProvider<T>>(
'The $runtimeType that invoked the callback was:',
this,
style: DiagnosticsTreeStyle.errorProperty,
);
},
));
return defaultValue;
}
}
}
/// Report to a [Router] when the user taps the back button on platforms that
/// support back buttons (such as Android).
///
/// When [Router] widgets are nested, consider using a
/// [ChildBackButtonDispatcher], passing it the parent [BackButtonDispatcher],
/// so that the back button requests get dispatched to the appropriate [Router].
/// To make this work properly, it's important that whenever a [Router] thinks
/// it should get the back button messages (e.g. after the user taps inside it),
/// it calls [takePriority] on its [BackButtonDispatcher] (or
/// [ChildBackButtonDispatcher]) instance.
///
/// The class takes a single callback, which must return a [Future<bool>]. The
/// callback's semantics match [WidgetsBindingObserver.didPopRoute]'s, namely,
/// the callback should return a future that completes to true if it can handle
/// the pop request, and a future that completes to false otherwise.
abstract class BackButtonDispatcher extends _CallbackHookProvider<Future<bool>> {
LinkedHashSet<ChildBackButtonDispatcher> _children;
@override
bool get hasCallbacks => super.hasCallbacks || (_children != null && _children.isNotEmpty);
/// Handles a pop route request.
///
/// This method prioritizes the children list in reverse order and calls
/// [ChildBackButtonDispatcher.notifiedByParent] on them. If any of them
/// handles the request (by returning a future with true), it exits this
/// method by returning this future. Otherwise, it keeps moving on to the next
/// child until a child handles the request. If none of the children handles
/// the request, this back button dispatcher will then try to handle the request
/// by itself. This back button dispatcher handles the request by notifying the
/// router which in turn calls the [RouterDelegate.popRoute] and returns its
/// result.
///
/// To decide whether this back button dispatcher will handle the pop route
/// request, you can override the [RouterDelegate.popRoute] of the router
/// delegate you pass into the router with this back button dispatcher to
/// return a future of true or false.
@override
Future<bool> invokeCallback(Future<bool> defaultValue) {
if (_children != null && _children.isNotEmpty) {
final List<ChildBackButtonDispatcher> children = _children.toList();
int childIndex = children.length - 1;
Future<bool> notifyNextChild(bool result) {
// If the previous child handles the callback, we returns the result.
if (result)
return SynchronousFuture<bool>(result);
// If the previous child did not handle the callback, we ask the next
// child to handle the it.
if (childIndex > 0) {
childIndex -= 1;
return children[childIndex]
.notifiedByParent(defaultValue)
.then<bool>(notifyNextChild);
}
// If none of the child handles the callback, the parent will then handle it.
return super.invokeCallback(defaultValue);
}
return children[childIndex]
.notifiedByParent(defaultValue)
.then<bool>(notifyNextChild);
}
return super.invokeCallback(defaultValue);
}
/// Creates a [ChildBackButtonDispatcher] that is a direct descendant of this
/// back button dispatcher.
///
/// To participate in handling the pop route request, call the [takePriority]
/// on the [ChildBackButtonDispatcher] created from this method.
///
/// When the pop route request is handled by this back button dispatcher, it
/// propagate the request to its direct descendants that have called the
/// [takePriority] method. If there are multiple candidates, the latest one
/// that called the [takePriority] wins the right to handle the request. If
/// the latest one does not handle the request (by returning a future of
/// false in [ChildBackButtonDispatcher.notifiedByParent]), the second latest
/// one will then have the right to handle the request. This dispatcher
/// continues finding the next candidate until there are no more candidates
/// and finally handles the request itself.
ChildBackButtonDispatcher createChildBackButtonDispatcher() {
return ChildBackButtonDispatcher(this);
}
/// Make this [BackButtonDispatcher] take priority among its peers.
///
/// This has no effect when a [BackButtonDispatcher] has no parents and no
/// children. If a [BackButtonDispatcher] does have parents or children,
/// however, it causes this object to be the one to dispatch the notification
/// when the parent would normally notify its callback.
///
/// The [BackButtonDispatcher] must have a listener registered before it can
/// be told to take priority.
void takePriority() {
if (_children != null)
_children.clear();
}
/// Mark the given child as taking priority over this object and the other
/// children.
///
/// This causes [invokeCallback] to defer to the given child instead of
/// calling this object's callback.
///
/// Children are stored in a list, so that if the current child is removed
/// using [forget], a previous child will return to take its place. When
/// [takePriority] is called, the list is cleared.
///
/// Calling this again without first calling [forget] moves the child back to
/// the head of the list.
///
// (Actually it moves it to the end of the list and we treat the end of the
// list to be the priority end, but that's an implementation detail.)
//
/// The [BackButtonDispatcher] must have a listener registered before it can
/// be told to defer to a child.
void deferTo(ChildBackButtonDispatcher child) {
assert(hasCallbacks);
_children ??= <ChildBackButtonDispatcher>{} as LinkedHashSet<ChildBackButtonDispatcher>;
_children.remove(child); // child may or may not be in the set already
_children.add(child);
}
/// Causes the given child to be removed from the list of children to which
/// this object might defer, as if [deferTo] had never been called for that
/// child.
///
/// This should only be called once per child, even if [deferTo] was called
/// multiple times for that child.
///
/// If no children are left in the list, this object will stop deferring to
/// its children. (This is not the same as calling [takePriority], since, if
/// this object itself is a [ChildBackButtonDispatcher], [takePriority] would
/// additionally attempt to claim priority from its parent, whereas removing
/// the last child does not.)
void forget(ChildBackButtonDispatcher child) {
assert(_children != null);
assert(_children.contains(child));
_children.remove(child);
}
}
/// The default implementation of back button dispatcher for the root router.
///
/// This dispatcher listens to platform pop route notifications. When the
/// platform wants to pop the current route, this dispatcher calls the
/// [BackButtonDispatcher.invokeCallback] method to handle the request.
class RootBackButtonDispatcher extends BackButtonDispatcher with WidgetsBindingObserver {
/// Create a root back button dispatcher.
RootBackButtonDispatcher();
@override
void addCallback(ValueGetter<Future<bool>> callback) {
if (!hasCallbacks)
WidgetsBinding.instance.addObserver(this);
super.addCallback(callback);
}
@override
void removeCallback(ValueGetter<Future<bool>> callback) {
super.removeCallback(callback);
if (!hasCallbacks)
WidgetsBinding.instance.removeObserver(this);
}
@override
Future<bool> didPopRoute() => invokeCallback(Future<bool>.value(false));
}
/// A variant of [BackButtonDispatcher] which listens to notifications from a
/// parent back button dispatcher, and can take priority from its parent for the
/// handling of such notifications.
///
/// Useful when [Router]s are being nested within each other.
///
/// Use [Router.of] to obtain a reference to the nearest ancestor [Router], from
/// which the [Router.backButtonDispatcher] can be found, and then used as the
/// [parent] of the [ChildBackButtonDispatcher].
class ChildBackButtonDispatcher extends BackButtonDispatcher {
/// Creates a back button dispatcher that acts as the child of another.
///
/// The [parent] must not be null.
ChildBackButtonDispatcher(this.parent) : assert(parent != null);
/// The back button dispatcher that this object will attempt to take priority
/// over when [takePriority] is called.
///
/// The parent must have a listener registered before this child object can
/// have its [takePriority] or [deferTo] methods used.
final BackButtonDispatcher parent;
/// The parent of this child back button dispatcher decide to let this
/// child to handle the invoke the callback request in
/// [BackButtonDispatcher.invokeCallback].
///
/// Return a boolean future with true if this child will handle the request;
/// otherwise, return a boolean future with false.
@protected
Future<bool> notifiedByParent(Future<bool> defaultValue) {
return invokeCallback(defaultValue);
}
@override
void takePriority() {
parent.deferTo(this);
super.takePriority();
}
@override
void deferTo(ChildBackButtonDispatcher child) {
assert(hasCallbacks);
super.deferTo(child);
}
@override
void removeCallback(ValueGetter<Future<bool>> callback) {
super.removeCallback(callback);
if (!hasCallbacks)
parent.forget(this);
}
}
/// A delegate that is used by the [Router] widget to parse a route information
/// into a configuration of type T.
///
/// This delegate is used when the [Router] widget is first built with initial
/// route information from [Router.routeInformationProvider] and any subsequent
/// new route notifications from it. The [parseRouteInformation] widget calls
/// the [parseRouteInformation] with the route information.
abstract class RouteInformationParser<T> {
/// Abstract const constructor. This constructor enables subclasses to provide
/// const constructors so that they can be used in const expressions.
const RouteInformationParser();
/// Converts the given route information into parsed data to pass to a
/// [RouterDelegate].
///
/// The method should return a future which completes when the parsing is
/// complete. The parsing may be asynchronous if, e.g., the parser needs to
/// communicate with the OEM thread to obtain additional data about the route.
///
/// Consider using a [SynchronousFuture] if the result can be computed
/// synchronously, so that the [Router] does not need to wait for the next
/// microtask to pass the data to the [RouterDelegate].
Future<T> parseRouteInformation(RouteInformation routeInformation);
/// Restore the route information from the given configuration.
///
/// This is not required if you do not opt for the route information reporting
/// , which is used for updating browser history for the web application. If
/// you decides to opt in, you must also overrides this method to return a
/// route information.
///
/// In practice, the [parseRouteInformation] method must produce an equivalent
/// configuration when passed this method's return value
RouteInformation restoreRouteInformation(T configuration) => null;
}
/// A delegate that is used by the [Router] widget to build and configure a
/// navigating widget.
///
/// This delegate is the core piece of the [Router] widget. It responds to
/// push route and pop route intent from the engine and notifies the [Router]
/// to rebuild. It also act as a builder for the [Router] widget and builds a
/// navigating widget, typically a [Navigator], when the [Router] widget
/// builds.
///
/// When engine pushes a new route, the route information is parsed by the
/// [RouteInformationParser] to produce a configuration of type T. The router
/// delegate receives the configuration through [setInitialRoutePath] or
/// [setNewRoutePath] to configure itself and builds the latest navigating
/// widget upon asked.
///
/// When implementing subclass, consider defining a listenable app state to be
/// used for building the navigating widget. The router delegate should update
/// the app state accordingly and notify the listener know the app state has
/// changed when it receive route related engine intents (e.g.
/// [setNewRoutePath], [setInitialRoutePath], or [popRoute]).
///
/// All subclass must implement [setNewRoutePath], [popRoute], and [build].
///
/// See also:
///
/// * [RouteInformationParser], which is responsible for parsing the route
/// information to a configuration before passing in to router delegate.
/// * [Router], which is the widget that wires all the delegates together to
/// provide a fully functional routing solution.
abstract class RouterDelegate<T> extends Listenable {
/// Called by the [Router] at startup with the structure that the
/// [RouteInformationParser] obtained from parsing the initial route.
///
/// This should configure the [RouterDelegate] so that when [build] is
/// invoked, it will create a widget tree that matches the initial route.
///
/// By default, this method forwards the [configuration] to [setNewRoutePath].
///
/// Consider using a [SynchronousFuture] if the result can be computed
/// synchronously, so that the [Router] does not need to wait for the next
/// microtask to schedule a build.
Future<void> setInitialRoutePath(T configuration) {
return setNewRoutePath(configuration);
}
/// Called by the [Router] when the [Router.routeInformationProvider] reports that a
/// new route has been pushed to the application by the operating system.
///
/// Consider using a [SynchronousFuture] if the result can be computed
/// synchronously, so that the [Router] does not need to wait for the next
/// microtask to schedule a build.
Future<void> setNewRoutePath(T configuration);
/// Called by the [Router] when the [Router.backButtonDispatcher] reports that
/// the operating system is requesting that the current route be popped.
///
/// The method should return a boolean [Future] to indicate whether this
/// delegate handles the request. Returning false will cause the entire app
/// to be popped.
///
/// Consider using a [SynchronousFuture] if the result can be computed
/// synchronously, so that the [Router] does not need to wait for the next
/// microtask to schedule a build.
Future<bool> popRoute();
/// Called by the [Router] when it detects a route information may have
/// changed as a result of rebuild.
///
/// If this getter returns non-null, the [Router] will start to report new
/// route information back to the engine. In web applications, the new
/// route information is used for populating browser history in order to
/// support the forward and the backward buttons.
///
/// When overriding this method, the configuration returned by this getter
/// must be able to construct the current app state and build the widget
/// with the same configuration in the [build] method if it is passed back
/// to the the [setNewRoutePath]. Otherwise, the browser backward and forward
/// buttons will not work properly.
///
/// By default, this getter returns null, which prevents the [Router] from
/// reporting the route information. To opt in, a subclass can override this
/// getter to return the current configuration.
///
/// At most one [Router] can opt in to route information reporting. Typically,
/// only the top-most [Router] created by [WidgetsApp.router] should opt for
/// route information reporting.
T get currentConfiguration => null;
/// Called by the [Router] to obtain the widget tree that represents the
/// current state.
///
/// This is called whenever the [setInitialRoutePath] method's future
/// completes, the [setNewRoutePath] method's future completes with the value
/// true, the [popRoute] method's future completes with the value true, or
/// this object notifies its clients (see the [Listenable] interface, which
/// this interface includes). In addition, it may be called at other times. It
/// is important, therefore, that the methods above do not update the state
/// that the [build] method uses before they complete their respective
/// futures.
///
/// Typically this method returns a suitably-configured [Navigator]. If you do
/// plan to create a navigator, consider using the
/// [PopNavigatorRouterDelegateMixin].
///
/// This method must not return null.
///
/// The `context` is the [Router]'s build context.
Widget build(BuildContext context);
}
/// A route information provider that provides route information for the
/// [Router] widget
///
/// This provider is responsible for handing the route information through [value]
/// getter and notifies listeners, typically the [Router] widget, when a new
/// route information is available.
///
/// When the router opts for the route information reporting (by overrides the
/// [RouterDelegate.currentConfiguration] to return non-null), overrides the
/// [routerReportsNewRouteInformation] method to process the route information.
///
/// See also:
///
/// * [PlatformRouteInformationProvider], which wires up the itself with the
/// [WidgetsBindingObserver.didPushRoute] to propagate platform push route
/// intent to the [Router] widget, as well as reports new route information
/// from the [Router] back to the engine by overriding the
/// [routerReportsNewRouteInformation].
abstract class RouteInformationProvider extends ValueListenable<RouteInformation> {
/// A callback called when the [Router] widget detects any navigation event
/// due to state changes.
///
/// The subclasses can override this method to update theirs values or trigger
/// other side effects. For example, the [PlatformRouteInformationProvider]
/// overrides this method to report the route information back to the engine.
///
/// The [routeInformation] is the new route information after the navigation
/// event.
void routerReportsNewRouteInformation(RouteInformation routeInformation) {}
}
/// The route information provider that propagates the platform route information changes.
///
/// This provider also reports the new route information from the [Router] widget
/// back to engine using message channel method, the
/// [SystemNavigator.routeInformationUpdated].
class PlatformRouteInformationProvider extends RouteInformationProvider with WidgetsBindingObserver, ChangeNotifier {
/// Create a platform route information provider.
///
/// Use the [initialRouteInformation] to set the default route information for this
/// provider.
PlatformRouteInformationProvider({
RouteInformation initialRouteInformation
}) : _value = initialRouteInformation;
@override
void routerReportsNewRouteInformation(RouteInformation routeInformation) {
SystemNavigator.routeInformationUpdated(
location: routeInformation.location,
state: routeInformation.state,
);
_value = routeInformation;
}
@override
RouteInformation get value => _value;
RouteInformation _value;
void _platformReportsNewRouteInformation(RouteInformation routeInformation) {
if (_value == routeInformation)
return;
_value = routeInformation;
notifyListeners();
}
@override
void addListener(VoidCallback listener) {
if (!hasListeners)
WidgetsBinding.instance.addObserver(this);
super.addListener(listener);
}
@override
void removeListener(VoidCallback listener) {
super.removeListener(listener);
if (!hasListeners)
WidgetsBinding.instance.removeObserver(this);
}
@override
void dispose() {
// In practice, this will rarely be called. We assume that the listeners
// will be added and removed in a coherent fashion such that when the object
// is no longer being used, there's no listener, and so it will get garbage
// collected.
if (hasListeners)
WidgetsBinding.instance.removeObserver(this);
super.dispose();
}
@override
Future<bool> didPushRouteInformation(RouteInformation routeInformation) async {
assert(hasListeners);
_platformReportsNewRouteInformation(routeInformation);
return true;
}
@override
Future<bool> didPushRoute(String route) async {
assert(hasListeners);
_platformReportsNewRouteInformation(RouteInformation(location: route));
return true;
}
}
/// A mixin that wires [RouterDelegate.popRoute] to the [Navigator] it builds.
///
/// This mixin calls [Navigator.maybePop] when it receives an Android back
/// button intent through the [RouterDelegate.popRoute]. Using this mixin
/// guarantees that the back button still respects pageless routes in the
/// navigator.
///
/// Only use this mixin if you plan to build a navigator in the
/// [RouterDelegate.build].
mixin PopNavigatorRouterDelegateMixin<T> on RouterDelegate<T> {
/// The key used for retrieving the current navigator.
///
/// When using this mixin, be sure to use this key to create the navigator.
GlobalKey<NavigatorState> get navigatorKey;
@override
Future<bool> popRoute() {
final NavigatorState navigator = navigatorKey?.currentState;
if (navigator == null)
return SynchronousFuture<bool>(false);
return navigator.maybePop();
}
}
......@@ -85,6 +85,7 @@ export 'src/widgets/primary_scroll_controller.dart';
export 'src/widgets/raw_keyboard_listener.dart';
export 'src/widgets/restoration.dart';
export 'src/widgets/restoration_properties.dart';
export 'src/widgets/router.dart';
export 'src/widgets/routes.dart';
export 'src/widgets/safe_area.dart';
export 'src/widgets/scroll_activity.dart';
......
......@@ -6,6 +6,8 @@
import 'package:flutter_test/flutter_test.dart';
import 'package:flutter/cupertino.dart';
import 'package:flutter/foundation.dart';
import 'package:flutter/services.dart';
void main() {
testWidgets('Heroes work', (WidgetTester tester) async {
......@@ -147,4 +149,101 @@ void main() {
expect(key2.currentState, isA<NavigatorState>());
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),
)
],
);
}
}
......@@ -4,7 +4,9 @@
// @dart = 2.8
import 'package:flutter/foundation.dart';
import 'package:flutter/semantics.dart';
import 'package:flutter/services.dart';
import 'package:flutter_test/flutter_test.dart';
import 'package:flutter/cupertino.dart';
import 'package:flutter/material.dart';
......@@ -947,6 +949,37 @@ void main() {
expect(key2.currentState, isA<NavigatorState>());
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 {
......@@ -968,3 +1001,69 @@ class MockAccessibilityFeature implements AccessibilityFeatures {
@override
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),
)
],
);
}
}
......@@ -264,4 +264,116 @@ void main() {
expect(find.text('non-regular page one'), findsOneWidget);
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),
)
],
);
}
}
......@@ -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() {
setUp(() {
WidgetsFlutterBinding.ensureInitialized();
......@@ -90,6 +100,38 @@ void main() {
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 {
final BinaryMessenger defaultBinaryMessenger = ServicesBinding.instance.defaultBinaryMessenger;
ByteData message;
......
......@@ -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() {
testWidgets('Push and Pop should send platform messages', (WidgetTester tester) async {
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);
}
// 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);
}
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment