Multi stack routes #222
Replies: 13 comments 26 replies
-
This is really interesting. You're tackling the one thing that has always stymied me with the nested Router approach with GoRouter, which is how do you have pages outside the tabs, these 'external' pages as you call them. I'm not totally clear what
? |
Beta Was this translation helpful? Give feedback.
-
Here is my idea. The code would look like this: [
GoRoute(
path: '/users/:userId',
newScope: true,
pageBuilder: (state, context) => MaterialPage<void>(child: UserPage(...)),
routes: [
GoRoute(
path: 'albums/:albumName',
pageBuilder: (state, context) => MaterialPage<void>(
key: state.pageKey,
child: AlbumsPage(tab: state.params['albumName']!),
),
routes: [
GoRoute(
path: ':albumId',
pageBuilder: ...,
),
],
),
],
),
] and possible routes would be:
That flag would do this:
Does this make any sense? IMO the router class should only have definitions of routes and where those take you, not any business logic, so:
But on the other hand with the Hmm, this is hard. EDIT: as I am thinking of it, instead of optional boolean |
Beta Was this translation helpful? Give feedback.
-
Posting @lulupointu 's sample for reference as I ponder this proposal: https://github.com/lulupointu/go_router/blob/master/example/lib/tabbed_route.dart |
Beta Was this translation helpful? Give feedback.
-
What about removing the need of manually creating a GoTabbedRoute(
path: '/',
initialTabPath: '/red', // or initalTabIndex: 0,
routes: TabItem.values.map(_coloredRoute).toList(),
pageBuilder: (context, state, child) => MaterialPage<void>(
key: state.pageKey,
child: child,
),
navigatorBuilder: (context, controller, child) => Scaffold(
body: child,
bottomNavigationBar: BottomNavigation(
currentTabIndex: controller.currentIndex,
onSelectTabIndex: (index) => controller.go(index),
),
),
),
|
Beta Was this translation helpful? Give feedback.
-
I think I would separate the hidden-nested navigation from the
The best advantages of this is that it that both functionalities can be used separately. GoTabRoute(
path: '/',
initialIndex: 0,
tabRoutes: [
GoNavigatorRoute(
path: 'books',
initialLocation: '/'
routes: [
GoRoute(path: '/', pageBuilder: BookPageBuilder()),
GoRoute(path: '/:bookId', pageBuilder: BookPageBuilder()),
GoRoute(path: '/:bookId/edit', pageBuilder: BookPageBuilder()),
],
),
GoNavigatorRoute(
path: 'settings',
initialLocation: '/'
routes: [
GoRoute(path: '/', pageBuilder: BookPageBuilder()),
GoRoute(path: '/network', pageBuilder: BookPageBuilder()),
GoRoute(path: '/edit_password/', pageBuilder: BookPageBuilder()),
],
),
],
builder: (context, state, child, controller) => Scaffold(
body: child,
bottomNavigationBar: BottomNavigation(
currentTabIndex: controller.currentIndex,
onSelectTabIndex: controller.go,
),
),
) |
Beta Was this translation helpful? Give feedback.
-
@lulupointu I've been thinking a lot about this over the holiday break. The ideal solution would be that instead of nesting routes, you nest routers, specifically instances of |
Beta Was this translation helpful? Give feedback.
-
Regarding flutters behavior with multiple routers, the docs don't say a lot, but I guess they are pretty clear in what they do say:
So I guess we can think of child routers as just render surfaces, similar to the |
Beta Was this translation helpful? Give feedback.
-
fwiw, Using an _IndexedStackBuilder under the hood, and some sort of inherited |
Beta Was this translation helpful? Give feedback.
-
The lack of this feature is the #1 missing thing in GR, imo. I spent some time over the break digging into this issue enough to be able to really understand the need and get an intuition about the requirements of the implementation so I can actually judge @lulupointu's prototype. My ideal is still to be able to update |
Beta Was this translation helpful? Give feedback.
-
What I really like a about this proposed approach is this concept of I like the idea of being able to nest the declared GR at sub-locations in the tree, but I think you'd still want the option to declare all the tabbed-routes and normal routs at the top. And also, no matter where you put the routing tables, some controller somewhere has to manage the logic of filling up a lazily-loaded |
Beta Was this translation helpful? Give feedback.
-
Maybe we can start with a simplified use case to explore what "pushing it down" would really look like. Example site is 2 tabs, with 2 pages each, and there is 1 full-screen route. So 5 routes in total. The tabs should be stateful, and the full-screen routes should sit on top of all the tabs without wiping the tabs of all of their state. In lulu's proposal, I believe this would look something like:
Each This would end up generating the following valid "tabbed" pages which are stateful:
While also allowing /compose to be added on to the end of any of them, without wiping the underlying state:
Thinking on this for a bit it's quite interesting, and really allows you to compose 2 sets of routes together in a way that makes sense. @lulupointu please let me know how if I have butchered your proposal :D Q: How would we represent something like this if we instantiate the GR lower in the tree, or use multiple nested GR's? Maybe we can start from example usage code and work backwards to how it would work. |
Beta Was this translation helpful? Give feedback.
-
@lulupointu I do have a couple Q's about the proposal after looking at it more closely:
|
Beta Was this translation helpful? Give feedback.
-
Thinking through the 'push the routers down' approach, it seems like one approach that might work well is something like:
Maybe this can all be made automatic by making |
Beta Was this translation helpful? Give feedback.
-
This conversation aims at describing a new API called "multi-stack routes" and the changes needed internally in GoRouter (in particular the path matching algorithm).
This document is the result of internal discussion and might therefore seem esoteric but is mainly a way to discuss this matter with @csells is an environment with nice code highlighting 😄
External usage
The current usage of this API would look like this (names are likely to change):
Navigating to different tabs can be done with
context.go
as usual:The addition of this API (compared to simple route wrapping via
builder
) is that:tabbedRouteController
which allows to switch between tabs usingtabbedRouteController.go(index)
.tabbedRouteController
keeps track of the current index, accessible usingtabbedRouteController.currentIndex
tabbedRouteController.go(index)
to go back to a previous tab, you will find it where you left it.It should be noted that this is an expansion of the current
GoRouter
API, since it is simply impossible to achieve this behavior with the currentGoRouter
API (even with extensive url tracking or other tricks). This is becauseGoTabbedRoute
introduces (hidden) nested navigator which each have their own stack, hence the name: multi-stack API.Internal changes
Due to the way
GoRouter
is written, some internal changes are necessary. The most important one being the path matching algorithm. This will NOT be a breaking change for the existing GoRoute, and is only an internal technicality.To appreciate the challenges of the following topics, try to reflect on the fact that a
GoTabbedRoute
can be inside aGoTabbedRoute
inside aGoTabbedRoute
etc. Introducing algorithm general enough to handle this recursivity is the real challenge.1. Translate matching
GoRoute
s toPage
sThe first change comes to the way a
GoRoute
is translated to aPage
. Previously, eachGoRoute
which where matched by the path matching algorithm formed a this, which could be translated to a list ofPage
s (viaGoRoute.pageBuilder
). This list ofPage
s would then be used in FlutterNavigator
.However, as described earlier,
GoTabbedRoute
to create internalNavigator
s. Therefore, even one of itstabRoutes
is matching, its corresponding page should not be used in the mainNavigator
. Their should be a way when building the page to give the internal routes to its associatedGoRoute
(see description in "Path matching algorithm").2. Path matching algorithm
First, agreed convention on path matching on
GoTabbedRoute
s is the following:GoTabbedRoute
is match, but none of itstabRoutes
, this is not a matchtabRoutes
are matched like regular sub-routes, by appending their path to the path of the parentGoTabbedRoute
routes
(which are routes displayed aboveGoTabbedRoute.builder
) are matching by appending their path to any of thetabRoutes
path.Here is an example for clarity
Valid routes are:
Rule 1 stating that
/root
is not a valid route.Challenge 1: Rule 1
The first rule, boiling down to the fact that a
GoRoute
can have a matching path but is not matching (depending on its subroutes), is currently impossible.Challenge 2: Rule 2 & 3
The second and third rule describe the fact that a
GoRoute
can have two kinds of subroutes: internal routes (tabRoutes
in our case) and external routes (routes
).internal routes should be matched first by appending their path to the parent path:
childRestLoc
is empty, they can return the list of matches immediatelychildRestLoc
, they should use it both in their subroutes and it should be used for the external routesexternal routes:
GoRoute
do)subLoc
(name taken fromGoRouterDelegate._getLocRouteMatchStacks
)In both external routes cases, if they have no subroute and
childRestLoc
is not empty, this route should not be considered as matching.1 (Translate matching
GoRoute
s toPage
s) and 2 (Path matching algorithm) compatibilitySomething to keep in mind is that point 1 and 2 are intrinsically linked because 2 is used to return a bunch of
GoRoute
s, which should then be turned intoPage
s using 1.Limitations:
This API does not allow someone to
push
into a specific tab. This might be possible by addingTabbedController.push
but has not been explored yet so idk if this causes any implementation issue.Beta Was this translation helpful? Give feedback.
All reactions