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

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

Related

When do we initialise a provider in flutter?

I just arrived on a flutter project for a web app, and all developers have a problem using flutter provider for state management.
What is the problem
When you arrive on a screen, the variables of the corresponding provider are initialised by calling a function of the provider. This function calls an api, and sets the variables in the provider.
Problem : This function is called in the build section of the widget. Each time the window is resized, the widget is rebuilt, and the function is called again.
What we want
We want to call an api when the page is first displayed, set variables with the result, and not call the api again when the widget is rebuilt.
What solution ?
We use a push from the first screen to go to the second one. We can call the function of the provider at this moment, to initialise the provider just before the second screen.
ā†’ But a refresh on the second page will clear the provider variables, and the function to initialise them will not be called again.
We call the function to initialise the provider in the constructor of the second screen. Is it a good pattern ?
Thank you for your help in my new experience with flutter :)
I think you're mixing a couple different issues here:
How do you correctly initialize a provider
How do you call a method on initialization (only once)
For the first question:
In your main.dart file you want to do something like this:
#override
Widget build(BuildContext context) {
return MultiProvider(
providers: [
ChangeNotifierProvider(create: (context) => SomeProvider()),
ChangeNotifierProvider(create: (context) => AnotherProvider()),
],
child: YourRootWidget();
);
}
Then in a widget (that probably represents a "screen" in your app), you need to do something like this to consume state changes from that provider:
#override
Widget build(BuildContext context) {
return Container(
child: Consumer<SomeProvider>(
builder: (context, provider, child) {
return Text(provider.someState);
}
),
)
}
And you need to do something like this to get access to the provider to mutate state:
#override
Widget build(BuildContext context) {
SomeProvider someProvider = Provider.of<SomeProvider>(context, listen: false);
return Container(
child: TextButton(
child: Text('Tap me'),
onPressed: () async {
await someProvider.mutateSomeState();
}
),
)
}
Regarding the second question... You can (I think) just use the initState() method on a widget to make the call only 1 time. So...
#override
void initState() {
super.initState();
AnotherProvider anotherProvider = Provider.of<AnotherProvider>(context, listen: false);
Future.microtask(() {
anotherProvider.doSomethingElse();
});
}
If I'm off on any of that, I'm sorry. That mirrors my implementation and works fine/well.
A caveat here is that I think RiverPod is likely the place you really want to go (it's maybe easier to work with and has additional features that are helpful, etc.) but I've not migrated to RiverPod yet and do not have that figured out all the way.
Anyway... Good luck!
As far as I understood, you can wrap your application with MultiProvider and call the API before going to the second screen.

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

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, '')

Flutter: `Provider.of(context)` cannot be found when it is a `FutureProvider()`

I have a provider in my widget tree and I call Provider.of<List<...>>(context) to find it. If it is a provider of any type it works fine, however, as soon as change the provider from a Provider() (for example) to a FutureProvider() it doesn't work.
I haven't changed any widgets in the tree and haven't changed their position in the navigator. Provider.of() works fine but once I set it to be a FutureProvider() then it doesn't work.
Edit: My code looks something like this:
inside widget build:
return FutureProvider(
initialData: [],
create: (_) =>
DatabaseService(uid: _auth.getUser()!.uid).getJournalEntries(),
catchError: (context, error) {
print(error.toString());
},
...
}
Then one of the children is another widget and this is its build function:
List<JournalEntryData> entries =
Provider.of<List<JournalEntryData>>(context);
return ElevatedButton(
onPressed: () {
print(entries);
},
child: Text('print provider data'));
}
I get the following error:
Error: Could not find the correct Provider<List> above this TestButton Widget
This happens because you used a BuildContext that does not include the provider
of your choice. There are a few common scenarios:
You added a new provider in your main.dart and performed a hot-reload.
To fix, perform a hot-restart.
The provider you are trying to read is in a different route.
Providers are "scoped". So if you insert of provider inside a route, then
other routes will not be able to access that provider.
You used a BuildContext that is an ancestor of the provider you are trying to read.
Make sure that TestButton is under your MultiProvider/Provider<List>.
This usually happens when you are creating a provider and trying to read it immediately.
For example, instead of:
Widget build(BuildContext context) {
return Provider<Example>(
create: (_) => Example(),
// Will throw a ProviderNotFoundError, because `context` is associated
// to the widget that is the parent of `Provider<Example>`
child: Text(context.watch<Example>()),
),
}
consider using builder like so:
Widget build(BuildContext context) {
return Provider<Example>(
create: (_) => Example(),
// we use `builder` to obtain a new `BuildContext` that has access to the provider
builder: (context) {
// No longer throws
return Text(context.watch<Example>()),
}
),
}
The problem is that there is a provider in the widget tree:
Replacing the FutureProvider() with any other type of provider makes it work, but I need a FutureProvider().
I didn't define the type of the FutureProvider() so it was dynamic but in my Provider.of() call I specified a type which caused the issue. Specifiying the type of the FutureProvider() should solve the issue.

Flutter Bloc change state from different widget

