Skip to content
Projects
Groups
Snippets
Help
Loading...
Help
Submit feedback
Sign in
Toggle navigation
F
Front-End
Project
Project
Details
Activity
Releases
Cycle Analytics
Repository
Repository
Files
Commits
Branches
Tags
Contributors
Graph
Compare
Charts
Issues
0
Issues
0
List
Board
Labels
Milestones
Merge Requests
0
Merge Requests
0
CI / CD
CI / CD
Pipelines
Jobs
Schedules
Charts
Wiki
Wiki
Snippets
Snippets
Members
Members
Collapse sidebar
Close sidebar
Activity
Graph
Charts
Create a new issue
Jobs
Commits
Issue Boards
Open sidebar
abdullh.alsoleman
Front-End
Commits
b1b3c1a3
Unverified
Commit
b1b3c1a3
authored
May 19, 2021
by
Michael Goderbauer
Committed by
GitHub
May 19, 2021
Browse files
Options
Browse Files
Download
Email Patches
Plain Diff
State Restoration for Router (#82727)
parent
2d283504
Changes
4
Hide whitespace changes
Inline
Side-by-side
Showing
4 changed files
with
450 additions
and
143 deletions
+450
-143
app.dart
packages/flutter/lib/src/widgets/app.dart
+5
-4
router.dart
packages/flutter/lib/src/widgets/router.dart
+191
-96
router_restoration_test.dart
packages/flutter/test/widgets/router_restoration_test.dart
+167
-0
router_test.dart
packages/flutter/test/widgets/router_test.dart
+87
-43
No files found.
packages/flutter/lib/src/widgets/app.dart
View file @
b1b3c1a3
...
...
@@ -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
!,
...
...
packages/flutter/lib/src/widgets/router.dart
View file @
b1b3c1a3
...
...
@@ -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
;
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
(
_routeInformation
.
value
!=
null
)
{
final
RouteInformation
routeInformation
=
_routeInformation
.
value
!;
switch
(
_currentIntentionToReport
)
{
case
_IntentionToReportRouteInformation
.
none
:
assert
(
false
,
'_reportRouteInformation must not be called with _IntentionToReportRouteInformation.none'
);
return
;
case
_IntentionToReportRouteInformation
.
ignore
:
break
;
case
_IntentionToReportRouteInformation
.
maybe
:
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
)
{
break
;
case
_IntentionToReportRouteInformation
.
must
:
widget
.
routeInformationProvider
!.
routerReportsNewRouteInformation
(
routeInformation
);
_lastSeenLocation
=
routeInformation
.
location
;
}
_currentIntentionToReport
=
_IntentionToReportRouteInformation
.
none
;
return
;
break
;
}
_lastSeenLocation
=
routeInformation
.
location
;
}
_currentIntentionToReport
=
_IntentionToReportRouteInformation
.
none
;
}
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
_process
InitialRoute
(
)
{
void
_process
RouteInformation
(
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,16 +702,19 @@ class _RouterState<T> extends State<Router<T>> {
@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
,
return
UnmanagedRestorationScope
(
bucket:
bucket
,
child:
_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
,
),
),
);
}
...
...
@@ -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
];
}
}
packages/flutter/test/widgets/router_restoration_test.dart
0 → 100644
View file @
b1b3c1a3
// 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
,
),
);
}
}
packages/flutter/test/widgets/router_test.dart
View file @
b1b3c1a3
...
...
@@ -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 pass
es only routeInformationProvid
er'
,
(
WidgetTester
tester
)
async
{
testWidgets
(
'Router throw when pass
ing routeInformationProvider without routeInformationPars
er'
,
(
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
;
}
}
Write
Preview
Markdown
is supported
0%
Try again
or
attach a new file
Attach a file
Cancel
You are about to add
0
people
to the discussion. Proceed with caution.
Finish editing this message first!
Cancel
Please
register
or
sign in
to comment