Skip to content

Conversation

goderbauer
Copy link
Member

@goderbauer goderbauer commented Sep 11, 2020

Description

WidgetsApp, MaterialApp, and CupertinoApp 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 Routes during state restoration and by restoring the internal state of those Routes. However, not all Routes on the stack can be restored:

  • Page-based routes restore a restoration ID is provided to them.
  • Routes added with the existing imperative API (push, pushNamed, and friends) can never restore their state.
  • A 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 first Page-based route below it are restored. If there 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 by the Navigator, they will insert their own RestorationScope 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.

  • I read the Contributor Guide and followed the process outlined there for submitting PRs.
  • I signed the CLA.
  • I read and followed the Flutter Style Guide, including Features we expect every widget to implement.
  • I read the Tree Hygiene wiki page, which explains my responsibilities.
  • I updated/added relevant documentation (doc comments with ///).
  • All existing and new tests are passing.
  • The analyzer (flutter analyze --flutter-repo) does not report any problems on my PR.
  • I am willing to follow-up on review comments in a timely manner.

Breaking Change

Did any tests fail when you ran them? Please read Handling breaking changes.

@flutter-dashboard flutter-dashboard bot added f: cupertino flutter/packages/flutter/cupertino repository f: material design flutter/packages/flutter/material repository. framework flutter/packages/flutter repository. See also f: labels. work in progress; do not review labels Sep 11, 2020
@@ -317,6 +317,7 @@ class RestorationManager extends ChangeNotifier {
bucket.finalize();
}
_bucketsNeedingSerialization.clear();
print(_rootBucket!._rawData); // TODO(goderbauer): revert this debug print before submission.
Copy link
Contributor

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.
Copy link
Contributor

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',
Copy link
Contributor

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?

Copy link
Member Author

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.

Copy link
Contributor

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);
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

docs

Copy link
Member Author

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]
Copy link
Contributor

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
Copy link
Contributor

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
Copy link
Contributor

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
Copy link
Contributor

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;
Copy link
Contributor

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?

Copy link
Member Author

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,
Copy link
Contributor

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.
Copy link
Contributor

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.

Copy link
Member Author

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);
Copy link
Contributor

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}
Copy link
Contributor

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));
}

Copy link
Contributor

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.
Copy link
Contributor

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

@Hixie
Copy link
Contributor

Hixie commented Sep 11, 2020

Overall LGTM. Some minor comments.

@goderbauer goderbauer changed the title Make Navigator restorable Make Navigator restorable (inkl. WidgetsApp, MaterialApp, CupertinoApp) Sep 22, 2020
@goderbauer goderbauer force-pushed the restorableNavigator branch 2 times, most recently from 4be8695 to 47d24b6 Compare September 23, 2020 21:07
@goderbauer goderbauer marked this pull request as ready for review September 23, 2020 23:56
///
/// 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]).
Copy link
Contributor

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?

Copy link
Member Author

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;
Copy link
Contributor

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?

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

here and other places

Copy link
Member Author

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.

Copy link
Member Author

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?> {
Copy link
Contributor

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?

Copy link
Member Author

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?

Copy link
Contributor

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.

Copy link
Member Author

@goderbauer goderbauer left a 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;
Copy link
Member Author

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]).
Copy link
Member Author

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;
Copy link
Member Author

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?> {
Copy link
Member Author

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');
Copy link
Contributor

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?

Copy link
Contributor

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?

Copy link
Member Author

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.

Copy link
Member Author

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}) {
Copy link
Contributor

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?

Copy link
Contributor

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.

Copy link
Member Author

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}
Copy link
Contributor

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),
Copy link
Contributor

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

Copy link
Member Author

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('/'));
Copy link
Contributor

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

Copy link
Member Author

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.

Copy link
Contributor

@Hixie Hixie left a 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

Copy link
Contributor

@chunhtai chunhtai left a 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;
Copy link
Contributor

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?

Copy link
Member Author

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.

@goderbauer goderbauer force-pushed the restorableNavigator branch from b299b10 to 9f0856a Compare October 1, 2020 20:22
@fluttergithubbot
Copy link
Contributor

This pull request is not suitable for automatic merging in its current state.

  • The status or check suite Mac framework_tests has failed. Please fix the issues identified (or deflake) before re-applying this label.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
f: cupertino flutter/packages/flutter/cupertino repository f: material design flutter/packages/flutter/material repository. framework flutter/packages/flutter repository. See also f: labels.
Projects
None yet
Development

Successfully merging this pull request may close these issues.

5 participants