How to access Provided (Provider.of()) value inside showModalBottomSheet? - flutter

I have a FloatingActionButton inside a widget tree which has a BlocProvider from flutter_bloc. Something like this:
BlocProvider(
builder: (context) {
SomeBloc someBloc = SomeBloc();
someBloc.dispatch(SomeEvent());
return someBloc;
},
child: Scaffold(
body: ...
floatingActionButton: FloatingActionButton(
onPressed: _openFilterSchedule,
child: Icon(Icons.filter_list),
),
)
);
Which opens a modal bottom sheet:
void _openFilterSchedule() {
showModalBottomSheet<void>(
context: context,
builder: (BuildContext context) {
return TheBottomSheet();
},
);
}
I am trying to access SomeBloc using BlocProvider.of<SomeBloc>(context) inside TheBottomSheet but I get the following error:
BlocProvider.of() called with a context that does not contain a Bloc of type SomeBloc.
I have tried to use the solution described in https://stackoverflow.com/a/56533611/2457045 but only works for BottomSheet and not ModalBottomSheet.
Note: This is not restricted to BlocProvider or flutter_bloc. Any Provider from the provider package has the same behaviour.
How can I access BlocProvider.of<SomeBloc>(context) inside the showModalBottomSheet?
In case it's not possible to do that, how to adapt https://stackoverflow.com/a/56533611/2457045 solution to Modal Bottom Sheet?

InheritedWidgets, and therefore Providers, are scoped to the widget tree. They cannot be accessed outside of that tree.
The thing is, using showDialog and similar functions, the dialog is located in a different widget tree – which may not have access to the desired provider.
It is therefore necessary to add the desired providers in that new widget tree:
void myShowDialog() {
final myModel = Provider.of<MyModel>(context, listen: false);
showDialog(
context: context,
builder: (_) {
return Provider.value(value: myModel, child: SomeDialog());
},
);
}

Provider in showModalBottomSheet (Bottom-Sheet)
void myBottomSheet() {
final myModel = Provider.of<MyModel>(context, listen: false);
showModalBottomShee(
context: context,
builder: (_) {
return ListenableProvider.value(
value: myModel,
child: Text(myModel.txtValue),
);
},
);
}

You need move Provider to top layer(MaterialApp)
According to picture, Dialog widget is under MaterialApp, so this is why you using wrong context

