How do I access the url path params from GoRouter when using a MultiBlocProvider? - flutter

Currently we're building an app to learn Flutter and Bloc pattern at my company. We use a MultiRepositoryProvider as the main widget and GoRouter for routing. My route looks like this:
GoRoute(
path: '/game/:id',
builder: (context, state) => GameDetailScreen(),
),
In the MultiRepositoryProvider the child is a MultiBlocProvider and the provider for this screen is:
BlocProvider(
create: (BuildContext context) {
return GameDetailBloc(context.read<FirestoreRepo>());
},
),
The BlocProvider's create function returns the BuildContext but it's not clear to me how I get the GoRoute state to pass the url param id to the GameDetailBloc.
We managed to get this to work by setting the game's id in GoRoute's build function when creating the GameDetailScreen. Then we removed that BlocProvider in the MultiBlocProvider and then accessed the bloc from the BuildContext when building the widget but it doesn't seem correct and we're trying to find the "correct solution" to this problem. Any help is greatly appreciated. Thanks!

go_router has it's params in its state.
Hence pass the state to the page
Router
GoRoute(
name: "test",
path: "/test/:id",
builder: (context, state) {
return SampleWidget(
goRouterState: state, ๐Ÿ‘ˆ Pass state here
);
},
),
Usage
context.goNamed("test", params: {"id": "123"}),
Accesing in the page
class SampleWidget extends StatelessWidget {
GoRouterState? goRouterState;
SampleWidget({super.key, this.goRouterState});
#override
Widget build(BuildContext context) {
print(goRouterState?.params.toString()); ๐Ÿ‘ˆ access anywhere like so
return const Scaffold(
body: ...
);
}
}
I couln't completely understand the question. I have attempted to answer based on my interpretation. Let me know the specifics if you have any other requrirements.

Uri.base.toString().replaceAll(Uri.base.origin, '')

Related

How can you access GoRoute state.params outside of the routing process?

