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

Allow RestorationAPI to restore any StatefulWidget at runtime #80303

Open
esDotDev opened this issue Apr 12, 2021 · 13 comments
Open

Allow RestorationAPI to restore any StatefulWidget at runtime #80303

esDotDev opened this issue Apr 12, 2021 · 13 comments
Labels
a: state restoration RestorationManager and related APIs c: proposal A detailed proposal for a change to Flutter framework flutter/packages/flutter repository. See also f: labels. P3 Issues that are less important to the Flutter project team-framework Owned by Framework team triaged-framework Triaged by Framework team

Comments

@esDotDev
Copy link

esDotDev commented Apr 12, 2021

When switching navigation routes it is often desired to retain various view state such as ScrollPosition, TextFields and Animation state.

Currently this requires using something like an IndexedStack, where all Widgets are kept in the tree and in memory. This is really not conducive to changing routes with Navigator or MaterialApp.router.

It would be nice if somehow we could instead use the StateRestoration API to handle this. Ideally this would be syncronous, and restore data from RAM rather than disk, at least for the most recently cached states.

Desired Result:
When a state is disposed, it saves it's restorable fields. When a state with the same id is loaded again, it fetches any restored state it can. Ideally there is some way we can skip initState() here as well. Or incorporate it into it:

void initState(){
  if(canRestoreState == true) restoreState();
  else {
     // First init
  }
}
@esDotDev
Copy link
Author

esDotDev commented Apr 12, 2021

In terms of use cases, this is widely needed when it comes to app-routing right now, many of the app routing strategies lose state when changing routes with Navigator, and it's cumbersome to implement these basic framework restorations ourselves (restoring our own state is fairly easy, cause we can just hoist that up. But restoring every textField, scroller or AnimationController is a major pain point)

@HansMuller HansMuller added framework flutter/packages/flutter repository. See also f: labels. c: proposal A detailed proposal for a change to Flutter a: state restoration RestorationManager and related APIs labels Apr 12, 2021
@esDotDev
Copy link
Author

esDotDev commented Nov 2, 2021

Any thoughts on this @goderbauer? It seems we're extremely close to being able to do this right now... I just can't figure out how to trigger the Restoration on the state.

This would really be a game changer when it comes to creating better UX with Flutter and Nav 2.0. Users want their view states to be saved between routes and currently it's very cumbersome. PageStorage works ok, but has a number of limitations/issues. All the plumbing seems to be in place to do this, but I ca't figure out how to make it happen.

Maybe it's possible already? As a basic example I'd like this view to restore the count each time it loads (the count should persist), but it always resets:

JqBbr4Aets.mp4
class App extends StatelessWidget {
  const App({Key? key}) : super(key: key);

  @override
  Widget build(BuildContext context) => MaterialApp(restorationScopeId: 'app', home: CounterPage());
}

class CounterPage extends StatefulWidget {
  @override
  State<CounterPage> createState() => _CounterPageState();
}

class _CounterPageState extends State<CounterPage> with RestorationMixin {
  RestorableInt _count = RestorableInt(0);

  @override
  String? get restorationId => 'page1';

  @override
  void initState() {
    super.initState();
    // TODO: We need some call here, to tell RestorationManager to give us the latest data.
  }

  @override
  void restoreState(RestorationBucket? oldBucket, bool initialRestore) {
    // would like this to fetch the last saved value for this id
    registerForRestoration(_count, 'count');
  }

  void _incrementCounter() => setState(() => _count.value++);
 
  // Keep pushing the same page to the navigator, it should be able to persist it's state values
  void _openNewPage() => Navigator.of(context).push<void>(MaterialPageRoute(builder: (_) => CounterPage()));

  @override
  Widget build(BuildContext context) => Column(children: [
        OutlinedButton(onPressed: _incrementCounter, child: Text('count=${_count.value}')),
        OutlinedButton(onPressed: _openNewPage, child: Text('Load Page')),
      ]);
}

@HerrNiklasRaab
Copy link

@goderbauer May we have your thought in case this is easy :)

@goderbauer goderbauer added the P3 Issues that are less important to the Flutter project label May 4, 2022
@chunhtai
Copy link
Contributor

This may be a memory hazard. Right now restoration only store the current state in memory. so once a statefulwidget is destroyed, the state will be gone. If we enable this, the system need to try to preserve every state that has the restoration mixin without knowing whether it will be reuse in the future.

@esDotDev
Copy link
Author

esDotDev commented Sep 22, 2022

There could be a memory limit where the oldest items are purged when a limit is reached? Seems we're usually storing simple primitives here, so in (common) practice it would likely not consume a ton of space?

@chunhtai
Copy link
Contributor

I think it will be hard to have a default limit or even have this as a default behavior, I think what flutter can do is to provide a hook or an API to customize how state is store and read so that a third party package can decide how it is used.

@aytunch
Copy link

aytunch commented Sep 22, 2022

We are currently using an IndexedStack and all of our bottom nav bar tabs are always in memory. And our app is video+image intensive. I wish this kind of a solution existed. This would not only help memory pressure, but also battery consumption and general performance as well.

@chunhtai
Copy link
Contributor

chunhtai commented Sep 22, 2022

I think of it more, I don't think we need to get thing tangled up with restoration API, we could have a special widget that will prevent subtree to be destroyed and kept it in memory secretly until it was created again. We will still run into the risk about it gets abused and cause the OOM though. This will require change to framework because it currently don't allow dangling Elements or Rendering objects.

@aytunch
Copy link

aytunch commented Sep 23, 2022

@chunhtai if I understand your strategy correctly, you are talking about a widget similar to Offstage which will not be active at all until enabled? (i.e. no animations and focus will be present in the off state)

@chunhtai
Copy link
Contributor

No, something like

PreserveState(
  uniqueTag: ..
  child: TextField
),

what will happen is that once this widget is ever in the widget tree, its state will persist even if it is removed from tree later.

So you frame 1, you have

Scaffold(
 body: PreserveState(
  uniqueTag: 'abc',
  child: TextField
),
)

and they user type something in TextField

after several frame, tree remove the textfield for some UI change

Scaffold(
 body: SomeWidget()
)

and then after several frames, tree add the textfield back for some UI change

Scaffold(
 body: PreserveState(
  uniqueTag: 'abc',
  child: TextField
),
)

it will pick up the old state and fill the user typed text back to TextField

@aytunch
Copy link

aytunch commented Sep 23, 2022

Thank you for the demonstration. It is very promising.

child: condition == true ?
  PreserveState(
    uniqueTag: "abc",
    child: TextField()
  ) : Container(),

So even if condition is false and the TextField and the controller attached to it are not in the tree, once condition is back to true, whatever text the user entered will be visible.

If this was not TextField and instead a VideoPlayer what would the memory implications be?
When the PreserveState/VideoPlayer is not in the tree, do CPU and RAM is in full usage? Or are they as if there was no video player ever created in terms of resources?

What will happen behind the scenes of PreserveState?

@chunhtai
Copy link
Contributor

If we go with AutomaticKeepAlive route, the elements and renderobject will be kept alive, but in reasonable lifecycle state. so in theory, the videoplay if not pause, may still consuming resource, but it really depends on the implementation. This is just one idea I came up, there obvious need more thought on how to make it less dangerous to use.

Another way would be PreserveState only preserve restorable state, but that will require subtree to opt in to state restoration. This will store less data in memory to mitigate OOM, but it is still possible thing may go out of control.

@esDotDev
Copy link
Author

I think of it more, I don't think we need to get thing tangled up with restoration API, we could have a special widget that will prevent subtree to be destroyed and kept it in memory secretly until it was created again. We will still run into the risk about it gets abused and cause the OOM though. This will require change to framework because it currently don't allow dangling Elements or Rendering objects.

This would be a great addition to the SDK, yes it's a little dangerous but it provides so much power and utility. For years I've wished for some other way to keep views in memory other than Offstage widget.

We would just need some basic API to purge by ID, or purge all memory.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
a: state restoration RestorationManager and related APIs c: proposal A detailed proposal for a change to Flutter framework flutter/packages/flutter repository. See also f: labels. P3 Issues that are less important to the Flutter project team-framework Owned by Framework team triaged-framework Triaged by Framework team
Projects
None yet
Development

No branches or pull requests

8 participants