wrap your whole child widget inside the consumer.
void myShowDialog() {
showDialog(
context: context,
builder: Consumer<MyModel>(
builder: (context, value, builder) {
retuen widget();
}
);
}

You should split Scaffold widget and its children, to another StatefulWidget
From single Widget
class MainScreen extends StatelessWidget {
#override
Widget build(BuildContext context) {
return BlocProvider(
builder: (context) {
SomeBloc someBloc = SomeBloc();
someBloc.dispatch(SomeEvent());
return someBloc;
},
child: Scaffold(
body: ...
floatingActionButton: FloatingActionButton(
onPressed: _openFilterSchedule,
child: Icon(Icons.filter_list),
),
)
);
}
}
Splitted into these two widget
class MainScreen extends StatelessWidget {
#override
Widget build(BuildContext context) {
return BlocProvider(
builder: (context) {
SomeBloc someBloc = SomeBloc();
someBloc.dispatch(SomeEvent());
return someBloc;
},
child: Screen(),
);
}
}
and ..
class Screen extends StatelessWidget {
void _openFilterSchedule() {
showModalBottomSheet<void>(
context: context,
builder: (BuildContext context) {
return TheBottomSheet();
},
);
}
#override
Widget build(BuildContext context) {
return Scaffold(
body: ...
floatingActionButton: FloatingActionButton(
onPressed: _openFilterSchedule,
child: Icon(Icons.filter_list),
),
);
}
}

I found a solution, Just return your showModalBottomSheet with a StatefulBuilder and use the context of your modalsheet builder to pass to your provider. a snippet of my code below:
Future<Widget> showModal(int qty, Product product) async {
return await showModalBottomSheet(
isScrollControlled: true,
backgroundColor: Colors.transparent,
context: context,
builder: (BuildContext ctx) {
return StatefulBuilder(builder: (ctx, state) {
return Container(
child: RaisedButton(
onPressed: () {
Product prod = Product(product.id,
product.sku, product.name, qty);
Provider.of<CartProvider>(ctx, listen:
false).addCart(prod);}),);
}
}
);
}

TLDR: Make sure your import statement's casings match your project's folder casings.
I came across one other quirk while debugging this same error. I had several providers that were all working, including in showModalBottomSheets, however one was not working. After combing through the entire widget tree, without finding any discrepancies, I found that I had capitalized the first letter of a folder on one of the import statements of my problem-child notifier. I think this confused the compiler and caused it to throw the Could not find the correct Provider above this widget error.
After ensuring the import statement casing matched the folder name, my provider problems were resolved. Hopefully this will save someone a headache.

Not finding a clear explanation of adding multiple provided values, I thought I'd share here for reference.
await showMobileModals(
isDismissible: false,
context: context,
child: MultiProvider(
providers: [
Provider.value(
value: provided_one,
),
Provider.value(
value: provided_two,
),
Provider.value(
value: provided_three,
),
],
child: Container(),
),
);

Faced the same issue while dealing with showModelBottomSheet, since it happens to work in a different (context)widget tree I had to level up my state to that of the app so that I could access my provider using the context.

Related

Rebuild the text of the outer class with Provider

The code below is a simplification of my project.
I want it make like: When you push the button, text is rebuilt and increase the number.
However, text isn't reloaded. but I detected that the number of count is actualy increased.
Why this text isn't reloaded? Some way to detect changes of other classes?
class CLS extends ChangeNotifier{
int count = 0;
void add(int i){
count += i;
notifyListeners();
}
}
....
class Main extends StatelessWidget{
Widget txt(){
return Consumer<CLS>(
builder: (context, value, _) => Text(value.count.ToString())
);
}
Widget but(int i){
return ElevatedButton(
child: Text("Button"),
onPressed: context.read<CLS>().add(i)
);
}
#override
Widget build(BuildContext context){
return Scaffold(
body: ChangeNotifierProvider(
create: (BuildContext context) => CLS(),
child: Column( children: [
txt(),
but(1)
]));
}
}
You are using same context for providing and accessing your Provider, so it can't find it. You should use new context to access your provided Provider, because it looks for it's ancessor to find provided Provider. And also you should use that new context to read your Provider from your but() method, like so:
Widget but(int i, BuildContext ctx) {
return ElevatedButton(
child: Text("Button"), onPressed: () => ctx.read<CLS>().add(i));
}
#override
Widget build(BuildContext context) {
return Scaffold(
body: ChangeNotifierProvider(
create: (BuildContext context) => CLS(),
child: Builder(builder: (ctx) { // new context(ctx) to find Provider from parent's(ancessor's) context
return Column(
children: [
txt(),
but(1, ctx), // use that new context in your but method for accessing Provider
],
);
}),
),
);
}

Flutter: can't access Provider from showModalBottomSheet

I can't access a provider defined above a Scaffold from showModalBottomSheet in the FloatingActionButton.
I've defined a HomePage like so:
class HomePage extends StatelessWidget {
#override
Widget build(BuildContext context) {
return ChangeNotifierProvider(
create: (_) => MyProvider(),
builder: (context, _) {
return Scaffold(
body: Consumer<MyProvider>(
builder: (context, provider, _) {
return Text(provider.mytext); // this works fine
}
),
floatingActionButton: MyFAB(), // here is the problem
);
}
)
}
}
And this is MyFAB:
class MyFAB extends StatefulWidget {
#override
_MyFABState createState() => _MyFABState();
}
class _MyFABState extends State<MyFAB> {
#override
Widget build(BuildContext context) {
return FloatingActionButton(
...
onPressed: () => show(),
);
}
void show() {
showModalBottomSheet(
...
context: context,
builder: (BuildContext context) {
return Wrap(
children: [
...
FlatButton(
onPressed: Provider.of<MyProvider>(context, listen: false).doSomething(); //Can't do this
Navigator.pop(context);
)
],
);
}
);
}
}
Error: Could not find the correct Provider<MyProvider above this BottomSheet Widget.
Fixed by placing the provider above MaterialApp, as described here.
Bottom sheets are created at the root of the material app. If a prodiver is declared below the material app, a bottom sheet cannot access it because the provider is not an ancestor of the bottom sheet in the widget tree.
The screenshot below shows a widget tree: the whole app is inside Wrapper and the bottom sheet is not created inside Wrapper. It is created as another child of MaterialApp (with a root element Container in this case).
For your case:
// main.dart
class MyApp extends StatelessWidget {
#override
Widget build(BuildContext context) {
return ChangeNotifierProvider(
create: (_) => MyProvider(),
builder: (context, _) {
return MaterialApp(
home: HomePage(),
);
},
);
}
}
// home_page.dart
class HomePage extends StatelessWidget {
#override
Widget build(BuildContext context) {
return Scaffold(
floatingActionButton: MyFAB()
);
}
}
This is caused by passing it the wrong context. Wrap your FAB to a Builder widget and pass it as builder property. This will take a new context and pass it to showModalBottomSheet. Also, you can do onPressed: show, it's more concise.
class HomePage extends StatelessWidget {
#override
Widget build(BuildContext context) {
return ChangeNotifierProvider(
create: (_) => MyProvider(),
builder: (context, _) {
return Scaffold(
body: Consumer<MyProvider>(
builder: (context, provider, _) {
return Text(provider.mytext); // this works fine
}
),
floatingActionButton: MyFAB(context), // here is the problem
);
}
)
}
}
class MyFAB extends StatefulWidget {
#override
_MyFABState createState() => _MyFABState();
}
class _MyFABState extends State<MyFAB> {
#override
Widget build(BuildContext context) {
return FloatingActionButton(
...
onPressed: (context) => show(context),
);
}
void show(ctx) {
showModalBottomSheet(
...
context: ctx,
builder: (BuildContext context) {
return Wrap(
children: [
...
FlatButton(
onPressed: () {
Provider.of<MyProvider>(ctx, listen: false).doSomething(); //Can't do this
Navigator.pop(ctx)
};
)
],
);
}
);
}
}
SOLUTION
HomePage:
class HomePage extends StatelessWidget {
#override
Widget build(BuildContext context) {
return ChangeNotifierProvider(
create: (_) => MyProvider(),
builder: (context, _) {
return Scaffold(
body: Consumer<MyProvider>(
builder: (context, provider, _) {
return Text(provider.mytext); // this works fine
}
),
floatingActionButton: MyFAB(context), // here is the problem
);
}
)
}
}
MyFAB:
class MyFAB extends StatefulWidget {
final BuildContext ctx;
MyFAB(this.ctx)
#override
_MyFABState createState() => _MyFABState();
}
class _MyFABState extends State<MyFAB> {
#override
Widget build(BuildContext context) {
return FloatingActionButton(
...
onPressed: () => show(),
);
}
void show() {
showModalBottomSheet(
...
context: context,
builder: (BuildContext context) {
return Wrap(
children: [
...
FlatButton(
onPressed: Provider.of<MyProvider>(widget.ctx, listen: false).doSomething(); //Can't do this
Navigator.pop(context);
)
],
);
}
);
}
}
In my opinion: showModalBottomSheet builds a bottom sheet with context which comes from Material App
1st image
so when we return any Widget to show in the Bottom sheet it uses that Material app context as we can see in the builder property in the:1st image.
2ng Image: your code
so in your code, when you are writing: Provider.of(context, listen: false).doSomething(); it is using context from the builder: (BuildContext context) which is the context of Material App. we have to change this context in order to use this Provider without having to uplift the position of our Provider above the Material App.
Now if we want to keep using that context to get the benefits of that overlay and automatic detection of suitable themes and still want to use the context of a widget that does have access to our provider:
we can pass the context of the Widget which does have Provider access to the FAB, but we will have to keep passing that context through widgets till we need to use that Provider in our FAB or till we go to a different route: in which case we can start from a new context and provider as Providers are scoped in mature.
so in your HomePage either you can wrap your scaffold inside a Builder or you can create a new widget like this:"
3rd image
so that it will have its own context which does have access to the provider we need inside our FAB as shown below in 4th image:
4th image
and then in the builder property of showModalBottomSheet change the name of the parameter in an anonymous function so that it won't be confused with the MAterial App context and context we will be passing in (Builder context or IdeaScreen context in my case image 4th)
5th image
I am creating a new widget but you do not have need to do so you can directly write your Fab code inside the anonymous function:
and can use context(not newContext which is related to Material App context) while calling the Provider as you are already doing.
But I will show in my case What I am doing in my AddTask Widget in case anyone's use case is similar to mine:
6th image
expect a context, which does have a provider access, I my case its context of IdeaScreen.
and then use it just like this:
7th image

Can't access bloc in showModal context in Flutter

I wrap my scafflod with dart BlocProvider<ABloc,AState>
but when I use showDialog func and showDialog I want to access ABloc from context or BlocBuilder
doesn't contain bloc and throw error
is there a way to access bloc in this situatuion
(in my dialog I show text filed to get user name so I want to access bloc)
class MyHomePageState extends StateLessWidget {
#override
Widget build(BuildContext context) {
return BlocProvider(
lazy: false,
create: (context) => EditColorBloc(context.bloc<RetrieveColorBloc>()),
child: MainScafold());
}
}
class MainScafold extends StatelessWidget {
#override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text("sina"),
),
floatingActionButton:FloatingActionButton(
child: Icon(Icons.add),
onPressed: () => showDialog(
context: context,
builder: (ctx) {
// show dialog and use
// context o BlocBuilder to access
// EditColorBloc
// throw error BlocProvider.of() called with a context that does
// not contain a Cubit of type EditColorBloc
}
)
}
}
UPDATE v6.1.0
(Credit to #Sebastian Dennis)
context.bloc was deprecated in favor of provider's context.read, context.watch, and context.select. In the below code, just change context.bloc<TestBloc> to context.read<TestBloc>
You shouldn't use a BlocBuilder to wrap the Dialog. Reason being is that you're (most likely) not rebuilding the whole Dialog whenever a change occurs. Another, more important, reason is that the context in which your Dialog is being built doesn't contain your bloc, so you need to inject your bloc instance into the Dialog's context.
Instead, use BlocProvider.value and set the value parameter to BlocProvider.of(context) (aka context.bloc() ), and make sure the context is not the showDailog 's method context, but the context in the original build method.
Here is a working example. You just have to change the TestBloc or TestState etc to your own Bloc instances:
#override
Widget build(BuildContext context) {
return BlocProvider<TestBloc>(
create: (context) => TestBloc(InitTestState()),
child: Scaffold(
body: BlocBuilder<TestBloc, TestState>(
builder: (context, state) => Center(
child: FlatButton(
child: Text("Show Dialog"),
onPressed: () => showDialog(
context: context,
// Relavent code change here
builder: (ctx) => BlocProvider<TestBloc>.value(
value: context.bloc<TestBloc>(),
child: Dialog(
child: FlatButton(
child: Text("Notify bloc"),
onPressed: () => context
.bloc<TestBloc>()
.add(TestEvent()),
),
),
)),
),
),
)));

BlocProvider.of() called with a context that does not contain a Bloc - even that it does

First of, I do know how BLoC suppose to work, the idea behind it and I know the difference between BlocProvider() and BlocProvider.value() constructors.
For simplicity, my application has 3 pages with a widget tree like this:
App() => LoginPage() => HomePage() => UserTokensPage()
I want my LoginPage() to have access to UserBloc because i need to log in user etc. To do that, I wrap LoginPage() builder at App() widget like this:
void main() => runApp(App());
class App extends StatelessWidget {
#override
Widget build(BuildContext context) {
return MaterialApp(
title: 'My App',
home: BlocProvider<UserBloc>(
create: (context) => UserBloc(UserRepository()),
child: LoginPage(),
),
);
}
}
That obviously works just fine. Then, if User logs in successfully, he is navigated to HomePage. Now, I need to have access to two different blocs at my HomePage so I use MultiBlocProvider to pass existing UserBloc further and create a brand new one named DataBloc. I do it like this:
#override
Widget build(BuildContext context) {
return BlocListener<UserBloc, UserState>(
listener: (context, state) {
if (state is UserAuthenticated) {
Navigator.of(context).push(
MaterialPageRoute<HomePage>(
builder: (_) => MultiBlocProvider(
providers: [
BlocProvider.value(
value: BlocProvider.of<UserBloc>(context),
),
BlocProvider<DataBloc>(
create: (_) => DataBloc(DataRepository()),
),
],
child: HomePage(),
),
),
);
}
},
[...]
This also works. Problem happens when from HomePage user navigates to UserTokensPage. At UserTokensPage I need my already existing UserBloc that I want to pass with BlocProvider.value() constructor. I do it like this:
class _HomePageState extends State<HomePage> {
#override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
centerTitle: false,
title: Text('My App'),
actions: <Widget>[
CustomPopupButton(),
],
),
[...]
class CustomPopupButton extends StatelessWidget {
const CustomPopupButton({
Key key,
}) : super(key: key);
#override
Widget build(BuildContext context) {
return PopupMenuButton<String>(
icon: Icon(Icons.more_horiz),
onSelected: (String choice) {
switch (choice) {
case PopupState.myTokens:
{
Navigator.of(context).push(
MaterialPageRoute<UserTokensPage>(
builder: (_) => BlocProvider.value(
value: BlocProvider.of<UserBloc>(context),
child: UserTokensPage(),
),
),
);
}
break;
case PopupState.signOut:
{
BlocProvider.of<UserBloc>(context).add(SignOut());
Navigator.of(context).pop();
}
}
},
[...]
When I press button to navigate to MyTokensPage i get error with message:
════════ Exception caught by widgets library ═══════════════════════════════════════════════════════
The following assertion was thrown building Builder(dirty):
BlocProvider.of() called with a context that does not contain a Bloc of type UserBloc.
No ancestor could be found starting from the context that was passed to BlocProvider.of<UserBloc>().
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<UserBloc>(create: (context) => UserBloc())
Bad: BlocProvider(create: (context) => UserBloc()).
The context used was: CustomPopupButton
What am I doing wrong? Is it because i have extracted PopupMenuButton widget that somehow loses blocs? I don't understand what I can be doing wrong.
You can just wrap the Blocs you need to access through out the app by wrapping it at the entry point of the app like this
runApp(
MultiBlocProvider(
providers: [
BlocProvider<UserBloc>(
create: (context) =>
UserBloc(UserRepository()),
),
],
child: App()
)
);
}
and you can access this bloc at anywhere of your app by
BlocProvider.of<UserBloc>(context).add(event of user bloc());
EDIT 10/03/2022
Since this thread became very popular I feel I need to add some comments.
This is valid solution if your goal is to use blocs that are not provided above your MaterialApp widget, but instead being declared somewhere down the widget tree by wrapping your widget (eg. some page) with BlocProvider making it possible for that widget to access the bloc.
It is easier to avoid problems by declaring all your blocs in MultiBlocProvider somewhere up the widget tree (like I said before), but this topic was not created with that in mind. Feel free to upvote and use this aproach described in Amesh Fernando response but do that knowing the difference.
I fixed it. Inside App widget i create LoginPage with
home: BlocProvider<UserBloc>(
create: (context) => UserBloc(UserRepository()),
child: LoginPage(),
At LoginPage I simply wrap BlocBuilders one into another
Widget build(BuildContext context) {
return BlocListener<UserBloc, UserState>(
listener: (context, state) {
if (state is UserAuthenticated) {
Navigator.of(context).push(
MaterialPageRoute<HomePage>(
builder: (_) => BlocProvider.value(
value: BlocProvider.of<UserBloc>(context),
child: BlocProvider<NewRelicBloc>(
create: (_) => NewRelicBloc(NewRelicRepository()),
child: HomePage(),
),
),
),
);
}
},
[...]
PopupMenuButton navigates User to TokenPage with
Navigator.of(context).push(
MaterialPageRoute<UserTokensPage>(
builder: (_) => BlocProvider.value(
value: BlocProvider.of<UserBloc>(context),
child: UserTokensPage(),
),
),
);
And that solved all my problems.
Solution
Method A: Access UserBloc provider instance directly without passing it
I prefer this solution since it requires less code.
A.1 Wrap CustomPopupButton instance with provider Consumer so it rebuilds itself whenever UserBloc notifies listeners of value changes.
Change this:
actions: <Widget>[
CustomPopupButton(),
],
To:
actions: <Widget>[
Consumer<UserBloc>(builder: (BuildContext context, UserBloc userBloc, Widget child) {
return CustomPopupButton(),
});
],
A.2 Change Provider instance invocation inside the stateless widget to disable listening to value changes -- "listening" and resulting "rebuilds" are already done by Consumer.
A.2.1 Change this:
value: BlocProvider.of<UserBloc>(context),
To:
value: BlocProvider.of<UserBloc>(context, listen: false),
A.2.2 And change this:
BlocProvider.of<UserBloc>(context).add(SignOut());
To:
BlocProvider.of<UserBloc>(context, listen: false).add(SignOut());
Method B: pass UserBloc provider instance
Same thing as Method A, but:
In A.1 you'd pass userBloc like this: return CustomPopupButton(userBloc: userBloc),.
You'd declare final UserBloc userBloc; member property inside CustomPopupButton.
In A.2 you'd do this: userBloc.add(SignOut()); instead of BlocProvider.of<UserBloc>(context, listen: false).add(SignOut());
Explanation
flutter_bloc is using Provider, to be aware what's going on it's better understand Provider. Please refer to my answer here to understand my answer to your question, and to understand Provider and listen flag better.
Change name of context in builder whether in bottomSheet or materialPageRoute.
So that bloc can access parent context through context
unless it's going to take context from builder (bottom sheet). This can lead
to an error which you can't reach the instance of bloc .
showModalBottomSheet(
context: context,
builder: (context2) { ===> change here to context2
BlocProvider.value(
value: BlocProvider.of<BlocA>(context),
child: widgetA(),
),
}
You need to either decompose your widget into two widgets (which I recommend for testability reasons) or use a Builder widget to get a child context.
class MyHomePage extends StatelessWidget { #override Widget build(BuildContext context) { return BlocProvider( create: (_) => TestCubit(), child: MyHomeView(), ); } } class MyHomeView extends StatelessWidget { #override Widget build(BuildContext context) { return Scaffold( body: Center( child: RaisedButton(onPressed: () => BlocProvider.of<TestCubit>(context)...) ), ); } }
source: solved by Felix Angelov, https://github.com/felangel/bloc/issues/2064
you don't have to use BlocProvider.value() to navigate to another screen, you can just wrap MaterialApp into BlocProvider as a child of it

Flutter – MultiBlocProvider doesn't instantiate BLoC

So I have this code
#override
Widget build(BuildContext context) {
return MultiBlocProvider(
providers: [
BlocProvider<ProfilePageBloc>(
create: (context) => ProfilePageBloc(userRepository: UserRepository.instance),
),
BlocProvider<HomeBloc>(
create: (context) => HomeBloc(),
),
],
child: BlocBuilder<HomeBloc, HomeState>(
builder: (context, state) {
return Scaffold(
body: BlocBuilder<HomeBloc, HomeState>(
builder: (context, state) {
if (state is StateThatDoesNotMatter) {
final HomeBloc homeBloc = BlocProvider.of<HomeBloc>(context);
final ProfilePageBloc profileBloc = BlocProvider.of<ProfilePageBloc>(context);
print("homeBloc: ${homeBloc.toString()}");
print("profileBloc: ${profileBloc.toString()}");
return Center(
child: Text("Doesn't really matter"),
);
},
),
);
},
),
);
}
and the output I get is
homeBloc: Instance of 'HomeBloc'
profileBloc: null
I expected profileBloc to be instantiated like homeBloc was.
This makes me completely unable to continue development. I have no idea why it's like that.
The best thing is, it worked a few times, but I wasn't able to reproduce this behavior.
Any help would be greatly appreciated
It turned out that 'ProfilePageBloc` wasn't instantiated because an assertion was failing in its constructor.
Code I've provided in the quesiton wasn't enough to solve it.
More info here