Skip to content

Instantly share code, notes, and snippets.

@tolo
Last active March 23, 2024 07:49
Show Gist options
  • Star 22 You must be signed in to star a gist
  • Fork 4 You must be signed in to fork a gist
  • Save tolo/f7e6c30cad3ac76085d75255ba509f10 to your computer and use it in GitHub Desktop.
Save tolo/f7e6c30cad3ac76085d75255ba509f10 to your computer and use it in GitHub Desktop.
Example showing how to use go_router to build persistent nested navigation (i.e. separate nested navigation trees) with a BottomNavigationBar.
// This temporary implementation is now obsolete, see instead:
// https://pub.dev/documentation/go_router/latest/go_router/StatefulShellRoute-class.html
@tolo
Copy link
Author

tolo commented Sep 27, 2022

This example shows how you can use go_router to build a persistent nested navigation with a BottomNavigationBar, where each tab uses its own separate Navigator. When switching tabs, the navigation state of the other tabs is retained, meaning you can return to the exact same state when going back.

@iEricKoh
Copy link

when I have a route defined with dynamic param, for example: /b/:id(\d+).

once i switch to screen B - details, /b/1, and then switch to screen A, I will got an error no routes for location: /b/:id(\d+) when I try to go back to tab B, i believe it was cause by the code GoRouter.of(context).go(_tabs[index].currentLocation);, any idea how to go to the screen B details screen in this case?

@tolo
Copy link
Author

tolo commented Sep 30, 2022

Yes, you are correct, it was indeed caused by how currentLocation was calculated. Updated now (using GoRouterState instead), try and see if it works better.

@iEricKoh
Copy link

iEricKoh commented Sep 30, 2022

@tolo thanks for the quick update!

the error i mentioned above is gone which is awesome!
tho there's a new issue i just found.

Screen A -> Screen B -> Screen B details -> (press back) Screen B -> Screen A -> tab Screen B

at this moment, it will have Screen B details open when i tapped the B on bottom bar.

expecting the screen B, not the details screen, isn't it?

the last state of each tab are: screen A, screen B, but it has screen B details open with i navigated from tab A to tab B.

@tolo
Copy link
Author

tolo commented Sep 30, 2022

Good find, thanks! There is currently a bug in go_router that causes the navigation state (RouteMatchList) to not update properly upon pop. Have tried to work around it in the latest update.

@iEricKoh
Copy link

iEricKoh commented Sep 30, 2022

awesome!!

so far everything works fine, expect one small things tho it might not be something related with you package.

flutter/flutter#111861
something wrong in the latest shell-route, the android back button is not working under release mode.

The issue was solved in the PR already already, flutter/packages#2652

since i have your branch as ref, not sure how do i merge two changes from two different branch together, i guess it required you to make the ref to remove-asserts-poproute in your branch instead?

  go_router:
    git:
      url: git@github.com:tolo/flutter_packages.git
      path: packages/go_router
      ref: nested-persistent-navigation

@tolo
Copy link
Author

tolo commented Oct 2, 2022

Hoping it will be possible to merge flutter/packages#2650 soon, so both these changes can be used 😊

@esDotDev
Copy link

esDotDev commented Oct 21, 2022

I'm confused how this actually works. Wouldn't the root navigator only hold state for the current stack of routes?

I understand that you store root navigator it in Offstage widget, and proxy .pages to the child navigator, just not getting how state is actually preserved in the non-active pages.

Are things like scroll position, textfield contents, or view-level sorting options preserved when changing between different tabs?

@tolo
Copy link
Author

tolo commented Oct 21, 2022

In this example, the root navigator will only contain a single Page, i.e. the one for BottomTabBarShellRoute. The Navigator for each tab will then hold the navigation stack for a particular tab, and since every Navigator will be kept "alive" in the IndexedStack (although Offstage), widgets in the navigation stacks will not be disposed when switching tabs.

So yes, everything will be as you left it, when returning to a previous tab.

@esDotDev
Copy link

I get it, super cool and elegant, very nice work!

@esDotDev
Copy link

esDotDev commented Oct 21, 2022

One potential I see here is that the scaffold has no access to a Navigator (and thus Overlay).

Can you show dialogs, or tooltips etc from within your scaffold with this technique? Or even just do Overlay.of(context) at all?

@tolo
Copy link
Author

tolo commented Oct 21, 2022

I get it, super cool and elegant, very nice work!

Thanks! 😊

Can you show dialogs, or tooltips etc from within your scaffold with this technique? Or even just do Overlay.of(context) at all?'

Should be fine. I just tried it (dialog and snackbar) in the sample code part of flutter/packages#2652. Perhaps I'll leave it in that sample.

@esDotDev
Copy link

esDotDev commented Oct 21, 2022

Weird... so that must mean that despite the root Navigator being offstage, it's internal Overlay is still accessible? INTERESTING!

@tolo
Copy link
Author

tolo commented Oct 21, 2022

