Skip to content

[go_router] Preserve the state of routes #99124

Description

@chunhtai

copied from csells/go_router#134

Currently there is no automatic way to have your routes remember their internal state as you navigate between them. This is often a desired UX feature as it is seen commonly on mobile and desktop platforms. For example, when I change tabs in a desktop app like Steam, it remembers my scroll position and selected sorting features (ephemeral view state).

The canonical way to do this in flutter is to use an Offstage widget, or something that wraps one, like an IndexedStack. The classic "nested navigator" pattern is a variation on this technique, where multiple stateful navigators are kept in memory using a TabBarView or IndexedStack.

I'm not sure if this is even possible with GoRouter, but would like to put this idea out there.

This is the core problem to solve. Given:

routes: [
  GoRoute(path: 'page1', pageBuilder: (_, state) => MaterialPage(key: state.pageKey, child: Page1())),
  GoRoute(path: 'page2', pageBuilder: (_, state) => MaterialPage(key: state.pageKey, child: Page2())),
]

When page1 is the path, the result of () => MaterialPage(key: state.pageKey, child: Page2()) is stashed Offstage somehow. Vice versa when `page2 is rendered. It wouldn't load everything at once, but rather lazy load and "remember" pages it had seen before. Likely there should be an option to opt-out certain views for persistence.

Activity

  1. added
    packageflutter/packages repository. See also p: labels.
    on Feb 25, 2022
  2. added
    P3Issues that are less important to the Flutter project
    c: new featureNothing broken; request for a new capability
    on Mar 1, 2022
  3. changed the title [-][go_router] It would be nice if routes could automatically persist their state[/-] [+][go_router] Preserve the state of routes[/+] on Apr 6, 2022
  4. arielbh commented on Sep 21, 2022

    @arielbh

    @chunhtai @johnpryan
    Not sure how to upvote, but I would be happy if I could.
    Perhaps if any of you can provide some clarity about it with 5.0 imminent (?)
    Is it on the road map? and I just need to be more patient?
    Should the community try to fix it? (perhaps even me? 😅 )

  5. chunhtai commented on Sep 21, 2022

    @chunhtai
    ContributorAuthor

    We are currently still in the planning phase. Given this is one of the higher priority issue, I think it will likely to be worked on first. If you would like to work on it, that will be great, too.

  6. esDotDev commented on Sep 22, 2022

    @esDotDev

    fwiw, when this was original discussed there did seem to be quite a bit of overlap here and the Restoration API used to restore stateful views when they are killed by the OS.

    It seems like a with a little bit of tweaking that API could be leveraged to give the impression of stateful routes, without actually needing to hold all the views/navigators offstage. For the most part we are generally just looking to maintain things like scroll position, textfield entries, animations, etc all of which already support restoration API... and then there is small bits of internal view-state state like sorting options, or loaded data, which we could manually configure w/ RestorableInt, RestorableBool etc.

    Specifically we would need some sort of restorationManager.restore(State) API, and maybe a RestoreStateOnMountMixin that automatically restores on initState.

    Just food for thought, obviously would be great if things just persisted fully with no work from the developer. But it wouldn't be too bad if we have to use Restoration API, and then at least all our views will also work well when killed off by a mobile OS (or even an app crash) so you'd get two birds with one stone at least.

    This also has the benefit of being more scalable, each view can inflate it's data when shown, without trying to hold everything in RAM.

  7. tolo commented on Sep 22, 2022

    @tolo

    Instead of attempting to save and restore state, would it be an option to build a solution where a nested GoRouter object is used to preserve a navigation stack, while at the same time establishing a relationship with the parent GoRouter to facilitate deep linking etc? I may be wrong, but I think this is what a lot of people expected #99126 to accomplish.

    For instance, Beamer does something like this. Here is an example with bottom navigation.

    I don't know, maybe this has already been discussed and dismissed, if so I'm sorry for the noise.

  8. Dohmanlechx commented on Sep 22, 2022

    @Dohmanlechx

    As of the new go_router version 5.0.0 I refactored our app from using IndexedStack to actually using the ShellRoute of go_router for our bottom navigation logic. It turned out that no state was preserved, which didn't make sense. When an app has a bottom navigation menu, we expect every tab to preserve its state while changing tabs.

    Does someone have a solution for this?

  9. esDotDev commented on Sep 22, 2022

    @esDotDev

    My guess is that the scaffolding is in place for this now that there is proper nested Navigator support w/ ShellRoute. I just thought I would bring up the RestorationAPI here, because it's a different way to tackle the problem that might co-exist with the ability to stash views in RAM.

    I guess that would be a different feature, more like "Restore state of routes", rather than "preserve".

    The end UX would be the same though: tabs and pages appear to hold their state

  10. esDotDev commented on Sep 22, 2022

    @esDotDev

    fwiw, I created an issue on this here: #80303 about 18mths ago, it seems like we really are so close to enabling "on demand" restoration with StatefulWidgets but it's not possible due to unfortunate design choices in the Restoration API (or I'm just missing it).

    By way of example, you could imagine something like this:

    mixin SaveRestoreStateMixin on State, RestorationMixin {
      // These two calls are missing, they would need to signal to all descendants to save/restore
      void initState() => // restorationManager.restoreState(this);
      void dispose() => // restorationManager.saveState(this)
    }

    With that functionality, we can have restorable views really easy, like:

    class _MyViewState extends State<MyView> with RestorationMixin, SaveRestoreStateMixin  {
      // Example of a custom piece of view-level state, that would be restored when page is loaded again
      RestorableBool _sortAsc = RestorableBool(false);
    
      @override
      String? get restorationId => 'myView';
    
      @override
      void restoreState(RestorationBucket? oldBucket, bool initialRestore) {
        registerForRestoration(_sortAsc, '_sortAsc');
      }
    
      @override
      Widget build(BuildContext context) {
        return Column(
          children: [
              // This textfield would restore it's internal state automatically 
              TextField(restorationId: '$restorationId-search'); 
              
               // Use restored view-level state for my list sorting.
               // The descendant `Scrollable` would restore it's own scroll position automatically as long as it is given a restorationId
              MyList(sortAsc: _sortAsc.value, restorationId: '$restorationId-scrollPos'),
          ]
        )
      }
    }

    All descendant widgets like lists or textfields would maintain their state since Restoration is built into the SDK. Having restorable view-level state is fairly simple as shown ^. For more complex data, it's likely that is hoisted up out of the widget tree anyways, and does not need to be persisted at the view/widget level.

    This strikes me as fairly elegant. It adds about 6 lines to a view for basic restoration support, and then 1 extra line for each view-level property that needs to be restored. In many cases simply adding the Mixins and a restorationId would be enough to give a view the impression of being preserved.

  11. tolo commented on Sep 22, 2022

    @tolo

    Yeah, I think I have stumbled over that issue before, and I agree that a state restoration based solution could possibly be used as a clean way of supporting "persistent" navigation stacks in for instance a BottomNavigationBar, without changing the intended way of configuring routes in go_router too much. I haven't used state restoration in Flutter myself that much though, but it seems like many parts that could form the foundation for such an implementation already exists in Flutter, even though this is perhaps a slightly different use case.

    I can however imagine that in some cases (i.e. when using complex view hierarchies, logic and state, along with deeper navigation stacks etc), it would be easier and less complex to just keep the views and navigation stacks in memory. And in that case, a solution with separate, nested routers (like the Beamer example) might be preferable. Of course, nested GoRouters already work on a basic level, but support for things like deep linking are missing (this demo/example for instance is using nested GoRouters, without deep linking support).

    But I guess the biggest question is how go_router could be modified to support these two different approaches. For the second approach at least, I imagine that it's more about documenting a particular usage pattern (with a good example), than making large changes in go_router.

  12. 9 remaining items

  13. rddewan commented on Feb 14, 2023

    @rddewan

    any update on it ? please we required this feature

  14. sovetski commented on Feb 26, 2023

    @sovetski

    It's really weird that there is no clean and especially official solution for this problem, which is still important and is even supported by native languages ​​like swift...

  15. lucavenir commented on Feb 26, 2023

    @lucavenir

    You can read updates and such in the linked pull request

  16. ahmednfwela commented on May 23, 2023

    @ahmednfwela

    @chunhtai this issue can finally get closed? :)

  17. johnpryan commented on May 23, 2023

    @johnpryan
    Contributor

    Yes, I think we can close this now. Pull request flutter/packages#2650 is merged and published as version 7.1.0

  18. github-actions commented on Jun 6, 2023

    @github-actions

    This thread has been automatically locked since there has not been any recent activity after it was closed. If you are still experiencing a similar issue, please open a new bug, including the output of flutter doctor -v and a minimal reproduction of the issue.

  19. locked as resolved and limited conversation to collaborators on Jun 6, 2023
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    P3Issues that are less important to the Flutter projectc: new featureNothing broken; request for a new capabilityp: go_routerThe go_router packagepackageflutter/packages repository. See also p: labels.

    Type

    No type

    Projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions