Unverified Commit b1b3c1a3 authored by Michael Goderbauer's avatar Michael Goderbauer Committed by GitHub

State Restoration for Router (#82727)

parent 2d283504
......@@ -1099,10 +1099,10 @@ class WidgetsApp extends StatefulWidget {
/// Providing a restoration ID inserts a [RootRestorationScope] into the
/// widget hierarchy, which enables state restoration for descendant widgets.
///
/// Providing a restoration ID also enables the [Navigator] built by the
/// [WidgetsApp] to restore its state (i.e. to restore the history stack of
/// active [Route]s). See the documentation on [Navigator] for more details
/// around state restoration of [Route]s.
/// Providing a restoration ID also enables the [Navigator] or [Router] built
/// by the [WidgetsApp] to restore its state (i.e. to restore the history
/// stack of active [Route]s). See the documentation on [Navigator] for more
/// details around state restoration of [Route]s.
///
/// See also:
///
......@@ -1518,6 +1518,7 @@ class _WidgetsAppState extends State<WidgetsApp> with WidgetsBindingObserver {
if (_usesRouter) {
assert(_effectiveRouteInformationProvider != null);
routing = Router<Object>(
restorationScopeId: 'router',
routeInformationProvider: _effectiveRouteInformationProvider,
routeInformationParser: widget.routeInformationParser,
routerDelegate: widget.routerDelegate!,
......
......@@ -13,6 +13,8 @@ import 'basic.dart';
import 'binding.dart';
import 'framework.dart';
import 'navigator.dart';
import 'restoration.dart';
import 'restoration_properties.dart';
/// A piece of routing information.
///
......@@ -26,8 +28,16 @@ import 'navigator.dart';
/// 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 changes back to web engine.
/// The latter case happens in web application where the [Router] reports route
/// changes back to the web engine.
///
/// The current [RouteInformation] of an application is also used for state
/// restoration purposes. Before an application is killed, the [Router] converts
/// its current configurations into a [RouteInformation] object utilizing the
/// [RouteInformationProvider]. The [RouteInformation] object is then serialized
/// out and persisted. During state restoration, the object is deserialized and
/// passed back to the [RouteInformationProvider], which turns it into a
/// configuration for the [Router] again to restore its state from.
class RouteInformation {
/// Creates a route information object.
///
......@@ -48,13 +58,17 @@ class RouteInformation {
/// the text inside a [TextField] or the scroll position in a [ScrollView].
/// These widget states can be stored in the [state].
///
/// Currently, this information is only used by Flutter on the web:
/// the data is stored in the browser history entry when the
/// On the web, this information is stored in the browser history when the
/// [Router] reports this route information back to the web engine
/// through the [PlatformRouteInformationProvider]. The information
/// is then passed back, along with the [location], when the user
/// clicks the back or forward buttons.
///
/// This information is also serialized and persisted alongside the
/// [location] for state restoration purposes. During state restoration,
/// the information is made available again to the [Router] so it can restore
/// its configuration to the previous state.
///
/// The state must be serializable.
final Object? state;
}
......@@ -226,6 +240,25 @@ class RouteInformation {
/// [RouterDelegate.currentConfiguration] and
/// [RouteInformationParser.restoreRouteInformation] APIs to provide an optimal
/// user experience when running on the web platform.
///
/// ## State Restoration
///
/// The [Router] will restore the current configuration of the [routerDelegate]
/// during state restoration if it is configured with a [restorationScopeId] and
/// state restoration is enabled for the subtree. For that, the value of
/// [RouterDelegate.currentConfiguration] is serialized and persisted before the
/// app is killed by the operating system. After the app is restarted, the value
/// is deserialized and passed back to the [RouterDelegate] via a call to
/// [RouterDelegate.setRestoredRoutePath] (which by default just calls
/// [RouterDelegate.setNewRoutePath]). It is the responsibility of the
/// [RouterDelegate] to use the configuration information provided to restore
/// its internal state.
///
/// To serialize [RouterDelegate.currentConfiguration] and to deserialize it
/// again, the [Router] calls [RouteInformationParser.restoreRouteInformation]
/// and [RouteInformationParser.parseRouteInformation], respectively. Therefore,
/// if a [restorationScopeId] is provided, a [routeInformationParser] must be
/// configured as well.
class Router<T> extends StatefulWidget {
/// Creates a router.
///
......@@ -233,8 +266,8 @@ class Router<T> extends StatefulWidget {
/// router does not depend on route information. A common example is a sub router
/// that builds its content completely based on the app state.
///
/// If the [routeInformationProvider] is not null, the [routeInformationParser] must
/// also not be null.
/// If the [routeInformationProvider] or [restorationScopeId] is not null, then
/// [routeInformationParser] must also not be null.
///
/// The [routerDelegate] must not be null.
const Router({
......@@ -243,11 +276,10 @@ class Router<T> extends StatefulWidget {
this.routeInformationParser,
required this.routerDelegate,
this.backButtonDispatcher,
this.restorationScopeId,
}) : assert(
(routeInformationProvider == null) == (routeInformationParser == null),
'Both routeInformationProvider and routeInformationParser must be provided '
'if this router parses route information. Otherwise, they should both '
'be null.',
(routeInformationProvider == null && restorationScopeId == null) || routeInformationParser != null,
'A routeInformationParser must be provided when a routeInformationProvider or a restorationId is specified.'
),
assert(routerDelegate != null),
super(key: key);
......@@ -293,6 +325,27 @@ class Router<T> extends StatefulWidget {
/// router, or the [ChildBackButtonDispatcher] for other routers.
final BackButtonDispatcher? backButtonDispatcher;
/// Restoration ID to save and restore the state of the [Router].
///
/// If non-null, the [Router] will persist the [RouterDelegate]'s current
/// configuration (i.e. [RouterDelegate.currentConfiguration]). During state
/// restoration, the [Router] informs the [RouterDelegate] of the previous
/// configuration by calling [RouterDelegate.setRestoredRoutePath] (which by
/// default just calls [RouterDelegate.setNewRoutePath]). It is the
/// responsibility of the [RouterDelegate] to restore its internal state based
/// on the provided configuration.
///
/// The router uses the [RouteInformationParser] to serialize and deserialize
/// [RouterDelegate.currentConfiguration]. Therefore, a
/// [routeInformationParser] must be provided when [restorationScopeId] is
/// non-null.
///
/// See also:
///
/// * [RestorationManager], which explains how state restoration works in
/// Flutter.
final String? restorationScopeId;
/// Retrieves the immediate [Router] ancestor from the given context.
///
/// This method provides access to the delegates in the [Router]. For example,
......@@ -401,6 +454,7 @@ class Router<T> extends StatefulWidget {
}
typedef _AsyncPassthrough<Q> = Future<Q> Function(Q);
typedef _DelegateRouteSetter<T> = Future<void> Function(T);
// Whether to report the route information in this build cycle.
enum _IntentionToReportRouteInformation {
......@@ -414,10 +468,14 @@ enum _IntentionToReportRouteInformation {
ignore,
}
class _RouterState<T> extends State<Router<T>> {
class _RouterState<T> extends State<Router<T>> with RestorationMixin {
Object? _currentRouteInformationParserTransaction;
Object? _currentRouterDelegateTransaction;
late _IntentionToReportRouteInformation _currentIntentionToReport;
_IntentionToReportRouteInformation _currentIntentionToReport = _IntentionToReportRouteInformation.none;
final _RestorableRouteInformation _routeInformation = _RestorableRouteInformation();
@override
String? get restorationId => widget.restorationScopeId;
@override
void initState() {
......@@ -425,9 +483,15 @@ class _RouterState<T> extends State<Router<T>> {
widget.routeInformationProvider?.addListener(_handleRouteInformationProviderNotification);
widget.backButtonDispatcher?.addCallback(_handleBackButtonDispatcherNotification);
widget.routerDelegate.addListener(_handleRouterDelegateNotification);
_currentIntentionToReport = _IntentionToReportRouteInformation.none;
if (widget.routeInformationProvider != null) {
_processInitialRoute();
}
@override
void restoreState(RestorationBucket? oldBucket, bool initialRestore) {
registerForRestoration(_routeInformation, 'route');
if (_routeInformation.value != null) {
_processRouteInformation(_routeInformation.value!, () => widget.routerDelegate.setRestoredRoutePath);
} else if (widget.routeInformationProvider != null) {
_processRouteInformation(widget.routeInformationProvider!.value!, () => widget.routerDelegate.setInitialRoutePath);
}
}
......@@ -436,7 +500,7 @@ class _RouterState<T> extends State<Router<T>> {
String? _lastSeenLocation;
void _scheduleRouteInformationReportingTask() {
if (_routeInformationReportingTaskScheduled)
if (_routeInformationReportingTaskScheduled || widget.routeInformationProvider == null)
return;
assert(_currentIntentionToReport != _IntentionToReportRouteInformation.none);
_routeInformationReportingTaskScheduled = true;
......@@ -447,62 +511,33 @@ class _RouterState<T> extends State<Router<T>> {
assert(_routeInformationReportingTaskScheduled);
_routeInformationReportingTaskScheduled = false;
if (_routeInformation.value != null) {
final RouteInformation routeInformation = _routeInformation.value!;
switch (_currentIntentionToReport) {
case _IntentionToReportRouteInformation.none:
assert(false);
assert(false, '_reportRouteInformation must not be called with _IntentionToReportRouteInformation.none');
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;
break;
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;
break;
case _IntentionToReportRouteInformation.must:
final RouteInformation? routeInformation = _retrieveNewRouteInformation();
if (routeInformation != null) {
widget.routeInformationProvider!.routerReportsNewRouteInformation(routeInformation);
break;
}
_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;
return widget.routeInformationParser?.restoreRouteInformation(configuration);
}
void _setStateWithExplicitReportStatus(
......@@ -532,6 +567,7 @@ class _RouterState<T> extends State<Router<T>> {
}
void _maybeNeedToReportRouteInformation() {
_routeInformation.value = _retrieveNewRouteInformation();
_currentIntentionToReport = _currentIntentionToReport != _IntentionToReportRouteInformation.none
? _currentIntentionToReport
: _IntentionToReportRouteInformation.maybe;
......@@ -582,28 +618,21 @@ class _RouterState<T> extends State<Router<T>> {
super.dispose();
}
void _processInitialRoute() {
void _processRouteInformation(RouteInformation information, ValueGetter<_DelegateRouteSetter<T>> delegateRouteSetter) {
_currentRouteInformationParserTransaction = Object();
_currentRouterDelegateTransaction = Object();
_lastSeenLocation = widget.routeInformationProvider!.value!.location;
_lastSeenLocation = information.location;
widget.routeInformationParser!
.parseRouteInformation(widget.routeInformationProvider!.value!)
.parseRouteInformation(information)
.then<T>(_verifyRouteInformationParserStillCurrent(_currentRouteInformationParserTransaction, widget))
.then<void>(widget.routerDelegate.setInitialRoutePath)
.then<void>(delegateRouteSetter())
.then<void>(_verifyRouterDelegatePushStillCurrent(_currentRouterDelegateTransaction, widget))
.then<void>(_rebuild);
}
void _handleRouteInformationProviderNotification() {
_currentRouteInformationParserTransaction = Object();
_currentRouterDelegateTransaction = Object();
_lastSeenLocation = widget.routeInformationProvider!.value!.location;
widget.routeInformationParser!
.parseRouteInformation(widget.routeInformationProvider!.value!)
.then<T>(_verifyRouteInformationParserStillCurrent(_currentRouteInformationParserTransaction, widget))
.then<void>(widget.routerDelegate.setNewRoutePath)
.then<void>(_verifyRouterDelegatePushStillCurrent(_currentRouterDelegateTransaction, widget))
.then<void>(_rebuild);
assert(widget.routeInformationProvider!.value != null);
_processRouteInformation(widget.routeInformationProvider!.value!, () => widget.routerDelegate.setNewRoutePath);
}
Future<bool> _handleBackButtonDispatcherNotification() {
......@@ -614,7 +643,6 @@ class _RouterState<T> extends State<Router<T>> {
.then<bool>(_verifyRouterDelegatePopStillCurrent(_currentRouterDelegateTransaction, widget))
.then<bool>((bool data) {
_rebuild();
_maybeNeedToReportRouteInformation();
return SynchronousFuture<bool>(data);
});
}
......@@ -663,6 +691,7 @@ class _RouterState<T> extends State<Router<T>> {
Future<void> _rebuild([void value]) {
setState(() {/* routerDelegate is ready to rebuild */});
_maybeNeedToReportRouteInformation();
return SynchronousFuture<void>(value);
}
......@@ -673,7 +702,9 @@ class _RouterState<T> extends State<Router<T>> {
@override
Widget build(BuildContext context) {
return _RouterScope(
return UnmanagedRestorationScope(
bucket: bucket,
child: _RouterScope(
routeInformationProvider: widget.routeInformationProvider,
backButtonDispatcher: widget.backButtonDispatcher,
routeInformationParser: widget.routeInformationParser,
......@@ -684,6 +715,7 @@ class _RouterState<T> extends State<Router<T>> {
// will have a BuildContext that contains the _RouterScope.
builder: widget.routerDelegate.build,
),
),
);
}
}
......@@ -1094,8 +1126,9 @@ abstract class RouteInformationParser<T> {
/// Restore the route information from the given configuration.
///
/// This may return null, in which case the browser history will not be updated.
/// See [Router]'s documentation for details.
/// This may return null, in which case the browser history will not be
/// updated and state restoration is disabled. See [Router]'s documentation
/// for details.
///
/// The [parseRouteInformation] method must produce an equivalent
/// configuration when passed this method's return value.
......@@ -1125,6 +1158,17 @@ abstract class RouteInformationParser<T> {
///
/// All subclass must implement [setNewRoutePath], [popRoute], and [build].
///
/// ## State Restoration
///
/// If the [Router] owning this delegate is configured for state restoration, it
/// will persist and restore the configuration of this [RouterDelegate] using
/// the following mechanism: Before the app is killed by the operating system,
/// the value of [currentConfiguration] is serialized out and persisted. After
/// the app has restarted, the value is deserialized and passed back to the
/// [RouterDelegate] via a call to [setRestoredRoutePath] (which by default just
/// calls [setNewRoutePath]). It is the responsibility of the [RouterDelegate]
/// to use the configuration information provided to restore its internal state.
///
/// See also:
///
/// * [RouteInformationParser], which is responsible for parsing the route
......@@ -1143,10 +1187,29 @@ abstract class RouterDelegate<T> extends Listenable {
/// 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.
///
/// See also:
///
/// * [setRestoredRoutePath], which is called instead of this method during
/// state restoration.
Future<void> setInitialRoutePath(T configuration) {
return setNewRoutePath(configuration);
}
/// Called by the [Router] during state restoration.
///
/// When the [Router] is configured for state restoration, it will persist
/// the value of [currentConfiguration] during state serialization. During
/// state restoration, the [Router] calls this method (instead of
/// [setInitialRoutePath]) to pass the previous configuration back to the
/// delegate. It is the responsibility of the delegate to restore its internal
/// state based on the provided configuration.
///
/// By default, this method forwards the `configuration` to [setNewRoutePath].
Future<void> setRestoredRoutePath(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.
///
......@@ -1188,23 +1251,30 @@ abstract class RouterDelegate<T> extends Listenable {
/// 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.
///
/// ## State Restoration
///
/// This getter is also used by the [Router] to implement state restoration.
/// During state serialization, the [Router] will persist the current
/// configuration and during state restoration pass it back to the delegate
/// by calling [setRestoredRoutePath].
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.
/// This is called whenever the [Future]s returned by [setInitialRoutePath],
/// [setNewRoutePath], or [setRestoredRoutePath] complete as well as when this
/// 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].
/// [PopNavigatorRouterDelegateMixin]. If state restoration is enabled for the
/// [Router] using this delegate, consider providing a non-null
/// [Navigator.restorationScopeId] to the [Navigator] returned by this method.
///
/// This method must not return null.
///
......@@ -1340,3 +1410,28 @@ mixin PopNavigatorRouterDelegateMixin<T> on RouterDelegate<T> {
return navigator.maybePop();
}
}
class _RestorableRouteInformation extends RestorableValue<RouteInformation?> {
@override
RouteInformation? createDefaultValue() => null;
@override
void didUpdateValue(RouteInformation? oldValue) {
notifyListeners();
}
@override
RouteInformation? fromPrimitives(Object? data) {
if (data == null) {
return null;
}
assert(data is List<Object?> && data.length == 2);
final List<Object?> castedData = data as List<Object?>;
return RouteInformation(location: castedData.first as String?, state: castedData.last);
}
@override
Object? toPrimitives() {
return value == null ? null : <Object?>[value!.location, value!.state];
}
}
// 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.
import 'package:flutter/foundation.dart';
import 'package:flutter/material.dart';
import 'package:flutter_test/flutter_test.dart';
void main() {
testWidgets('Router state restoration without RouteInfomrationProvider', (WidgetTester tester) async {
final UniqueKey router = UniqueKey();
_TestRouterDelegate delegate() => tester.widget<Router<Object?>>(find.byKey(router)).routerDelegate as _TestRouterDelegate;
await tester.pumpWidget(_TestWidget(routerKey: router));
expect(find.text('Current config: null'), findsOneWidget);
expect(delegate().newRoutePaths, isEmpty);
expect(delegate().restoredRoutePaths, isEmpty);
delegate().currentConfiguration = '/foo';
await tester.pumpAndSettle();
expect(find.text('Current config: /foo'), findsOneWidget);
expect(delegate().newRoutePaths, isEmpty);
expect(delegate().restoredRoutePaths, isEmpty);
await tester.restartAndRestore();
expect(find.text('Current config: /foo'), findsOneWidget);
expect(delegate().newRoutePaths, isEmpty);
expect(delegate().restoredRoutePaths, <String>['/foo']);
final TestRestorationData restorationData = await tester.getRestorationData();
delegate().currentConfiguration = '/bar';
await tester.pumpAndSettle();
expect(find.text('Current config: /bar'), findsOneWidget);
expect(delegate().newRoutePaths, isEmpty);
expect(delegate().restoredRoutePaths, <String>['/foo']);
await tester.restoreFrom(restorationData);
expect(find.text('Current config: /foo'), findsOneWidget);
expect(delegate().newRoutePaths, isEmpty);
expect(delegate().restoredRoutePaths, <String>['/foo', '/foo']);
});
testWidgets('Router state restoration with RouteInfomrationProvider', (WidgetTester tester) async {
final UniqueKey router = UniqueKey();
_TestRouterDelegate delegate() => tester.widget<Router<Object?>>(find.byKey(router)).routerDelegate as _TestRouterDelegate;
_TestRouteInformationProvider provider() => tester.widget<Router<Object?>>(find.byKey(router)).routeInformationProvider! as _TestRouteInformationProvider;
await tester.pumpWidget(_TestWidget(routerKey: router, withInformationProvider: true));
expect(find.text('Current config: /home'), findsOneWidget);
expect(delegate().newRoutePaths, <String>['/home']);
expect(delegate().restoredRoutePaths, isEmpty);
provider().value = const RouteInformation(location: '/foo');
await tester.pumpAndSettle();
expect(find.text('Current config: /foo'), findsOneWidget);
expect(delegate().newRoutePaths, <String>['/home', '/foo']);
expect(delegate().restoredRoutePaths, isEmpty);
await tester.restartAndRestore();
expect(find.text('Current config: /foo'), findsOneWidget);
expect(delegate().newRoutePaths, isEmpty);
expect(delegate().restoredRoutePaths, <String>['/foo']);
final TestRestorationData restorationData = await tester.getRestorationData();
provider().value = const RouteInformation(location: '/bar');
await tester.pumpAndSettle();
expect(find.text('Current config: /bar'), findsOneWidget);
expect(delegate().newRoutePaths, <String>['/bar']);
expect(delegate().restoredRoutePaths, <String>['/foo']);
await tester.restoreFrom(restorationData);
expect(find.text('Current config: /foo'), findsOneWidget);
expect(delegate().newRoutePaths, <String>['/bar']);
expect(delegate().restoredRoutePaths, <String>['/foo', '/foo']);
});
}
class _TestRouteInformationParser extends RouteInformationParser<String> {
@override
Future<String> parseRouteInformation(RouteInformation routeInformation) {
return SynchronousFuture<String>(routeInformation.location!);
}
@override
RouteInformation? restoreRouteInformation(String configuration) {
return RouteInformation(location: configuration);
}
}
class _TestRouterDelegate extends RouterDelegate<String> with ChangeNotifier {
final List<String> newRoutePaths = <String>[];
final List<String> restoredRoutePaths = <String>[];
@override
String? get currentConfiguration => _currentConfiguration;
String? _currentConfiguration;
set currentConfiguration(String? value) {
if (value == _currentConfiguration) {
return;
}
_currentConfiguration = value;
notifyListeners();
}
@override
Future<void> setNewRoutePath(String configuration) {
_currentConfiguration = configuration;
newRoutePaths.add(configuration);
return SynchronousFuture<void>(null);
}
@override
Future<void> setRestoredRoutePath(String configuration) {
_currentConfiguration = configuration;
restoredRoutePaths.add(configuration);
return SynchronousFuture<void>(null);
}
@override
Widget build(BuildContext context) {
return Text('Current config: $currentConfiguration', textDirection: TextDirection.ltr);
}
@override
Future<bool> popRoute() async => throw UnimplementedError();
}
class _TestRouteInformationProvider extends RouteInformationProvider with ChangeNotifier {
@override
RouteInformation? get value => _value;
RouteInformation? _value = const RouteInformation(location: '/home');
set value(RouteInformation? value) {
if (value == _value) {
return;
}
_value = value;
notifyListeners();
}
}
class _TestWidget extends StatefulWidget {
const _TestWidget({Key? key, this.withInformationProvider = false, this.routerKey}) : super(key: key);
final bool withInformationProvider;
final Key? routerKey;
@override
State<_TestWidget> createState() => _TestWidgetState();
}
class _TestWidgetState extends State<_TestWidget> {
@override
Widget build(BuildContext context) {
return RootRestorationScope(
restorationId: 'root',
child: Router<String>(
key: widget.routerKey,
restorationScopeId: 'router',
routerDelegate: _TestRouterDelegate(),
routeInformationParser: _TestRouteInformationParser(),
routeInformationProvider: widget.withInformationProvider ? _TestRouteInformationProvider() : null,
),
);
}
}
......@@ -85,17 +85,13 @@ void main() {
final BuildContext textContext = key.currentContext!;
// This should not throw error.
Router<dynamic>? router = Router.maybeOf(textContext);
final Router<dynamic>? router = Router.maybeOf(textContext);
expect(router, isNull);
bool hasFlutterError = false;
try {
router = Router.of(textContext);
} on FlutterError catch(e) {
expect(e.message.startsWith('Router'), isTrue);
hasFlutterError = true;
}
expect(hasFlutterError, isTrue);
expect(
() => Router.of(textContext),
throwsA(isFlutterError.having((FlutterError e) => e.message, 'message', startsWith('Router')))
);
});
testWidgets('Simple router can handle pop route', (WidgetTester tester) async {
......@@ -137,12 +133,13 @@ void main() {
expect(find.text('popped'), findsOneWidget);
});
testWidgets('Router throw when passes only routeInformationProvider', (WidgetTester tester) async {
testWidgets('Router throw when passing routeInformationProvider without routeInformationParser', (WidgetTester tester) async {
final SimpleRouteInformationProvider provider = SimpleRouteInformationProvider();
provider.value = const RouteInformation(
location: 'initial',
);
try {
expect(
() {
Router<RouteInformation>(
routeInformationProvider: provider,
routerDelegate: SimpleRouterDelegate(
......@@ -151,32 +148,33 @@ void main() {
},
),
);
} on AssertionError catch(e) {
expect(
e.message,
'Both routeInformationProvider and routeInformationParser must be provided if this router '
'parses route information. Otherwise, they should both be null.',
},
throwsA(isAssertionError.having(
(AssertionError e) => e.message,
'message',
'A routeInformationParser must be provided when a routeInformationProvider or a restorationId is specified.',
)),
);
}
});
testWidgets('Router throw when passes only routeInformationParser', (WidgetTester tester) async {
try {
testWidgets('Router throw when passing restorationId without routeInformationParser', (WidgetTester tester) async {
expect(
() {
Router<RouteInformation>(
routeInformationParser: SimpleRouteInformationParser(),
restorationScopeId: 'foo',
routerDelegate: SimpleRouterDelegate(
builder: (BuildContext context, RouteInformation? information) {
return Text(information!.location!);
},
),
);
} on AssertionError catch(e) {
expect(
e.message,
'Both routeInformationProvider and routeInformationParser must be provided if this router '
'parses route information. Otherwise, they should both be null.',
},
throwsA(isAssertionError.having(
(AssertionError e) => e.message,
'message',
'A routeInformationParser must be provided when a routeInformationProvider or a restorationId is specified.',
)),
);
}
});
testWidgets('PopNavigatorRouterDelegateMixin works', (WidgetTester tester) async {
......@@ -1091,6 +1089,35 @@ testWidgets('ChildBackButtonDispatcher take priority recursively', (WidgetTester
await tester.pump();
expect(find.text('second callback'), findsOneWidget);
});
testWidgets('Router reports location if it is different from location given by OS', (WidgetTester tester) async {
final List<RouteInformation> reportedRouteInformation = <RouteInformation>[];
final SimpleRouteInformationProvider provider = SimpleRouteInformationProvider(
onRouterReport: reportedRouteInformation.add,
)..value = const RouteInformation(location: '/home');
await tester.pumpWidget(buildBoilerPlate(
Router<RouteInformation>(
routeInformationProvider: provider,
routeInformationParser: RedirectingInformationParser(<String, RouteInformation>{
'/doesNotExist' : const RouteInformation(location: '/404'),
}),
routerDelegate: SimpleRouterDelegate(
builder: (BuildContext _, RouteInformation? info) => Text('Current route: ${info?.location}'),
reportConfiguration: true,
),
),
));
expect(find.text('Current route: /home'), findsOneWidget);
expect(reportedRouteInformation, isEmpty);
provider.value = const RouteInformation(location: '/doesNotExist');
await tester.pump();
expect(find.text('Current route: /404'), findsOneWidget);
expect(reportedRouteInformation.single.location, '/404');
});
}
Widget buildBoilerPlate(Widget child) {
......@@ -1275,3 +1302,20 @@ class SimpleAsyncRouterDelegate extends RouterDelegate<RouteInformation> with Ch
@override
Widget build(BuildContext context) => builder(context, routeInformation);
}
class RedirectingInformationParser extends RouteInformationParser<RouteInformation> {
RedirectingInformationParser(this.redirects);
final Map<String, RouteInformation> redirects;
@override
Future<RouteInformation> parseRouteInformation(RouteInformation information) {
return SynchronousFuture<RouteInformation>(redirects[information.location] ?? information);
}
@override
RouteInformation restoreRouteInformation(RouteInformation configuration) {
return configuration;
}
}
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