How to scope a ChangeNotifier to some routes using Provider? - flutter

I have a ChangeNotifier, and I would like to share it between multiple routes but not all routes:
Page1 is my first page. I need share data of ChangeNotifierProvider with Page2, Page3 and Page only and on enter Page1 call dispose of my ChangeNotifierProvider.
How can I do this using provider?

To do so, the easiest solution is to have one provider per route, such that instead of:
Provider(
builder: (_) => SomeValue(),
child: MaterialApp(),
)
you have:
final value = SomeValue();
MaterialApp(
routes: {
'/foo': (_) => Provider.value(value: value, child: Foo()),
'/bar': (_) => Provider.value(value: value, child: Bar()),
'/cannot-access-provider': (_) => CannotAccessProvider(),
}
)
It is, on the other hand, not possible to have your model "automatically disposed".
provider is not able in such a situation to know that it is safe to dispose of the object.

Related

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.

ModalRoute.withName never returns true in pushNamedAndRemoveUntil

I have been wrestling with this issue for a couple of days now and I haven't been able to find a solution, hence the post.
At one point in my app, I need to pop 2 routes from the navigator stack and push a new route. After researching the best way to do this, I have found that using pushNamedAndRemoveUntil is the best way, as I can specify ModalRoute.withName('/<route_name>') and it will pop the routes until it reaches /<route_name> at which point it will stop and push the new route. This is the line I have been using Navigator.of(context).pushNamedAndRemoveUntil('/raceadmin_reporting', ModalRoute.withName('/raceadmin_page'));.
My issue though is that it doesn't work for me. It doesn't seem to matter what I put in /<route_name>, pushNamedAndRemoveUntil pops all the routes, which leads me to believe that ModalRoute.withName never returns true.
I have also tried Navigator.of(context).pushNamedAndRemoveUntil('/raceadmin_reporting', (route) => route == RaceAdminPage.route()); and it doesn't work either.
When I look at the debugger, this is what I see:
App Navigator Stack
The route I am trying to pop until is the RaceAdmin page, which is clearly in the stack. In the definition of that class, I added the line static const routeName = '/raceadmin_page'; which is what I call in ModalRoute.withName('/raceadmin_name') and it doesn't work.
My routes are defined in the routes.dart file as per below:
static Route<dynamic> generateRoute(RouteSettings settings) {
switch (settings.name) {
case '/':
return MaterialPageRoute(builder: (context) => RailMeatApp());
case '/raceadmin_page':
return MaterialPageRoute(builder: (context) => const RaceAdminPage());
case '/raceadmin_pendinglist':
return MaterialPageRoute(builder: (context) => PendingResultsList());
case '/raceadmin_pendingresults':
final args = settings.arguments as Map<String?, String?>;
return MaterialPageRoute(
builder: (context) => PendingResultsPage(
raceId: args['raceId'] as String,
));
case '/raceadmin_reporting':
return MaterialPageRoute(builder: (context) => RaceAdminReporting());
default:
return _errorRoute();
}
}
And my MaterialApp is defined as per below:
return MaterialApp(
navigatorKey: _navigatorKey,
home: _railmeatHome(),
onGenerateRoute: RailmeatRoutes.generateRoute,
);
}
In the Flutter debugger, I can look at the MaterialApp widget and I can see in its state that the navigator has 4 entries in its _history property as shown below:
MaterialApp Widget Properties
If I click on any of the entries in the _history, I see the the same info as below:
Route property in _history
As you can see, the name property under _settings is null, which, in my mind, would explain why ModalRoute.withName can't find the right route, but I am not sure that my thinking is accurate.
What should I do differently to make pushNamedAndRemoveUntil work?
Thanks a lot in advance,
Bertrand.
Try this instead
Navigator.of(context)
.pushNamedAndRemoveUntil('/routename', (Route<dynamic> route) => false);
Or
Navigator.of(context).popUntil(ModalRoute.withName('/root'));

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

How named routes in Flutter eliminate duplication?

