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!,
......
// 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,46 +133,48 @@ 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 {
Router<RouteInformation>(
routeInformationProvider: provider,
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.',
);
}
expect(
() {
Router<RouteInformation>(
routeInformationProvider: provider,
routerDelegate: SimpleRouterDelegate(
builder: (BuildContext context, RouteInformation? information) {
return Text(information!.location!);
},
),
);
},
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 {
Router<RouteInformation>(
routeInformationParser: SimpleRouteInformationParser(),
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.',
);
}
testWidgets('Router throw when passing restorationId without routeInformationParser', (WidgetTester tester) async {
expect(
() {
Router<RouteInformation>(
restorationScopeId: 'foo',
routerDelegate: SimpleRouterDelegate(
builder: (BuildContext context, RouteInformation? information) {
return Text(information!.location!);
},
),
);
},
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