No, the root navigator is never offstage, only the sibling navigators in the IndexStack used for the bottom navigation. That is, at any point in time, there will only be one nested navigator “onstage”, and the parent (root) navigator will remain onstage at all times.

@tolo
Copy link
Author

tolo commented Oct 22, 2022

Ok, this line threw me off: https://gist.github.com/tolo/f7e6c30cad3ac76085d75255ba509f10#file-nested_navigation_shell_route-dart-L126

Right, yeah, that's the ugly bit with this workaround, which you won't get with flutter/packages#2650. But that Navigator is anyway the one created by ShellRoute, not the root one. So it's just hiding an obsolete Navigator.

@amerblackbird
Copy link

Offstage widget cause main screen build twice.

@hmbenhaim
Copy link

hmbenhaim commented Nov 22, 2022

Offstage widget cause main screen build twice.

It's amazing just i have 1 issue.
just burn a few hours till I found it. when the keyboard gets visible offstage gets called and then rebuild the all page, again and again, so it's impossible to use it with text field on android, on desktop it fine but still rebuilds many times

@davidhicks980
Copy link

Excellent fix @tolo !

One small addition. If you are having trouble with hero animations, I'm fairly certain you can add a HeroController() observer to your nested Navigator. From what I can tell from the docs, MaterialApp usually adds this automatically.

 final heroController = HeroController();

  Widget buildNavigator(BuildContext context) {
    if (pages.isNotEmpty) {
      return Navigator(
        observers: [heroController],
        key: navigatorKey,
        pages: pages,
        onPopPage: _handlePopPage,
      );
    } else {
      return const SizedBox.shrink();
    }
  }

@tolo
Copy link
Author

tolo commented Dec 4, 2022

@davidhicks980, actually support for navigation observers (and hero controllers) is being implemented in the PR flutter/packages#2664. Hoping that will be finished soon so flutter/packages#2650 can benefit from it.

My main focus right not is to complete flutter/packages#2650 so it can be merged into go_router. @amerblackbird and @hmbenhaim, please have a look at that branch and the corresponding sample (stateful_shell_route.dart) there, and see if that works better. If not, please add a comment in flutter/packages#2650.

@hmbenhaim
Copy link

hmbenhaim commented Dec 4, 2022

@amerblackbird and @hmbenhaim, please have a look at that branch and the corresponding sample (stateful_shell_route.dart) there, and see if that works better. If not, please add a comment in flutter/packages#2650.

Hi @tolo I have tried this branch and this sample it works amazing so simple and so good and perform very well. I hadn't any issues with it so far I cloned this branch and I'm using it directly once it merge I'll use the library again. Waiting for the merge. Thank you!

@tolo
Copy link
Author

tolo commented Dec 5, 2022

Happy to hear @hmbenhaim! 😊

@sv-22
Copy link

sv-22 commented Jan 29, 2023

Mate, you are a legend! Thanks a lot! 🍻

@tolo
Copy link
Author

tolo commented Jan 29, 2023

Mate, you are a legend! Thanks a lot! 🍻

🙏😊🍻

@hazzo
Copy link

hazzo commented Feb 26, 2023

Here are my 2 cents until the PR adding support for this use case it's merged. If you updated to go_router 6.0.7+ this was introduced:

Use HeroControllerScope for nested Navigator that fixes Hero Widgets not animating in Nested Navigator.

So the above gist won't work until a small change it's made to work with 6.0.7 versions and above.
On the ScaffoldWithNavBar widget inside the BottomTabBarShellRoute the currentNavigator prop can't be casted to Navigator directly because the builder method will not return now a Navigatorbut instead a HeroControllerScope.

So in line 128 this change should be done to make the gist work:

ScaffoldWithNavBar(tabs: tabs, key: scaffoldKey,
  currentNavigator: (fauxNav as HeroControllerScope).child as Navigator,
  currentRouterState: state, routes: routes),
]);

@tanhsnkt1997
Copy link

why in StatefulWidget i call @OverRide
void initState() {
super.initState();
print("-----RUN------ ")}

  • At tab:: firstly -----RUN------ call double, but since the 2nd time onwards it only calls 1 time. Please help me :(

@Zeehshan
Copy link

Here are my 2 cents until the PR adding support for this use case it's merged. If you updated to go_router 6.0.7+ this was introduced:

Use HeroControllerScope for nested Navigator that fixes Hero Widgets not animating in Nested Navigator.

So the above gist won't work until a small change it's made to work with 6.0.7 versions and above. On the ScaffoldWithNavBar widget inside the BottomTabBarShellRoute the currentNavigator prop can't be casted to Navigator directly because the builder method will not return now a Navigatorbut instead a HeroControllerScope.

So in line 128 this change should be done to make the gist work:

ScaffoldWithNavBar(tabs: tabs, key: scaffoldKey,
  currentNavigator: (fauxNav as HeroControllerScope).child as Navigator,
  currentRouterState: state, routes: routes),
]);

type '_CustomNavigator' is not a subtype of type 'HeroControllerScope' in type cast

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment