-
Notifications
You must be signed in to change notification settings - Fork 29.1k
Make Navigator restorable (inkl. WidgetsApp, MaterialApp, CupertinoApp) #65658
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Make Navigator restorable (inkl. WidgetsApp, MaterialApp, CupertinoApp) #65658
Conversation
@@ -317,6 +317,7 @@ class RestorationManager extends ChangeNotifier { | |||
bucket.finalize(); | |||
} | |||
_bucketsNeedingSerialization.clear(); | |||
print(_rootBucket!._rawData); // TODO(goderbauer): revert this debug print before submission. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
fyi
@@ -948,6 +951,25 @@ class WidgetsApp extends StatefulWidget { | |||
/// {@endtemplate} | |||
final Map<Type, Action<Intent>> actions; | |||
|
|||
/// {@template flutter.widgets.widgetsApp.restorationScopeId} | |||
/// Providing a restoration ID enables state restoration for the app. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
there's a line above this, like, "The identifier to use when enabling state restoration for the navigator." or whatever, missing. (The first line of docs should be a noun phrase.)
@@ -1470,6 +1492,7 @@ class _WidgetsAppState extends State<WidgetsApp> with WidgetsBindingObserver { | |||
} else { | |||
assert(_navigator != null); | |||
routing = Navigator( | |||
restorationScopeId: 'nav', |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
why "nav"? Why not "n" or "navigator" or "\0x01" or "$Navigator" or something?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Yes. Any of those are valid as long as they are unique within the surrounding restoration scope. Here, the navigator is the only thing in this scope. Children (the individual routes) have their own scope.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
So why "nav" specifically?
@@ -49,6 +52,8 @@ typedef RouteBuilder<T> = Route<T> Function(BuildContext context, RouteSettings | |||
/// Used by [Navigator.onGenerateInitialRoutes]. | |||
typedef RouteListFactory = List<Route<dynamic>> Function(NavigatorState navigator, String initialRoute); | |||
|
|||
typedef RestorableRouteBuilder<T> = Route<T> Function(BuildContext context, Object arguments); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
docs
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Interestingly, the analyzer didn't warn about the missing docs here. Filed https://github.com/dart-lang/linter/issues/2238.
/// for this route. | ||
/// | ||
/// If the restoration scope ID changes (e.g. because restoration is enabled | ||
/// or disabled) throughout the life of the route, the [ValueListenable] |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
throughout -> during
throughout implies it happening continually, during refers to a particular case of it happening
/// If the restoration scope ID changes (e.g. because restoration is enabled | ||
/// or disabled) throughout the life of the route, the [ValueListenable] | ||
/// notifies its listeners. As an example, the ID changes to null while the | ||
/// route is transitioning off screen. At that point, the route is considered |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
"...transitioning off screen, which triggers a notification on this field" or some such. (Right now, you say "as an example" but don't actually then say why what you describe is an example of the previous sentence)
/// friends) can never restore their state. | ||
/// * A [Route] added with the restorable imperative API ([restorablePush], | ||
/// [restorablePushNamed], and all other imperative methods with "restorable" | ||
/// in the name) restores its state if all routes below it up to and |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
"in their names"
/// is no [Page]-based route below it, it only restores its state if all | ||
/// routes below it restore theirs. | ||
/// | ||
/// If a [Route] is deemed restorable, the [Navgiator] will set its |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
typo in "navigator"
/// | ||
/// * [RestorationManager], which explains how state restoration works in | ||
/// Flutter. | ||
final String restorationScopeId; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
should this mention the default id that WidgetsApp uses?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
WidgetsApp doesn't use a default id.
It internally specifies an ID for the Navigator it creates. However, that's an implementation detail that's not exposed to the public.
@@ -2386,7 +2467,8 @@ class _NotAnnounced extends Route<void> { | |||
class _RouteEntry extends RouteTransitionRecord { | |||
_RouteEntry( | |||
this.route, { | |||
@required _RouteLifecycle initialState, | |||
@required _RouteLifecycle initialState, | |||
this.restorationInformation, |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
indentation
/// for it or null if restoration cannot be enabled for this route. | ||
String get restorationId { | ||
// User-provided restoration ids of Pages are prefixed with 'p'. Generated | ||
// ids for pageless routes are prefixed with r to avoid clashes. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
put some punctuation between the prefix and the id (or use something other than letters), to avoid the risk of someone being offended if they use the id "rick" or something.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
lol, but good point. Added a "+".
@@ -2845,6 +2985,11 @@ class NavigatorState extends State<Navigator> with TickerProviderStateMixin { | |||
_effectiveObservers = widget.observers; | |||
} | |||
|
|||
// Use [_nextPagelessRestorationScopeId] to get the next id. | |||
final RestorableNum<int> _rawNextPagelessRestorationScopeId = RestorableNum<int>(0); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
declare this before first use
/// The method returns an opaque ID for the pushed route that can be used by | ||
/// the [RestorableRouteFuture] to gain access to the actual [Route] object | ||
/// added to the navigator and its return value. | ||
/// {@endtempalte} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
"endtempalte" is a typo
might be good to have some sample code here
@@ -3464,6 +3613,46 @@ class NavigatorState extends State<Navigator> with TickerProviderStateMixin { | |||
return push<T>(_routeNamed<T>(routeName, arguments: arguments)); | |||
} | |||
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
the restorable* methods should be mentioned in their corresponding non-restorable version
/// When [present] has been called to add a route, it may only be called again | ||
/// after the previously added route has completed. | ||
/// | ||
// TODO(goderbauer): Add a code sample that showcases how to use this. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
we should do that in this PR
Overall LGTM. Some minor comments. |
9a16c33
to
16c0487
Compare
4be8695
to
47d24b6
Compare
/// | ||
/// If non-null, a [RootRestorationScope] is inserted into the widget | ||
/// hierarchy, which descendant widgets can use to store and restore their | ||
/// state (for example by using the [RestorationMixin]). |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Does user have chance to use the RootRestorationScope? isn't this only used by the navigator widget or router widget?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Rephrased.
@@ -317,6 +319,9 @@ class CupertinoApp extends StatefulWidget { | |||
/// {@macro flutter.widgets.widgetsApp.actions.seeAlso} | |||
final Map<Type, Action<Intent>> actions; | |||
|
|||
/// {@macro flutter.widgets.widgetsApp.restorationScopeId} | |||
final String restorationScopeId; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
should this be nullable String?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
here and other places
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
At the time the PR was created, this file had not been migrated to NNBD, so this was correct. I am going to rebase this to the latest master, though, and then it will have to be nullable, yes.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
In other unmigrated files it cannot be marked as nullable just yet.
/// } | ||
/// ``` | ||
/// {@end-tool} | ||
class RestorableRouteFuture<T> extends RestorableProperty<String?> { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Couldn't the restorablePush just return the restorable future? if we asked user to provide their own restorationid to the route, I think we can probably restore this future with the logic similar to the _hookOntoRouteFuture?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
After state restoration, how would the entity that pushed the route get access to the restorableRouteFuture again that was returned when the route was initially pushed?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Is this because there is no way to restore the future chain unless all the call back are static functions? Restore the route future is possible I think? but then caller will have to reattach their future chain again.
ca06bd5
to
b299b10
Compare
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
All comments addressed. PTAL
@@ -317,6 +319,9 @@ class CupertinoApp extends StatefulWidget { | |||
/// {@macro flutter.widgets.widgetsApp.actions.seeAlso} | |||
final Map<Type, Action<Intent>> actions; | |||
|
|||
/// {@macro flutter.widgets.widgetsApp.restorationScopeId} | |||
final String restorationScopeId; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
At the time the PR was created, this file had not been migrated to NNBD, so this was correct. I am going to rebase this to the latest master, though, and then it will have to be nullable, yes.
/// | ||
/// If non-null, a [RootRestorationScope] is inserted into the widget | ||
/// hierarchy, which descendant widgets can use to store and restore their | ||
/// state (for example by using the [RestorationMixin]). |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Rephrased.
@@ -317,6 +319,9 @@ class CupertinoApp extends StatefulWidget { | |||
/// {@macro flutter.widgets.widgetsApp.actions.seeAlso} | |||
final Map<Type, Action<Intent>> actions; | |||
|
|||
/// {@macro flutter.widgets.widgetsApp.restorationScopeId} | |||
final String restorationScopeId; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
In other unmigrated files it cannot be marked as nullable just yet.
/// } | ||
/// ``` | ||
/// {@end-tool} | ||
class RestorableRouteFuture<T> extends RestorableProperty<String?> { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
After state restoration, how would the entity that pushed the route get access to the restorableRouteFuture again that was returned when the route was initially pushed?
/// | ||
/// ```dart | ||
/// void _switchToAudioVolume() { | ||
/// Navigator.restorablePushReplacementNamed(context, '/settings/volume'); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
If it's the same API as pushReplacementNamed, does it need to be different?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
for example, don't we need to be doing something with the return value?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
You only have to do something with the return value if you want to get access to the return value of the route or the route object itself. If you don't care about any of this, the usage is exactly the same. See also the flutter.widgets.navigator.restorablePushNamed.returnValue
docs.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I also added some clearer language around this to the doc.
/// | ||
/// {@macro flutter.widgets.navigator.restorablePushNamed.returnValue} | ||
@optionalTypeArgs | ||
static String restorablePush<T extends Object?>(BuildContext context, RestorableRouteBuilder<T> routeBuilder, {Object? arguments}) { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
is this supposed to return String?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
never mind i didn't see the @macro flutter.widgets.navigator.restorablePushNamed.returnValue
.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Instead of a String
I could make this one return an opaque RouteId object, if that sounds better...
/// ``` | ||
/// {@end-tool} | ||
/// | ||
/// {@macro flutter.widgets.navigator.restorablePushNamed.returnValue} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
this should probably be before the sample
/// title: const Text('Sample Code'), | ||
/// ), | ||
/// floatingActionButton: FloatingActionButton( | ||
/// onPressed: () => Navigator.restorablePush(context, _myRouteBuilder), |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
this seems to discard the return value
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
which is fine, see new and improved doc on return value.
/// | ||
/// ```dart | ||
/// void _openCalendar() { | ||
/// navigator.restorablePushNamedAndRemoveUntil('/calendar', ModalRoute.withName('/')); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
this also ignores the return value
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
which is fine, see new and improved doc on return value.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
LGTM modulo some docs questions
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
LGTM
|
||
@override | ||
// TODO(goderbauer): remove the kIsWeb check when https://github.com/flutter/flutter/issues/33615 is resolved. | ||
bool get isRestorable => !kIsWeb; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Does the restoration framework sends update if the platform is web?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The web embedder currently does not turn on restoration.
b299b10
to
9f0856a
Compare
This pull request is not suitable for automatic merging in its current state.
|
Description
WidgetsApp
,MaterialApp
, andCupertinoApp
now take a restoration ID to enable state restoration for the app (and the Navigator build by those widgets).The navigator now takes a restoration id to restore its state by recreating the current history stack of
Route
s during state restoration and by restoring the internal state of thoseRoute
s. However, not allRoute
s on the stack can be restored:Page
-based routes restore a restoration ID is provided to them.Route
s added with the existing imperative API (push
,pushNamed
, and friends) can never restore their state.Route
added with the newly added restorable imperative API (restorablePush
,restorablePushNamed
, and all other imperative methods with "restorable" in their name) restores its state if all routes below it up to and including the firstPage
-based route below it are restored. If there is noPage
-based route below it, it only restores its state if all routes below it restore theirs.If a
Route
is deemed restorable by theNavigator
, they will insert their ownRestorationScope
into the tree that the widgets in the route can use to restore their own state.Related Issues
#62916
Tests
I added the following tests:
Restoration tests for the Navigator and the newly added restorable API.
Checklist
Before you create this PR, confirm that it meets all requirements listed below by checking the relevant checkboxes (
[x]
). This will ensure a smooth and quick review process.///
).flutter analyze --flutter-repo
) does not report any problems on my PR.Breaking Change
Did any tests fail when you ran them? Please read Handling breaking changes.