I cannot understand the reason why someone should use named routes, with Navigator.pushNamed(), instead of the normal way with Navigator.push().
The tutorial page states that:
if we need to navigate to the same screen in many parts of our apps,
this can result in code duplication. In these cases, it can be handy
to define a “named route,” and use the named route for Navigation
Duplication
How will the duplication be generated when using simple routing and how it will can be eliminated with the use of named routes?
I fail to understand what is the difference of
Navigator.push(
context,
MaterialPageRoute(builder: (context) => SecondRoute()),
);
from
Navigator.pushNamed(context, '/second');
in the context of duplication.
Consider you go with Navigator.push() in many widgets:
// inside widget A:
Navigator.push(
context,
MaterialPageRoute(builder: (context) => SecondRoute()),
);
// inside widget B:
Navigator.push(
context,
MaterialPageRoute(builder: (context) => SecondRoute()),
);
// inside widget C:
Navigator.push(
context,
MaterialPageRoute(builder: (context) => SecondRoute()),
);
Now let say you need to change your App and the widget SecondRoute needs to receive a value on it's constructor. Now you have a problem, since you have multiple copies of the same code on several locations, you need to make sure you will update all of those copies, which can be tedious and error prone:
// inside widget A:
Navigator.push(
context,
MaterialPageRoute(builder: (context) => SecondRoute(
title: 'Title A',
)),
);
// inside widget B:
Navigator.push(
context,
MaterialPageRoute(builder: (context) => SecondRoute(
title: 'Title B',
)),
)),
);
// inside widget C:
Navigator.push(
context,
MaterialPageRoute(builder: (context) => SecondRoute(
title: 'Title A', // ERROR! Forgot to change the variable after a copy/paste
)),
)),
);
Now let's consider you go with named routes.
Firstly I would never recommend anyone to actually use the name directly for navigation, but instead use a static variable reference, this way if you need to change it in the future its way simpler and secure, as you can't forget to update it anywhere, like this:
class Routes {
static const String second = '/second';
}
Another way is to have a reference inside the route itself, a static const String inside SecondRoute, so we can use it as SecondRoute.routeName. It's a matter of personal preference IMO.
Then your widgets will navigate using:
// inside widget A:
Navigator.pushNamed(context, Routes.second); // Routes.second is the same as '/second'
// inside widget B:
Navigator.pushNamed(context, Routes.second);
// inside widget C:
Navigator.pushNamed(context, Routes.second);
Now if you need to pass a parameter to SecondRoute upon creation you can do it in a centralized location using the MaterialApp onGenerateRoute, as this tutorial explains in more detail. Your code will be changed to:
// inside widget A:
Navigator.pushNamed(context, Routes.second, arguments: 'Title A');
// inside widget B:
Navigator.pushNamed(context, Routes.second, arguments: 'Title B');
// inside widget C:
// You can still make mistakes here, but the chances are smaller.
Navigator.pushNamed(context, Routes.second, arguments: 'Title C');
MaterialApp(
onGenerateRoute: (settings) {
if (settings.name == Routes.second) {
final String title = settings.arguments;
return MaterialPageRoute(
builder: (context) => SecondRoute(title: title),
);
}
},
);
The amount of duplicated code is decreased, but on the other hand the onGenerateRoute code gets more complex as you make more routes, as all of their creation will be centralized there, so IMHO it's more about a personal preference then a general guideline.
Push and PushNamed have the similar effect, Push will switch to the route you specified while PushNamed will switch to the route with the route name specified.
What the Tutorial page means for duplication is duplication of code not duplication of routes.
For instance, you have a route where you would want to check whether the user is signed in and show the corresponding page
Using Push only:
Page1:
//This is page 1....
RaisedButton(
child: Text('Go to second'),
onPressed: () {
if (user.state = "login") {
Navigator.of(context).push(
MaterialPageRoute(
builder: (context) => SecondPage(),
),
)
}else{
Navigator.of(context).push(
MaterialPageRoute(
builder: (context) => SecondPageAnonymous(),
),
)
}
}
)
....
In another page, Page2, you will need to repeat the same code:
//This is page 2....
RaisedButton(
child: Text('Go to second'),
onPressed: () {
if (user.state = "login") {
Navigator.of(context).push(
MaterialPageRoute(
builder: (context) => SecondPage(),
),
)
}else{
Navigator.of(context).push(
MaterialPageRoute(
builder: (context) => SecondPageAnonymous(),
),
)
}
}
)
....
With PushNamed, you just have to declare it once and you can basically reuse it over and over again.
In your onGenerateRoute:
onGenerateRoute: (settings) {
switch (settings.name) {
case '/':
return MaterialPageRoute(builder: (_) => FirstPage());
case '/second':
if (user.state = "login") {
return MaterialPageRoute(
builder: (_) => SecondPage()
);
}else{
return MaterialPageRoute(
builder: (_) => SecondPageAnonymous()
);
}
default:
return _errorRoute();
}
},
Now in ANY pages in your project, you could do this:
Navigator.of(context).pushNamed('/second')
Without needing to repeat the checking of sign in or even the error handling every time you used it. The obvious benefit is that you can stay consistent throughout the app by preventing duplicate code piece, instead of repeating it again and again.
Now, this however DOES NOT prevent duplicates of routes! There is no different between push and pushNamed in this context!
But since your routes are now named, you can do popUntil('/') easily to go back to the first instance of the route, instead of creating it again or PushReplacementNamed.
The only advantage I can see using Navigate with named routes is to have routes declared inside your MaterialApp, so that developer can only be used assigned routes i.e widgets, pages,
If anyone uses other than that, It will give an error 'onUnknownRoute is called.'
Here is my beginner flutter thoughts:
It makes the code cleaner: Without declaring the routes at the level higher widgets, new screens will appear out of nowhere, in response to anything that happens in the app. It is much easier to understand the navigation skeleton/ structure when you declare the routes together, and even more so at a higher widget, especially for other developers. Of course, this doesn't help with understanding exactly when those routes are actually navigated to, but its a small improvement, and brings us back into the declarative paradigm. The hint provided by the declared routes will help a newer developer understand your navigation flow.
For folks visiting this question in 2022. Flutter actually now recommends not using named routes
Note: Named routes are no longer recommended for most applications. For more information, see Limitations in the navigation overview page.
https://docs.flutter.dev/cookbook/navigation/named-routes
If you use push(), you have to import the file in which SecondRoute is located every time you need to navigate to that screen. This is excessive code duplication for big projects that you need to navigate around the different screens.
If you use pushNamed(), you need to define the routes only once in your MaterialApp. Then, you can navigate to any screen from anywhere without repeating the same thing like you have to with push().
Another big reason to choose PushNamed() over the other one is to be able to build your own navigation system with it. You can decide whether or not routes are available for certain users even before they navigate to the screen.
for understanding why we should use Navigator.pushNamed instead Navigator.push first let's be familiar with Navigator methods. did you ever heart about Navigator.popUntil or Navigator.pushAndRemoveUntil?
we use Navigator.popUntil when we want to pop in the stack to a specific route. if you check the documentation you can find that it's very easy to use these methods with the pushNamed method. also, check all methods in the documentation. when I try to understand routing in flutter this article was very useful for me.
and as a disadvantage, it's very hard to handle parameters in this approach. you should create onGenerateRoute and handle parameters for each route.