On my apps home page, I have a list view that rebuilds whenever the user clicks on a new page.
I have implemented the block pattern using the flutter_bloc plugin but I don't know how to change the state from another widget.
Two things you will have to keep in mind for changing state with flutter bloc:
Dependency injection (DI) of a bloc.
Interaction with your bloc instance.
Dependency injection of a bloc
Case 1. You need to provide bloc to widget subtree within one route.
To provide a single instance of a bloc to multiple widgets within a subtree you use BlocProvider widget. It creates bloc instance, automatically disposes of it when needed and provides bloc to its children via BlocProvider.of<T>(context), where T is the name of your bloc:
BlocProvider(
create: (BuildContext context) => BlocA(),
child: ChildA(),
);
Keep in mind, that by deafult it is created with property lazy: true, means that create: (BuildContext context) => BlocA(), will be executed after invoke of BlocProvider.of<T>(context). If you dont want it - set lazy: false in advance.
Case 2. You need to provide bloc to widgets from another route (to another context).
BlocProvider automatically disposes of a bloc instance with context of new route instantiated, but that will not happen if you use BlocProvider.value:
BlocProvider.value(
value: BlocProvider.of<BlocA>(context),
child: ScreenA(),
);
Important note: BlocProvider.value should only be used for providing existing instances to new subtree, do not create Bloc instance with it
Interaction with your bloc instance
Starting from bloc v6.1.0 context.bloc and context.repository are deprecated in favor of context.read and context.watch.
context.select allows to update UI based on a part of a bloc state:
final name = context.select((UserBloc bloc) => bloc.state.user.name);
context.read access a bloc with a BuildContext and do not result rebuilds.
context.watch gets a value from the nearest ancestor provider of its type and subscribes to the provider.
To access the bloc's state
If you need a widget rebuilding due bloc value changing use context.watch or BlocBuilder:
// Using context.watch at the root of the build method will result in the entire widget being rebuilt when the bloc state changes.
#override
Widget build(BuildContext context) {
final state = context.watch<MyBloc>().state;
return Text('$state');
}
or with BlocBuilder:
// If the entire widget does not need to be rebuilt, either use BlocBuilder to wrap the parts that should rebuild
#override
Widget build(BuildContext context) {
return BlocBuilder<MyBloc, MyState>(
builder: (context, state) => Text('$state'),
);
}
To access the bloc so that an event can be added
Use context.read:
#override
Widget build(BuildContext context) {
return ElevatedButton(
onPressed: () => context.read<MyBloc>().add(MyEvent()),
...
)
}
You can place the BlocProvider to a common ansestor like as a parent of your MaterialApp widget, or you can pass the bloc to your new page like this:
BlocProvider.value(
value: BlocProvider.of<BlocA>(context),
child: ScreenB(),
);

Flutter Bloc , Bloc state , navigate?

what Iā€™m facing now is after I implemented bloc following one of the tutorials, I'm stuck now in place where after I'm getting the response and the state is changed, I want to navigate to another widget
#override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text(APP_TITLE),
),
body: buildBody(context));
}
}
BlocProvider<SignInBloc> buildBody(BuildContext context) {
return BlocProvider(
create: (_) => sl<SignInBloc>(),
child: Center(
child: Padding(
padding: const EdgeInsets.all(16),
child: Column(
children: <Widget>[
BlocBuilder<SignInBloc, SignInState>(
builder: (context, state) {
if(state is Empty)
return MessageDisplay(message: 'Sign In please.',);
else if(state is Loaded)
return HomePage();
else
return MessageDisplay(message: 'Sign In please.',);
}
),
SignInControls(),
],
),
),
),
);
}
in state of loaded I want to navigate to another widget.
so how to achieve that, and what is the best way for it?
You can't use the navigator or change the state while the widget is being built (your case).
There're two ways
1. The old fashioned way
WidgetsBinding.instance.addPostFrameCallback((_){
// Your code goes here
});
2. Since you already implemented the BLOC library you have a more elegant way to achieve this by using BlocListener. you can learn more about it in the documentation
Hope i helped!
Navigation can be used like Inherited widgets:
Navigator nav = Navigator.of(this.context);
then you can use somthing like:
nav.push(MaterialPageRoute(builder: (context) => YourSecondPage()))
in flutter, you can't just move to some page directly. you should use a route.
I think the cleanest way to use named routes. this is an example:
// here you put a class of names to use later in all of your project.
class RouteNames{
static String homepage = "/";
static String otherPage= "/otherpage";
}
// in your main file , MyApp class
var routes = {
RouteNames.homepage: (context)=> new MyHomePage(),
RouteNames.otherPage: (context)=> new MyOtherPage()
};
// then use routes variable in your MaterialApp constructor
// and later on in your project you can use this syntax:
Navigator.of(context).pushNamed(RouteNames.otherPage);
I think this way is clean and it's centralized, it's good if you want to send arguments to routes.
To learn more about navigation: navigation official documentation is pretty good
A note about the Bloc builder & listener:
Since BlocBuilder is going to be called lots of times. it should only contain widgets and widgets only. if you put navigation code inside it, this code would be called multiple times.
As Ayham Orfali said You definitely should use BlocListener for that. Inside it you can listen to changes in state. here is an example
// some code
children: <Widget>[
BlocListener(
bloc: BlocProvider.of<SignInBloc>(context),
listener: (context, state) {
if(state is Loaded){
Navigator.of(context).pushNamed("some other page");
}
// else do nothing!
},
child:// just bloc builder which contains widgets only. ,
SignInControls(),
]
// some other code