NavigationRail with go_router - flutter

I'm developing a web/desktop application that has a fairly standard UI layout involving a NavigationRail on the left and a content pane taking up the remainder of the screen.
I've reciently added go_router so I can properly support URLs in web browsers, however in doing so I have lost the ability to have any form of transition/animation when moving between pages as calling context.go() causes a hard cut to the next page.
Theres also the issue that go_router routes have to return the full page to be rendered meaning I need to include the navigation rail on every page rather than each page being just the content relevant to that page. I believe this is also the main reason all animations are broken, because clicking a link effectively destroys the current navigation rail and builds a new one for the new page
I couldn't see anything in go_router but is there any form of builder API available that can output and refresh a single section of the page? I'm thinking of something like bloc's BlocBuilder which listens for state changes and rebuilds just the widget its responsible for when a change occures.
Alternatively, is there a way to update the current URL without rebuilding the whole page?
Or is go_router just not capable of what I'm after, and if so, are there any alternatives that can do this?
The overall effecti I'm after is similar to the material site https://m3.material.io/develop
Clicking around the various buttons feels like you are navigating around within an app rather than clicking on links and loading new pages
Thanks for your help

I solved this with a Shell Route. I got the idea when going through the go_router 5 migration guide. I saw that they had a navigatorBuilder property in which they were wrapping every route in a Scaffold with AppBar. And it said to migrate:
Before migration:
final GoRouter router = GoRouter(
routes: <GoRoute> [
GoRoute(
path: '/',
builder: (_, __) => const Text('/'),
),
GoRoute(
path: '/a',
builder: (_, __) => const Text('/a'),
)
],
navigatorBuilder: (BuildContext context, GoRouterState state, Widget child) {
return Scaffold(
appBar: AppBar(title: Text(state.location)),
body: child,
);
}
);
After Migration:
final GoRouter router = GoRouter(
routes: <GoRoute> [
ShellRoute(
builder: (_, GoRouterState state, child) {
return Scaffold(
body: child,
appBar: AppBar(title: Text(state.location)),
);
},
routes: [
GoRoute(
path: 'a',
builder: (_, __) => const Text('a'),
),
GoRoute(
path: 'b',
builder: (_, __) => const Text('b'),
)
],
),
],
);
Now every route in the ShellRoute.routes property will be wrapped with that Scaffold and AppBar, it will not rebuild upon navigation, so will behave as a persistent AppBar, NavigationBar, or whatever should. Now it shows the page transition animations when you do context.go('some_route') (for example from a NavigationBar in the ShellRoute). You also don't run into the issue Felix ZY was having, accessing GoRouter from the context.

Related

GoRouter | Extra is cleared on hot reload

Let's say I have a route defined as the following:
GoRoute(
path: Screen.classDetail.path,
builder: (context, state) => AdminClassScreen(
classItem: state.extra as Class,
),
),
and I navigate to it using
router.push(Screen.classDetail.path, extra: extra);
When I hot reload on Web, the application goes into a bad state because the object I am passing in as extra is not preserved. Is there a way around this?

How to get previous location of route using Go Router?

What im trying to do is for example if router came from login to verification screen do something but when prev location from sign up and not login dont do something.
So i want to get the prev location to do some logic to it, im aware that i can pass params to pages then do the logic from it for example passing a bool and do the logic if true or false but i want a better or right way, if you know want i mean. Thank you.
And i encounter this method from go router, what it does? is it for logging purpose only, im trying to add route name or location in the parameter but it throws an error. What param do i need to input in here, i dont know where to find Route thank you.
context.didPush(Route<dynamic> route, Route<dynamic>? previousRoute);
You should pass the route you are coming from to the route you are pushing to.
This way you can know where you came from and apply the specific logic.
I found a solution by creating a main route then put the other in the subroute of the router then i will call
context.location
in the page from GoRouter pagckage then it will return the path of the location
GoRoute(
name: AppRoute.login,
path: '/login',
builder: (context, state) => LoginView(credentials: state.extra as Credentials?),
routes: [
GoRoute(
name: AppRoute.signUp,
path: 'signup',
pageBuilder: (BuildContext context, GoRouterState state) => CupertinoPage<void>(
key: state.pageKey,
child: const SignupView(),
),
),
GoRoute(
name: AppRoute.verifyNumber,
path: 'verifyNumber',
pageBuilder: (BuildContext context, GoRouterState state) => CupertinoPage<void>(
key: state.pageKey,
child: VerifyNumberView(user: state.extra as User),
),
),
]
),
its important that you put your sub route because if not it will only return its own route not including the base route
result with subroute : /login/forgot_password
result without subroute: /forgot_password
base on the result i can do the logic

Unable to access BLoC in go_router v5.0 redirect method, BLoC is not injected in the context

I'm using flutter_bloc and go_router.
This is an implementation of multiple-page-form, for this demo it's just 2 pages. I used ShellRoute to provide MultiFormBloc to 2 routes, so that both routes will have access to the same MultiFormBloc instance.
In the redirect method in step 2 route, I wanted to conditionally redirect user back to step 1 based on if user has completed step 1 or not (since that there's a possibility that user can request for step 2 page directly without finishing step 1, e.g. entering /step2 in the browser's search bar)
However, it seems like MultiFormBloc is not injected in the BuildContext provided in the redirect method.
How can I access bloc injected deep in the tree (not top level in main.dart) in the redirect method? Or is there a better way to do this conditional logic?
...
ShellRoute(
builder: (context, state, child) {
// Use [ShellRoute] for the sole purpose of providing [MultiFormBloc] to
// 2 routes. So that the 2 routes have access to the same bloc instance.
return BlocProvider(
create: (context) => MultiFormBloc(),
child: child,
);
},
routes: [
GoRoute(
name: 'step1',
path: 'step1',
builder: (context, state) => Step1(),
routes: [
GoRoute(
name: 'step2',
path: 'step2',
builder: (context, state) => Step2(),
redirect: (context, state) {
// If user haven't completed form 1, redirect user to form 1 page.
// Error accessing [MultiFormBloc], bloc not injected in context.
if (context.read<MultiFormBloc>().state.step1Value == null) {
return '/step1';
}
return null;
},
),
],
),
],
),
...
If you want to pass the same bloc to independent rotes you have to pass it’s value instead. You first create your bloc final myBloc = MyBloc(). Then, pass its value to each route.
// When you want to navigate to Step1()
BlocProvider.value(
value: myBloc,
child: Step1(),
)
// When you want to navigate to Step2()
BlocProvider.value(
value: myBloc,
child: Step2(),
)
Now, both Step1() and Step2() routes share the same bloc instance and have access to the same state.