I hope some simple pseudocode is enough so that both me and you can understand the question and answer.
The problem I'm facing is especially hard when using flutter-web, where the refresh restarts the whole program.
I want to use the path parameters to build objects in a child widgets build method.
GoRouter(routes:...,GoRoute(path="/categories/:category/",
builder:(context,state){
category = state.params['category];
return ParentWidget(category);}
ParentWidget extends StatelessWidget {
build(context){
return ChildWidget();}}
ChildWidget extends StatlessWidget {
build(context){
return "do something with category";}}
Now one way which I can think of and should technically work without any errors would be to pass the params first into the ParentWidget and then pass it along to the next child and so on. But if there's a long chain of child widgets it gets quite tedious and I'm guessing error prone as well. The other thing I was thinking was to use providers: pass the param once again to the parent widget and then make the parent widget send it to a provider. But then the question becomes, where do I do it? Apparently I shouldn't update a provider on build(), but if I do it on initState() it only does it bugs out if I change into a route that include the same widget tree but different path e.g. /categories/apples -> /categories/bananas.
Ps. For some reason I don't remember what the problem with refreshing was. (It has something to do with resetting the providers). But I'll update it when I remember.
go_router has it's params in its state.
Hence pass the state to the page
Router
GoRoute(
name: "test",
path: "/test/:id",
builder: (context, state) {
return SampleWidget(
goRouterState: state, ๐Ÿ‘ˆ Pass state here
);
},
),
Usage
context.goNamed("test", params: {"id": "123"}),
Accesing in the page
class SampleWidget extends StatelessWidget {
GoRouterState? goRouterState;
SampleWidget({super.key, this.goRouterState});
#override
Widget build(BuildContext context) {
print(goRouterState?.params.toString()); ๐Ÿ‘ˆ access anywhere like so
return const Scaffold(
body: ...
);
}
}

Flutter initial routes with parameters

I need to pass an argument for my initialRoute. I found this issue and tried it like this:
initialRoute: AuthService.isLoggedIn() ? Views.home : Views.firstStart,
onGenerateInitialRoutes: (String initialRouteName) {
return [
AppRouter.generateRoute(
RouteSettings(
name: AuthService.isLoggedIn() ? Views.home : Views.firstStart,
arguments: notificationPayloadThatLaunchedApp,
),
),
];
},
onGenerateRoute: AppRouter.generateRoute,
This almost works. The problem I have is that somehow after calling this...
Navigator.pushReplacementNamed(
context,
Views.loading,
);
... my Multiprovider which is a parent of my GetMaterialApp is being called again which crashed my app because I call a function when initializing my providers:
#override
Widget build(BuildContext context) {
return MultiProvider(
providers: [
ChangeNotifierProvider(
create: (context) {
var dataProvider = DataProvider();
dataProvider.init( // <- this is called again which should not happen
context,
);
return dataProvider;
},
),
],
child: GetMaterialApp(
title: 'Flutter Boilerplate',
navigatorKey: Get.key,
initialRoute: AuthService.isLoggedIn() ? Views.home : Views.firstStart,
onGenerateInitialRoutes: (String initialRouteName) {
return [
AppRouter.generateRoute(
RouteSettings(
name: AuthService.isLoggedIn() ? Views.home : Views.firstStart,
arguments: notificationPayloadThatLaunchedApp,
),
),
];
},
onGenerateRoute: AppRouter.generateRoute,
),
);
}
I feel like the way how I pass the initial argument is wrong. Is there any other/better way to get this done? Let me know if you need any more info!
I think you misunderstand the usage of new API onGenerateInitialRoutes cause by the name it should load without the calling of
Navigator.pushReplacementNamed(
context,
Views.loading,
);
API at all. if you call this route from another widget, it means this is already the second route already. it should be the default route for your application. so it makes no sense to give a parameter to an initial route at all.
since you have used Provider Package, it is better to just get whatever argument(parameter) that you want to send via Provider API itself.
if you want to give hardcoded data, then just treat it as a normal Widget class.
home: MyHomePage(name:"parameter name",data:DataHome()), :
GetMaterialApp(
title: 'Flutter Boilerplate',
navigatorKey: Get.key,
home: MyHomePage(name:"parameter name",data:DataHome()),
onGenerateRoute: AppRouter.generateRoute,
);
take note that if you calling Navigator.pushReplacementNamed API . your provider will be gone cause of the Flutter Widget Tree most Navigator API will create a new Widget tree level from the root. since the provider only provides for all its child only. so you cant use any provider data since you have a different ancestor.
so if you only have one page to go I suggest you use the home attribute in MaterialApp API cause it will treat your initial page as a child and Provider API can receive the data cause up in the Widget tree, it has an Ancestor Provider API:
MultiProvider(
providers: [
ChangeNotifierProvider(
if you want to move between pages via navigator after the main page.
consider using NestedNavigator so flutter will not create a new route from the root of the widget tree. so you still can access Provider API's data.
guessing from your variable name. I assume that you want to handle a notification
routing, check this deeplink

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

Access BlocProvider.of in routes which are outside the initial flow of the application

I need the UserBloc to be accessible globally around my App because it seems like is not possible to access a BlocProvider in routes which are not directly dependent on the main widget tree.
The WidgetTree structure of my app (the relevant part) right now is:
App
โ”œโ”€โ”€ HomePage
โ”‚ โ””โ”€โ”€ LessonPage
โ””โ”€โ”€ ProfilePage
From the LessonPage is possible to navigate to the ProfilePage but this breaks the child-parent relationship for which you can call BlocProvider.of.
So i wonder how should I handle this properly.
The solution i'm using right now is the following, but this is causing me issues since close of UserBloc is never called since App is never really disposed, leaving the Stream open when it should not.
runApp(MultiBlocProvider(
providers: [
BlocProvider<AuthBloc>(
create: (context) =>
AuthBloc(authRepository: authRepository)..add(AppStarted()),
),
//.. other BlocProvider not relevant to this question ..
],
child: BlocProvider<UserBloc>(
create: (BuildContext context) => UserBloc(
userRepository: UserRepository(),
authBloc: BlocProvider.of<AuthBloc>(context),
),
child: App(
authRepository: authRepository,
),
)));
});
Another possible solution i'm seeing here it will be to handle this in the route handler, for instance:
//... some more code...
#override
HandlerFunc get handlerFunc => (BuildContext context, Map<String, List<String>> params) {
return BlocProvider<UserBloc>(
create: (BuildContext context) => UserBloc(
userRepository: UserRepository(),
authBloc: BlocProvider.of<AuthBloc>(context),
),child: ProfilePage(email: params[email][0])
);
};
}
But this will create several instances of the UserBloc which will have to call close as much time as they got instantiated basically performing, at best, useless operations.
I don't really like neither of the 2 solution, that's why i'm seeking for help.
*Note: for handling routes i'm using https://pub.dev/packages/fluro so that method handleFunc is taken from that, it will simply be called on Navigator.pushNamed of any registered route.
Which one is the advisable approach?
Any other solution i'm not seeing here?
Thank you in advance for your help!

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
//..
)