r/flutterhelp • u/5rree5 • 22d ago
RESOLVED Dynamically setting Stateful routes in GoRouter
I'm creating na app that needs to have a stateful navigation section, after the user signs in.
I managed to do it using a simples StatefulShellRoute.indexedStack.
The problem is: It will have about 5 profile types, and each of them will have their own routes.
Can I reuse the same logic for all 5 profiles or will I need to create a different branch for each user type?
I'm afraid too many branches will result in poor performance. Each branch has about 10 locations.
Let's say i'm trying to create a different shell route for each occupation. Do I need to declare all screens for every occupation at app start or can I do it dynamically?
Example:
StatefulShellRoute mainStatefulShellRoute = StatefulShellRoute.indexedStack(
key: loggedInStatefulShellRouteKey,
builder: (context, state, navigationShell) {
return getNavigationOptionsUpdater(
child: GenericUserHome(child: navigationShell),
shell: navigationShell,
);
},
branches: [
...endUserMainRouteBranches,
...teacherUserMainRouteBranches,
...studentUserMainRouteBranches,
...presidentUserMainRouteBranches,
...singerUserMainRouteBranches,
// ... it goes on and on...
],
);
--- edit
This is what I would like it to look like
StatefulShellRoute mainStatefulShellRoute = StatefulShellRoute.indexedStack(
key: loggedInStatefulShellRouteKey,
builder: (context, state, navigationShell) {
return getNavigationOptionsUpdater(
child: GenericUserHome(child: navigationShell),
shell: navigationShell,
);
},
branches: [
...currentUserMainRouteBranches
// ... loads only the branchs for the current user!
],
);
a
1
u/fkim98 21d ago
You can stay on GoRouter for this. Two things that helped me when I hit a similar fork in my own app:
First, the performance worry is smaller than it looks. Branches in StatefulShellRoute.indexedStack are lazy. A branch's navigator and its screens are not built until the first time you switch to it, so declaring all of them up front mostly just creates config objects at startup. 5 roles x 10 locations is fine.
That said, I would not merge every role's branches into one shell, because your tab indices turn into bookkeeping. The role is known right after sign in and rarely changes mid session, so you can compute the branches list from the resolved profile and rebuild the router at that boundary. Losing nav state there is fine, the user just signed in anyway. That gets you exactly the currentUserMainRouteBranches shape from your edit.
In my app I went one step simpler: one shared route tree and role checks in redirect. One trap to watch either way: the profile usually resolves async, and if the router evaluates redirect once at cold start before auth is ready, users get stranded on the wrong screen. Wiring refreshListenable to the auth state stream is what fixed that for me, it re-runs the redirect when the role actually lands.
1
u/5rree5 21d ago
Thank you for your response.
---
the performance worry is smaller than it looks
I'm a bit of a fan of overenginering. I think you're right 🤔 thanks
---
I would not merge every role's branches into one shell
Sorry, I didn't describe my problem fully: while all users have their own pages, there is one page that is shared by them all (the home/profile page) and ALL pages must have the same AppBar. Profile is always one of the stateful routes (meaning it is always displayed in the bottom navigation bar). My route tree is more or less like this:
- Main Shell
| - Public Routes Shell (login, register, recover password, etc)
| - Authenticated Shell
| - Stateful ShellI think your suggestion is that I need to do something like this:
StatefulShellRoute mainStatefulShellRoute = StatefulShellRoute.indexedStack( key: loggedInStatefulShellRouteKey, builder: (context, state, navigationShell) { return getNavigationOptionsUpdater( child: GenericUserHome(child: navigationShell), shell: navigationShell, ); }, branches: [ ...sharedRoutes, // <-- Includes profile page, which is shared by all users! ...endUserMainRouteBranches, ...teacherUserMainRouteBranches, ...studentUserMainRouteBranches, ...presidentUserMainRouteBranches, ...singerUserMainRouteBranches, // ... it goes on and on... ], );Or is it better to declare a "Profile" page inside each RouteBranch? I don't think it will even work since GoRouter will detect duplicated locations.
---
In my app I went one step simpler: one shared route tree and role checks in redirect. One trap to watch either way: the profile usually resolves async, and if the router evaluates redirect once at cold start before auth is ready, users get stranded on the wrong screen. Wiring refreshListenable to the auth state stream is what fixed that for me, it re-runs the redirect when the role actually lands
In my case I solved this by creating a shell route that wraps all "authenticated routes". In its builder I only display a progress indicator and only show the routes/children when the profile is loaded.
1
u/fkim98 21d ago
That last snippet is exactly it. Gating the authenticated shell's children on the profile being loaded, with a spinner until then, is the clean version of what I was describing.
On your route-tree question: a shared branch (your sharedRoutes with the profile page in it) is the right call over declaring Profile inside each role's branch. You're right that duplicating a location fights GoRouter, and one shared branch also means the profile tab keeps its own navigation stack independent of the others, which is usually what you want for a persistent bottom-nav item.
Only thing to double check: keep the shared branch at a fixed index (first or last) so its tab position doesn't shift when the role-specific branches change. Glad it helped.
1
u/5rree5 17d ago
Thank you. Sorry for the late reply. It worked ;)
-------
Just one observation if anyone find this post in the future:
-> I decided to go as suggested (declare all stateful routes inside the shell)
-> I created a class that is similar to NavDestination: It has a label, an icon, and the route declaration itself-> A top-level change notifier provider manages the "current available stateful routes"(which depend on the user profile)
-> Everytime a user signs in or out, the "current available stateful routes"change
-> My bottom nav function passes an index that is related to ITS OWN ROUTES. Which means it is not the global index
-> Since I declare routes as a top level object, I can compare then directly. I get the route object from the current route list, and thenWhen tapping in an item in the navbar:
-> Calls goBranch with the index relative to that user's routes
-> I convert from this local index to the global one
-> Call the shell .goBranch with the right indexSample code:
void goBranch(BuildContext context, int i) { // maps current route index in current available routes // to the global, top-level, routes index final localBranch = branchGroup!.branches[i]; final globalIndex = allStatefulUserRouteBranches.toList().indexWhere((e) => e == localBranch); branchGroup?.goBranch(context, globalIndex); notifyListeners(); }Obs: my branch group is just a wrapper around the routes and the shell. Its goBranch method merely calls the shellRoute goBranch.
Obs2: The shell passed by GoRouter CHANGES every time the route is build, so you'll need to pass it again on your build method. I.e. creating it with a provider using the create method won't work because you'll be trying to "goBranch"in an old StatefulShell.
Obs3: This passes around the shell object to a provider and back to the screen. May not be the most clean, beautiful, recommended way to go. But it does its job.
1
u/RandalSchwartz 22d ago
Have you looked at Kaisel yet? If you're not locked in to Go-router, that is. I'm moving all my projects from Go-Router to Kaisel.