-
Notifications
You must be signed in to change notification settings - Fork 96
provide for multiple navigation stacks? #82
Comments
The iOS style nested state keeping bottom navigation pattern is one that is often asked for when talking about nested navigation. I wonder if it can't be built already with current version and some tiny bit of extra Flutter code. I might try it later. |
Happy to see this on the roadmap. I think it would be a great addition. My approach uses Navigator 1.0 and has some drawbacks as it keeps all the navigation stacks in memory. Potentially it would be better if a Navigator 2.0-based solution could keep track of the state of each page/stack, and recreate the pages as needed when the user switches between tabs. |
If you want to check out how the "competition" is doing this, then Routemaster supports it. But it is based on Cupertino solution in the SDK and limited to it. I wanted to have this feature also on other "custom" top level navigation widgets like sidebars, top navs in web, nav rails (SDK and custom variant) etc. When experimenting with Routemaster a few versions back, I "borrowed" the stack solution from Flutter SDK Cupertino code, and used it as a stack for any indexed custom navigator widget to create the same multiple navigation stacks solution. I think it can be used as is with GoRouter as well, but I have not tried the idea yet, been to busy with other things, but very curios to test it, ...eventually, hopefully soon. When I do, I will use a suitable package sample to try the idea. |
Beamer also supports it, they have an example in here: https://github.com/slovnicki/beamer/tree/master/examples/bottom_navigation_multiple_beamers |
Thanks, @rydmike . I'm looking forward to what you come up with. |
I'm focusing on this feature (and on the nested navigation in general) in my package routeborn. When there is a nested navigation, the navigation stack is branching into multiple navigation stacks like a tree. Currently the implementation is in a way of single source of truth regarding the navigation stacks (the SSOT is the Since the branching analogy, a method for changing branches in nested navigation is the EDIT: |
Hey @KristianBalaj would you want to join forces and bring your multiple navigation stack expertise to go_router? |
That would be nice, but currently I don't have resources to contribute to open source. |
I would love to see this feature! This is the one thing holding me back from using go_router. Thanks @csells for all of your amazing work so far. |
I think go_router does everything it can do to implement this feature, it can't really be implemented "out of the box", the flutter developer needs to do a little bit of extra lifting. This is because go_router is path based, there is fundamentally no such thing as 'multiple stacks' ever in memory. There is simply a path, and that will match to some stack of pages, and you can wrap those pages in an external tab menu, which gets you most of the way to nested nav in appearence, just not in behavior. What's left to do, is the key feature people expect from this paradigm which is "each tab preserves it's state". This breaks down into 2 parts as far as I see:
The first thing can be done by having some imperative code that saves off the last viewed path for each tab, and when the tab is pressed, it tries to look up the last known page, and show that rather than the default. For example, the default url for tab1 might be The second requirement is harder, having the stateful views remember/restore their state. I don't think there is actually a good solution for this that will work with GoRouter, unless you just have 1 top level pageRoute and all subsequent page routing is handled internally inside that widget. In theory the |
Couldn't that second requirement be accomplished with Offstage widgets and a Stack? (As described in the article at the top) That way when switching tabs the state wouldn't be lost. |
Ya, that's basically what I mean with "1 top level pageRoute and all subsequent page routing is handled internally inside that widget", like GoRouter is mapped to one persistent route, and an IndexedStack or something is handling the page-routing from there somehow? But then GoRouter is not doing much, except parsing some args for us and updating the browser location. It's worth noting too, if you can access the navigation path history, then it's pretty trivial to have these menu buttons that know they are "/tab1", and can either link to their default path, or look up the last history location that contained their path. So that part is not really as cludgy as it sounds and you end up with the UX most clients seem to want. Maybe there's a way with nested routers or navigators that I just don't understand. |
I'd love a sample of that... |
I do have a lib I made for almost this exact purpose https://pub.dev/packages/path_stack It just wraps So all the pages are just kept in memory for free. I'll see what I can do for a little demo of combining this with |
Nice! |
Thinking about it a bit more, my worry is that I don't think this is the right direction for GoRouter. We'd lose all the nice stacking logic you have, and have to reproduce most of the path parsing and guard logic. Seems like the better approach is to just try and remember tabs, and restore page state manually somehow. It's a bit harder, but then everything should just click together. |
is there some of the useful bits of GoRouter that can be exposed to implement this functionality? |
As long as you expose the history of paths, then the tab btn stuff should be pretty trivial. Restoration is the hard one, This line from the docs is interesting: If we could somehow trigger that restoration call each time we change routes, I think in theory we could get the "Stateful Tabs" paradigm working pretty neatly. Maybe it already does get called? I haven't played restoration API's for quite a while. |
I'm not sure what you mean by "history of paths". Do you mean the stack of routes that a location creates? Those trivial to expose. |
I guess I should have said history of locations. The equivalent of your browser history. A list of strings, representing the chronological history of your current browsing session. This would allow each tab to effectively "remember" it's last visited child page by just looking at the history, and checking for the most recent location, and loading that when the tab is pressed. If you don't expose this, if would be easy enough to listen to the router, and record each new location as it comes in allowing us to generate this history stack manually. Nice if it were exposed though. |
go_router doesn't track this info. you can listen to changes on the |
Ok, ya as I mentioned I know goRouter is a changeNotifier, so we could always listen to it and do it manually. Would be nice if it maintained this state though, since it's quite useful to be able to peer into history stack and do stuff with it, like we just demonstrated :D |
VRouter provides something like this: https://pub.dev/documentation/vrouter/latest/vrouter/VHistory-class.html so these use cases are made a little easier. |
Any thoughts on the more difficult aspect which is having routes be able to restore their state when loaded? I wonder if there is some way we can trigger |
Maybe we implement the UX first and then get the state restoration working after? |
@esDotDev |
Yep, we're trading RAM for improved UX in this case. "memory for free", I just mean that the routes are cached in ram, so no extra work is needed for the dev, and all state is maintained, from viewstate to internal scroll state etc. In my |
Some digging this morning turned up this very interesting looking API: Docs are pretty opaque, it states: If you jump over to see the rules, you get: Which I don't really understand. |
Ya that's a bug because I wasn't properly assigning page keys, but I was too lazy to re-record. It works properly when page keys are assigned: yeuaSZauES.mp4Basically Navigator was getting confused, and thinking we were adding a new page on top of the old, rather than replacing both pages. Since as far as it could tell it was just 2 children, of type |
Unfortunately it seems all flutter This means it's not possible for us to provide a single Not a deal breaker, but it means each toplevel page view that wants to restore it's children needs to add some boilerplate like:
Seems like restoration will be a better end-solution, as |
Thinking more about how GoRouter can support "stateful routes" in general, without needing any restoration/storage hassles, it fundamentally comes down to: can you cache the results of I think that comes down to a question of whether the child-widget of a If you can have stateful routes, and you can use a bit of logic to remember the last viewed page for any tab, then I think you get a really elegant and easy implementation of the "multiple nav stacks" paradigm, with full browser support. Also, fwiw, I have logged an issue last April requesting support for this use case w/ Restoration |
I created an issue specifically to discuss state preservation of routes, cause it's really it's own feature. Curious if anyone has any bright ideas on this problem in particular: |
I've created a small demo of a persistent bottom navigation with independent routes and helper methods to navigate across different tabs (similar to how the Play Store Console behaves with its Navigation Rail). I don't know how feasible would be for https://github.com/davidmartos96/go_router_bottom_nav_demo/tree/main/lib |
For reference, in addition to Andrea Bizzotto's article mentioned at the start, there's also Hans Muller's take on nested navigation: |
Thanx for the idea of using provided router delegates @davidmartos96 💟 I've implemented this approach in combination with a |
fyi @lulupointu on these approaches |
@csells yes I've read these before designing the Multi stack API. State restoration would not be at the level that @esDotDev (since this would means caching every visited page). However the API uses |
@esDotDev how is the current state of this topic? |
For now, I think your best approach is to just forward a bunch of urls to the same Unfortunately that means not using a lot of GR's path-parsing mechanisms as it just forwards |
Multi-Stack API looks like the best idea to me, it does all of the above stuff automatically, and allows you to continue to declare complex paths with GR. Where it might get a bit weird is the path-matching... maybe we can dig into any issues there. At the very least, it seems hard to explain in a simple way. |
There are definitely some cases where using a nested Navigator makes sense (see Hans' article for an example). But I wonder if we should recommend configuring the inner Navigator using the |
Here's a gist that shows what I mean. The app uses a BottomNavigation bar and an AnimatedSwitcher to set up lateral navigation between two screens. The first screen (LibraryScreen) builds a nested Navigator with the class LibraryScreen extends StatelessWidget {
final String? songId;
const LibraryScreen({
this.songId,
Key? key,
}) : super(key: key);
@override
Widget build(BuildContext context) {
// Use a local variable to promote to a non-nullable type
final songId = this.songId;
return Navigator(
onPopPage: (route, dynamic result) {
final didPop = route.didPop(result);
if (didPop) {
// The user isn't viewing the song screen anymore. Go to the
// parent screen.
GoRouter.of(context).go('/');
}
return didPop;
},
pages: [
MaterialPage(
child: Scaffold(
appBar: AppBar(),
body: Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Text(
'Home Screen',
style: Theme.of(context).textTheme.headline4,
),
TextButton(
onPressed: () {
GoRouter.of(context).go('/song/123');
},
child: const Text('View song 123'),
),
],
),
),
),
),
if (songId != null) MaterialPage(child: SongScreen(songId: songId))
],
);
}
} The problem is that once the user taps the "recents" tab in the BottomNavigationBar, the previous route might have been |
The scenario that I get asked about all the time is for a tabbed ui (whether on the top or the bottom) to have a nav stack per tab and that switching between tabs should remember the users spot in the nav stack for each tab. I've come to understand that this is what "nested nav" means to people and it's a common app requirement that go_router should support. |
Hi @davidmartos96 |
Yes, unfortunately that approach doesn't quite work for web deep linking. |
Right, this is one of the core pieces of logic provided by lulu's tabbed router example. Someone needs to remember what was the last visited url for each tab, so that when the tab is clicked, it can link into the last loaded url for that tab (or fallback to the "home tab") The simplest way I've done this in the past, is to just addListener onto GR, and save a list of url changes. Then when a tab is pressed, (eg "settings") it can just check that history and grab the last url that This is an alternative to actually having real nested navigators, each with their own stack of pages. I'm not sure how that would actually work when using names routes... this approach is more about faking it so it gives the appearance or general UX of individual navigators. |
Thanks for everyone work on this issue. Solving this would make go_router the router to use. One issue with other implementations of BottomNavigation and IndexedStacks is that on iOS you lose access to the scroll-to-top with status bar tap functionality. This is because of issues with the PrimaryScrollController from a root stack eating up those clicks. And if you try to pass PSC to all the different stacks then they all scroll up. So far using CupertinoTabScaffold/CupertinoTabBar doesn't seem to carry the problem (but has so many other problems with routers). Just throwing this out there as something to keep in mind with your implementations. If its working with your implementation, then mores the better! On the edge of my app to see if this comes to go_router as everything else seems to work really well and easy |
@mark8044 Yeah, I've faced the same issue and just done what Facebook does in their app. A repeated tap on the currently active bottom nav takes you back to the top 🤷 |
I haven't tried, but couldn't you wrap every tab with a PrimaryScrollController widget and pass a different controller to each tab? |
I think the solution lies within that idea. The problem so far with bottom nav implementations is you have a structure like this:
So the BottomNav implementation uses as IndexStack that lies on top of a Scaffold and the clicks just don't pass to where you want them to. Quite a few people have brought this up with flutter team, with ideas to allow us to access the root PSC, but that too is imperfect. Ive manually accessed the PSC before setting up a BottomNavigationBar and then passed it into the various tabs, and then the scroll to top does work, but then the problem is every list in every tab simultaneous scroll up, so that is also useless. Whats interesting is things like |
I could easily solve the scroll to top problem by using the following package: https://pub.dev/packages/scrolls_to_top |
@csells We need something like this: https://youtu.be/9oH42_Axr3Q. |
e.g. https://medium.com/coding-with-flutter/flutter-case-study-multiple-navigators-with-bottomnavigationbar-90eb6caa6dbf
The text was updated successfully, but these errors were encountered: