Skip to content
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

State Restoration support for Veggie Seasons app #433

Merged
merged 5 commits into from
Nov 2, 2020

Conversation

goderbauer
Copy link
Member

@goderbauer goderbauer commented May 7, 2020

This PR enables state restoration for the Veggie Seasons app using Flutter's state restoration framework (Design Doc: http://flutter.dev/go/state-restoration-design).

The changes in this PR require a Flutter version after commit flutter/flutter@053ebf2.

State Restoration for Flutter is currently only enabled for Android (adding support for iOS is tracked in flutter/flutter#62915). In order to test the state restoration aspect of this app follow these steps:

  1. Change into the veggieseasons directory.
  2. Run flutter create .. This generates the boilerplate required to run the veggie seasons app on Android.
  3. Enable "Don't keep activities" in the developer settings of your Android device.
  4. Run the app on the Android device via flutter run.
  5. Navigate around in the app, change some state.
  6. Switch to the home screen or another app via the app switcher. This kills the Veggie Seasons app.
  7. Switch back the Veggie Seasons app via the app switcher. This will restart the Veggie Seasons app.
  8. Observe that no state is lost.

Part of flutter/flutter#62916.

Background

When an app is backgrounded on e.g. Android, the OS may kill the app at any time to reclaim resources. When the user switches back to the app, the app is relaunched and expected to restore the same state it had when it was killed. For that, apps can serialize out state information before they are killed and that information is handed back to them when they are relaunched.

Summary

In this PR, widget State objects that want some of their properties restored when the app is relaunched mixin the RestorationMixin. They then use (subclasses of) RestorableProperty to store information that they need restored. When the app is killed, the current value of the RestorableProperty is serialized out and when the app relaunches, the property magically has its old value again. App developer need to serialize out enough information to rebuild the same widget tree that was active when the app was killed.

Those RestorablePropertys in a State object need to be bound to a restoration id by calling RestorationMixin.registerForRestoration in the restoreState method. The serialized data of a property is stored under that id and the id is used again to pull the correct data for a given property out of the app's serialized state again when the app is relaunched.

The restoreState method added to State objects by the RestorationMixin is called after initState. In this method, RestorableProperty should be registered to initialize them with the data from the serialized state. Other initialization code, that depends on the value of a RestorableProperty, should also run here. The restoreState method will be called again if the app is asked to restore from different serialized state while it is running (this is currently uncommon, but may be relevant for some use cases on the web).

The framework comes with built-in RestorableProperty subclasses to store common values like an int, double, String, TextEditingController, etc. Apps can create their own subclasses to serialize and deserialize more complex custom objects.

Widgets, that ship with the framework, will have the functionality to restore state built-in. In order to enable it, app developers need to provide a restoration id to those widgets. The widgets will store their data under this id in the surrounding restoration scope.

Restoration ids need to be unique in their surrounding restoration scope. A new restoration scope can be established by inserting a RestorationScope widget into the tree.

@goderbauer goderbauer changed the title [PREVIEW, DO NOT SUBMIT] Preview of integrating state restoration into Veggie Seasons app [DO NOT SUBMIT] Preview of integrating state restoration into Veggie Seasons app May 7, 2020
// CupertinoApp/MaterialApp. However, in this particular app there are widgets
// above the CupertinoApp that need their state restored, so we are injecting
// the root scope manually above everything.
runApp(RestorationScope.root(
Copy link
Member

Choose a reason for hiding this comment

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

this code isn't on master yet is it? Are there non-root constructors?

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 code currently lives in a personal branch of mine. I am in the process of cleaning that up and then will make it available.

There are other constructors. The default one just starts a new child-scope within its parent scope. The root one actually inserts the data received from the engine into the tree.

@override
void initState() {
super.initState();
register(appState, const RestorationId('state'));
Copy link
Member

Choose a reason for hiding this comment

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

could this name be more tightly scoped? register sounds really broad.

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 am open to suggestions :)

However, it was meant to have a somewhat broader scope because, after this call the RestorationMixin manages the registered proeprty.

Copy link
Contributor

Choose a reason for hiding this comment

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

+1 to @xster's note. Something like registerForRestoration would help readers know immediately what the call is doing.

Copy link
Member Author

Choose a reason for hiding this comment

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

Another idea I just had: restoreProperty because this is actually the call that retrieves the serialized value from the restoration state, deserializes it, and puts it into the property.

class _SearchScreenState extends State<SearchScreen> {
final controller = TextEditingController();
class _SearchScreenState extends State<SearchScreen> with RestorationMixin {
RestorableTextEditingController controller = RestorableTextEditingController(TextEditingController());
Copy link
Member

Choose a reason for hiding this comment

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

oh wow, neat

title: 'Calorie Target',
),
);
Navigator.of(context).restorablePush<void>(_calorieSettingsScreen);
Copy link
Member

Choose a reason for hiding this comment

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

could the .of happen to be implemented by a RestorableNavigator? Or it's intended for the user to choose whether each navigation change is restorable or not?

Copy link
Member Author

Choose a reason for hiding this comment

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

There's no RestorableNavigator class. Just like any other built-in widget, the Navigator will attempt to restore its state if it has been configured with a restoration id.

Copy link
Contributor

@RedBrogdon RedBrogdon left a comment

Choose a reason for hiding this comment

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

This is really neat. I have a bunch of questions. 😄

  • What is your plan for this PR? Are you actually wanting to land it, or is this mostly an experiment as you work through your ideas?
  • Does this code need to be in the SDK itself, or could it be distributed as a plugin?
  • Is there a design doc for the API?
  • Have you worked through how the usage would be for someone building an app with a state management library like redux or BLoC?

@override
void initState() {
super.initState();
register(appState, const RestorationId('state'));
Copy link
Contributor

Choose a reason for hiding this comment

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

+1 to @xster's note. Something like registerForRestoration would help readers know immediately what the call is doing.

controller.addListener(_onTextChanged);
terms = controller.value.text;
Copy link
Contributor

Choose a reason for hiding this comment

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

Would it be possible to just route the properties directly inside the RestorableTextEditingController class:

String get text => _value.text;

in order to make terms = controller.value.text back into terms = controller.text?

Copy link
Member Author

Choose a reason for hiding this comment

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

Interesting idea. I think that may be possible. Let me experiment with that...

Copy link
Contributor

Choose a reason for hiding this comment

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

I'm guessing this didn't end up being feasible/advisable? 😄

Copy link
Member Author

Choose a reason for hiding this comment

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

It is doable, but ultimately, I thought it was more consistent if a RestorableFoo always wraps a value of type Foo. That's how it has to be for the primitive values int, double, String, etc. So I also extended that to the object type values for consistency.

Copy link
Member Author

Choose a reason for hiding this comment

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

As mentioned in the comment about AppState, merging the two also makes it harder to throw good/proper error messages when you forget to register the class.

@RedBrogdon RedBrogdon changed the title [DO NOT SUBMIT] Preview of integrating state restoration into Veggie Seasons app [WIP] Preview of integrating state restoration into Veggie Seasons app May 7, 2020
@RedBrogdon
Copy link
Contributor

Took the liberty of DO NOT SUBMIT => WIP, since that's the check our repo runs.

@goderbauer
Copy link
Member Author

@RedBrogdon Here are answers to your questions:

What is your plan for this PR? Are you actually wanting to land it, or is this mostly an experiment as you work through your ideas?

Eventually, I would like to submit this to have one example app with state restoration fully enabled. However, before that can happen, I need to finish the framework side API for state restoration and submit that to the flutter repository. In that process, this PR will probably change a little bit. If having this PR open for a long time is a problem, I am happy to close this. Opening this PR seems like the easiest way to solicit early feedback on API usage.

Does this code need to be in the SDK itself, or could it be distributed as a plugin?

My plan is to add the state restoration functionality directly to the Flutter SDK. In theory, the functionality itself could be implemented in a plugin, but we also want to enable state restoration for the widgets that ship with flutter. To do that, the SDK would have to depend on this plugin, which is awkward and something we try to avoid. Therefore, I am just going to implement this directly in the framework.

Is there a design doc for the API?

There is a half-backed one. I intent to finish that one up and make it available to the public to collect more feedback soon. I'll let you know.

Have you worked through how the usage would be for someone building an app with a state management library like redux or BLoC?

I have not. If the state held by those libraries is serializable, it should just work™. Do we have an example redux/BLoC app somewhere to which I could add state restoration to prove that theory?

@domesticmouse
Copy link
Contributor

@goderbauer answers inline

@RedBrogdon Here are answers to your questions:

What is your plan for this PR? Are you actually wanting to land it, or is this mostly an experiment as you work through your ideas?

Eventually, I would like to submit this to have one example app with state restoration fully enabled. However, before that can happen, I need to finish the framework side API for state restoration and submit that to the flutter repository. In that process, this PR will probably change a little bit. If having this PR open for a long time is a problem, I am happy to close this. Opening this PR seems like the easiest way to solicit early feedback on API usage.

The only issue with a long open PR is the usual maintenance concerns with updates. If you don't mind us pressing the sync button on the PR as we land maintenance changes around this PR, then I have no concerns. This style of early preview on new APIs is gold from my POV.

Does this code need to be in the SDK itself, or could it be distributed as a plugin?

My plan is to add the state restoration functionality directly to the Flutter SDK. In theory, the functionality itself could be implemented in a plugin, but we also want to enable state restoration for the widgets that ship with flutter. To do that, the SDK would have to depend on this plugin, which is awkward and something we try to avoid. Therefore, I am just going to implement this directly in the framework.

SGTM

Is there a design doc for the API?

There is a half-backed one. I intent to finish that one up and make it available to the public to collect more feedback soon. I'll let you know.

Have you worked through how the usage would be for someone building an app with a state management library like redux or BLoC?

I have not. If the state held by those libraries is serializable, it should just work™. Do we have an example redux/BLoC app somewhere to which I could add state restoration to prove that theory?

Here's the world's simplest Provider app, which should hopefully be a good stepping stone to things like redux and BLoC: https://github.com/flutter/samples/tree/master/provider_counter

@goderbauer
Copy link
Member Author

I've made some updates to incorporate the feedback I've gotten so far and to allow restoring to a different serialized state while the app is running (instead of just initializing the app from serialized state when it is launched).


// A custom RestorableProperty that saves and restores the data in an [AppState]
// object.
class RestorableAppState extends RestorableListenable<AppState> {
Copy link
Contributor

Choose a reason for hiding this comment

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

is RestorableListenable a RestorableProperty? If so it would be nice if it had Property in the name for clarity.
Ditto RestorableAppState.

/// Current app state. This is used to fetch veggie data.
AppState appState;

/// The veggie trivia about which to show.
Veggie veggie;

/// Index of the current trivia question.
int triviaIndex = 0;
RestorableNum<int> triviaIndex = 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.

I recommend just having RestorableInt and RestorableDouble, it's cleaner

Copy link
Contributor

Choose a reason for hiding this comment

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

(they can just be defined subclasses of RestorableNum)

@override
void didUpdateValue(PlayerStatus oldValue) {
notifyListeners();
}
Copy link
Contributor

Choose a reason for hiding this comment

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

Can you make a RestorableEnum ?

Copy link
Member Author

Choose a reason for hiding this comment

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

Originally I looked into that, but since enums don't have a common base class other than Object I don't see a good way to do that.

@Hixie
Copy link
Contributor

Hixie commented Jun 9, 2020

This looks great.

What's the testability story?

@goderbauer
Copy link
Member Author

Is there a design doc for the API?

The design document for the restoration API is available here: http://flutter.dev/go/state-restoration-design

@goderbauer goderbauer force-pushed the restoration branch 3 times, most recently from f8872fb to de329d5 Compare October 12, 2020 22:35
@goderbauer goderbauer changed the title [WIP] Preview of integrating state restoration into Veggie Seasons app [WIP] State Restoration support for Veggie Seasons app Oct 12, 2020
@goderbauer goderbauer changed the title [WIP] State Restoration support for Veggie Seasons app State Restoration support for Veggie Seasons app Oct 12, 2020
@goderbauer
Copy link
Member Author

goderbauer commented Oct 12, 2020

I updated this PR to the latest implementation of the state restoration framework in Flutter. It requires a version of flutter that's newer than this commit: flutter/flutter@053ebf2.

@RedBrogdon Is this something we can land in this repository as an example of how to make an app restorable? If so, I will add some tests to ensure that this doesn't break in the future.

I also updated the PR description to explain how one can observe state restoration in this app in action.

@RedBrogdon
Copy link
Contributor

I'm in a quandary.

On the one hand, Having state restoration represented in the samples repo would be a clear win, and VeggieSeasons is a great way to do it. On the other, samples in the repo (other than the ones in /experimental) are intended to run on the stable channel, which wouldn't be possible if this change were landed.

We could:

  • Update the version in place, and add a big note to its README that it no longer works on stable.
  • Leave the new version in your fork and simply direct people there via social outreach.
  • Move VeggieSeasons into the experimental directory for the next few months, leaving a redirect README in the old location.
  • Create a separate copy of VeggieSeasons in the experimental directory for the time being, then delete it when the new APIs get to stable and the primary one could be updated.

WDYT? I would lean toward the second or third one.

@goderbauer
Copy link
Member Author

I understand your quandary. :)

Given these options, I would prefer number 3:

Move VeggieSeasons into the experimental directory for the next few months, leaving a redirect README in the old location.

That way, there continues to be just one canonical copy of the app and we don't have to deal with multiple forks/copies that may become out of date and cause merge conflicts once state restoration fully lands on flutter stable.

If you're cool with that, I would adapt the PR to implement this option.

@goderbauer goderbauer force-pushed the restoration branch 2 times, most recently from 59e4ba4 to 3cec26c Compare October 29, 2020 19:24
@goderbauer
Copy link
Member Author

goderbauer commented Oct 29, 2020

@RedBrogdon This one is ready for review now. For a more sane review experience, I'd recommend reviewing the commits separately. The first one makes the app restorable, the second one moves it to experimental.

Copy link
Contributor

@RedBrogdon RedBrogdon left a comment

Choose a reason for hiding this comment

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

I have a few questions, but that's it. :)

veggieseasons/lib/main.dart Outdated Show resolved Hide resolved
controller.addListener(_onTextChanged);
terms = controller.value.text;
Copy link
Contributor

Choose a reason for hiding this comment

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

I'm guessing this didn't end up being feasible/advisable? 😄

veggieseasons/lib/screens/details.dart Show resolved Hide resolved
veggieseasons/README.md Show resolved Hide resolved
@RedBrogdon RedBrogdon merged commit ed15031 into flutter:master Nov 2, 2020
@goderbauer goderbauer deleted the restoration branch January 15, 2021 18:32
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

None yet

5 participants