StreamProvider returns no data when used with Navigator

The issue is that I don't get any values out of my StreamProviders (which are defined on a global level) within my Authenticated route:
runApp(MultiProvider(
providers: [
Provider.value(value: userService),
StreamProvider.value(value: authService.userStream, initialData: null),
StreamProvider.value(value: userService.userDataStream),
StreamProvider.value(value: userService.characterStream),
],
child: MyApp(),
));
}
I noticed that it's to do with the logic that I have for my Navigator (if I remove it the provider values are passed down the widget tree as expected). The Navigator I'm using is based around the idea that the app has 3 states: Not Authenticated, Authenticated and Authenticated-First-Time. I get the value whether I'm authenticated from the loginStream (so far everything works):
#override
Widget build(BuildContext context) {
return StreamBuilder(
stream: loginStream,
builder: (ctx, snapshot) {
if (!snapshot.hasData) return Loading();
LoginState state = snapshot.data;
if (state == LoginState.LOGGED_OUT) return LoginScreen();
if (state == LoginState.FIRST_TIME) return CharacterCreationScreen();
return Navigator(
key: navigatorKey,
initialRoute: "/home",
onGenerateRoute: (settings) => PageRouteBuilder(
pageBuilder: (ctx, _, __) => routes(settings)(ctx),
transitionsBuilder: pageTransition,
),
);
},
);
The thing is that if I'm Authenticated and say in the HomeScreen, then both userDataStream and characterStream return null even if there's actual data available. If I remove the StreamBuilder + LoginLogic itself and just have the Navigator widget returned above, then HomeScreen gets the correct values.
UPDATE:
I noticed that it's not even the StreamBuilder. If I remove the 3 if's within the builder, then the stream values are propagated correctly. Not sure why that happens.
I´m not quite sure if this helps since I´m lacking details but here is what I noticed so far:
If you create the objects in the multiprovider for the first time you should not use .value - check if this applies.
Try cleaning up the if statements in the function body of your StreamBuilder (use if, else if and else keywords.
Also, following your description, it sounds like whenever an if statement is true, returns and thus cancels the build´s function body, the stream somehow resets and defaults to null. Maybe look into that & update your question.
Change this
Provider.value(value: userService),
StreamProvider.value(value: authService.userStream, initialData: null),
To this
Provider(create: (context) => userService)
StreamProvider(create:(context) => authService.userStream, initialData: null),
Do the same for all the providers that u are registering
To expose a newly created object, use the default constructor of a provider. Do not use the .value constructor if you want to create an object, or you may otherwise have undesired side effects.
https://pub.dev/packages/provider

BlocProvider.of() called with a context that does not contain a Bloc of type MainBloc

I have a MainBloc that resides inside a main route, this route has a bottom app bar with multiple sub-routes, I want the same BLoC to run on all five sub-routes so that when one of them changes the state of the block the others will see the effect.
I tried this SO question but its really far from what I'm looking for, also I tried following what the error advised me to, but didn't work, here is the message that I got:
This can happen if:
1. The context you used comes from a widget above the BlocProvider.
2. You used MultiBlocProvider and didn't explicity provide the BlocProvider types.
Good: BlocProvider<MainBloc>(builder: (context) => MainBloc())
Bad: BlocProvider(builder: (context) => MainBloc()).
Main route:
#override
Widget build(BuildContext context) {
return MultiBlocProvider(
providers: [
BlocProvider<MainBloc>(
builder: (BuildContext context) => MainBloc(),
),
BlocProvider<OtherBloc>(
builder: (BuildContext context) => OtherBloc(),
),
],
child: /..., //here I have the bottom app bar with 5 buttons to navigate between sub-routes
);
one of the sub-routes:
#override
Widget build(BuildContext context) {
final MainBloc bloc = BlocProvider.of<MainBloc>(context);
return /...; //here I have the context of this sub-route.
}
from what I've seen from tutorials and articles this code should work, but I can't seem to find why not.
The problem is you cannot access InheritedWidgets across routes unless you provide the InheritedWidget above MaterialApp. I would recommend wrapping your new route in BlocProvider.value to provide the existing bloc to the new route like:
Navigator.of(context).push(
MaterialPageRoute<MyPage>(
builder: (_) {
return BlocProvider.value(
value: BlocProvider.of<MyBloc>(context),
child: MyPage(),
);
},
),
);
You can find more detailed information about this in the bloc documentation
As this child has the bottom app bar:
child: /..., //here I have the bottom app bar
then I assume that the MultiBlocProvider(..) is not wrapping the whole part of app which is using this Bloc, my suggestion here is to wrap the "MaterialApp" with "MultiBlocProvider".
return MultiBlocProvider(
providers: [..],
child: MaterialApp(..) // Set MaterialApp as the child of the MultiBlocProvider
//..
)