BlocConsumer accessing multiple providers - flutter

I have this app where my widget is wrapped with MultiBlocProvider where I send down 2 providers:
MultiBlocProvider(
providers: [
BlocProvider<AppCubit>(
create: (BuildContext context) => AppCubit(),
),
BlocProvider<ProfileCubit>(
create: (BuildContext context) => ProfileCubit(),
),
],
child: HomePage(),
On the HomePage widget I access the first one using a BlocConsumer but I don't know how to grab all of them. Do I need to keep nesting BlocConsumer into the builder section in order to access my providers? What is the recommended way of accessing the x number of providers I send down to my widgets?
class HomePage extends StatefulWidget {
#override
Widget build(BuildContext context) {
return BlocConsumer<AppCubit, AppState>(
...
builder: (context, state) {
return Scaffold(
backgroundColor: Theme.of(context).primaryColorLight,
body: null,
);
},
...
);
}
}

The MultiBlocProvider is adding your Blocs to the context down the BuildTree to the children of MultiBlocProvider, hence to the HomePage().
BlocConsumer is analog to using a BlocBuilder(for Rebuilding UI after State Change) and a BlocListener(for other reactions like navigation after State Change).
You can assign your Blocs in the initState() like following:
#override
void initState() {
super.initState();
appCubit = BlocProvider.of<AppCubit>(context);
profileCubit = BlocProvider.of<ProfileCubit>(context);
appCubit.add(SomeFetchEvent());
profileCubit.add(SomeFetchEvent());
}
NOTE: in the BlocConsumer/BlocBuilder you want to show UI regarding the current state. Therefore you must decide in during which State you want to nest the next BlocConsumer/BlocBuilder. For example:
BlocConsumer<AppCubit, AppState>(
...
builder: (context, state) {
if (state == *someState*){
// Nest next Consumer
BlocConsumer<ProfileCubit, AppState>(
...
builder: (context, state) {
if(state == *someState*){ return ...}
},
...
);
}
},
...
);
You might see, that it is not really useful to do that. If you don't need to change your UI if a State changes in AppCubit it would be useful to consider putting it in a BlocListener and put Profile Cubit in a BlocConsumer/BlocBuilder. For example:
#override
Widget build(BuildContext context) {
return Scaffold(
backgroundColor: Theme.of(context).primaryColorLight,
body: BlocListener<AppCubit, AppState>(
listener: (context, state) {
// do some stuff here, like Navigating, Changing Variable at specific
// state
},
child: BlocBuilder<ProfileCubit, ProfileState>(
builder: (context, state){
// Change your UI according to the current state
if(state == *someState*){
return *someWidget*
}
}
)
);
}
You will find more details here:
https://bloclibrary.dev/#/flutterbloccoreconcepts?id=multiblocprovider

Related

How to use nested Consumers in Flutter?

I need to access two different view model in one page in Flutter.
How to use nested Consumers in Flutter, Will they effect each other?
How can I use it in this code for example?
class CounterDisplay extends StatelessWidget {
#override
Widget build(BuildContext context) {
return Consumer<CounterModel>(
builder: (context, counterModel, child) {
return Text('${counterModel.count}');
},
);
}
}
you can use Consumer2<> to access two different providers like this :
class CounterDisplay extends StatelessWidget {
#override
Widget build(BuildContext context) {
return Consumer2<CounterModel, SecondModel>(
builder: (context, counterModel, secondModel, child) {
return Text('${counterModel.count}');
},
);
}
}
With this, your Text() widget will be rebuilt each time one the providers value is changed with notifyListener().
If your Text() widget doesn't need to be rebuilt with one of your providers, you can simply use Provider.of<MySecondProvider>(context, listen: false);.
Here for example:
class CounterDisplay extends StatelessWidget {
#override
Widget build(BuildContext context) {
return Consumer<CounterModel>(
builder: (context, counterModel, child) {
MyThemeProvider myThemeProvider = Provider.of<MyThemeProvider>(context, listen: false);
return Text('${counterModel.count}', color: myThemeProvider.isDark ? Colors.white : Colors.dark);
},
);
}
}
I hope this helps!
You can absolutely nest consumers, but you need to understand if you really want them to nest.
Case 1: Nesting consumers:
class CounterDisplay extends StatelessWidget {
#override
Widget build(BuildContext context) {
return Consumer<CounterModel>(
builder: (context, counterModel, child) {
return Consumer<SomeAnotherModel>(
builder: (anothercontext,anotherCounterModel, anotherChild) {
return Text('${counterModel.count}');
});
},
);
}
}
Please note that if consumer for CounterModel is rebuilt, everything will be rebuilt. If consumer for SomeAnotherModel is rebuild, only the part inside its builder would be rebuilt.
Instead of using consumers this way, I'd recommend them to be used as in case 2 below.
Case 2: Use consumers on the required components:
class CounterDisplay extends StatelessWidget {
#override
Widget build(BuildContext context) {
return Column(
children:[
Consumer<CounterModel>(
builder: (context, counterModel, child) { return...}),
Consumer<CounterModel>(
builder: (anotherContext, anotherModel, anotherChild) { return...}),
]
);
}
}

How to use multiple Blocs for different UIs?

I have a Landing UI that doesn't have any Blocs, a Register UI with it's Bloc, a Verification UI with it's Bloc, and a Home UI with it's Bloc.
In each one I defined the BlocProvider.of.
In the main I defined at the Home of Material App a Multiple Bloc Provider with each has it's child and the main child of the provider is the landing Page like this :
home: MultiBlocProvider(
providers: [
BlocProvider<UserBloc>(
create: (context) => UserBloc(UsRepoImp()),
child: RegisterUi(),
),
BlocProvider<VerificationBloc>(
create: (context) => VerificationBloc(VerRepoImp()),
child: VerificationUi(),
),
BlocProvider<HomeBloc>(
create: (context) => HomeBloc(HomeRepoImp()),
child: HomeUi(),
),
],
child: LandingUi(),
),
and one more thing the Verification UI is returned from a Register Bloc state like so :
BlocBuilder<UserBloc, UserState>(
builder: (context, state) {
if (state is UserLoading) {
return CircularProgressIndicator(
valueColor:
AlwaysStoppedAnimation<Color>(Color(0xff7b68ee)),
);
} else if (state is UserRegistered) {
return VerifyAccount();
} else if (state is UserError) {
return Text('Error');
}
return SizedBox(
height: 10.0,
);
},
),
But when I run I have an error that the Bloc shouldn't have an ancestor.
How am I supposed to make these Blocs to communicate with UI changings correctly?
I think you are using MultiBlocProvider in a wrong way. you should not provide child there, instead only provide the argument of the create function there, and then in your widget tree below this MultiBlocProvider you can use BlocBuilder to listen to any of the provided blocs above in the tree, and if you need to listen to multiple blocs in the same widget, you need to nest BlocBuilders.
example:
#override
Widget build(BuildContext context) => MultiBlocProvider(
providers: [
BlocProvider<RecorderBloc>(
create: (context) => myFirstBloc(),
),
BlocProvider<PermissionBloc>(
create: (context) => mySecondBloc(),
)
],
child:myChild()
);
Then inside my_child.dart :
#override
Widget build(BuildContext context) {
return BlocBuilder<MyFirstBloc, MyFirstBlocState>(
builder: (context, myFirstBlocState) =>
BlocBuilder<MySecondBloc, MySecondBlocState>(
builder: (context, secondBlocState) {
//return widget based on the states of both blocs...
},
),
);
}

Offstage Navigators rebuild

I have implemented the multiple Offstage Navigators for the bottomNavigationBar, further details can be seen here. The problem is that when using this method, each time we select a bottom navigation item, the FutureBuilder runs the future method and rebuilds the entire widget, each Offstage widget and all their children are also rebuilt.
For each Offstage widget, I'm loading data via html request and that means each time I switch a tab, 5 requests will be made.
This is my main Scaffold which holds the bottomNavigationBar.
#override
Widget build(BuildContext context) {
TabProvider tabProvider = Provider.of<TabProvider>(context);
return FutureBuilder(
future: initProvider(),
builder: (context, snapshot) {
if (snapshot.connectionState == ConnectionState.done) {
return WillPopScope(
onWillPop: _onWillPop,
child: Scaffold(
appBar: AppBar(
title: Text(tabName[tabProvider.currentTab],
),
body: Stack(children: <Widget>[
_buildOffstageNavigator(TabItem.feed, tabProvider.currentTab),
_buildOffstageNavigator(TabItem.explore, tabProvider.currentTab),
_buildOffstageNavigator(TabItem.guide, tabProvider.currentTab),
_buildOffstageNavigator(TabItem.map, tabProvider.currentTab),
_buildOffstageNavigator(TabItem.profile, tabProvider.currentTab),
]),
bottomNavigationBar: BottomNavigation(
currentTab: tabProvider.currentTab,
onSelectTab: tabProvider.selectTab,
),
),
);
} else {
return Text('Loading');
}
},
);
}
The FutureBuilder will initialize the values in my provider so each tab can access the cached data.
The _buildOffstageNavigator will return the below
return Offstage(
offstage: currentTab != tabItem,
child: TabNavigator(
navigatorKey: navigatorKeys[tabItem],
tabItem: tabItem,
),
);
Below is the Widget which is built inside the Scaffold body and hence inside the Offstage Navigator from above.
#override
Widget build(BuildContext context) {
TabProvider tabProvider = Provider.of<TabProvider>(context);
States stateData = tabProvider.exploreStateCache;
return Container(
child: ListView(
children: <Widget>[
Text(stateData.stateName),
Text(stateData.stateDescription),
],
),
);
}
I have followed this articles advice for using futures with the provider but something else is missing
Instead of creating futures in a build method, which as you have noticed, may be called several times, create them in a place that is invoked only once. For example, a StatefulWidget's initState:
class Foo extends StatefulWidget {
#override
_FooState createState() => _FooState();
}
class _FooState extends State<Foo> {
Future<MyData> _dataFuture;
#override
void initState() {
super.initState();
_dataFuture = getData();
}
#override
Widget build(BuildContext context) => FutureBuilder<MyData>(
future: _dataFuture,
builder: (context, snapshot) => ...,
);
}
A second thing you can improve is reduce the scope of what gets rebuild when the provider provides a new value for TabProvider. The context that you call Provider.of<Data>(context) gets rebuild when there's a new value for Data. That is done most conveniently with the various other widgets offered by the provider package, like Consumer and Selector.
So remove the Provider<TabProvder>.of(context) calls and use Consumers and Selectors. For example, to only rebuild the title when a tab is switched:
AppBar(
title: Selector<TabProvider, String>(
selector: (context, tabProvider) => tabName[tabProvider.currentTab],
builder: (context, title) => Text(title),
),
)
Selector only rebuilds the Text(title) widget, when the result of its selector callback is different from the previous value. Similarly for _buildOffstageNavigator:
Widget _buildOffstageNavigator(BuildContext context, TabItem tabItem) {
return Selector<TabProvider, bool>(
selector: (context, tabProvider) => tabProvider.currentTab != tabItem,
builder: (context, isCurrent) => Offstage(
offstage: isCurrent,
child: Selector<TabProvider, Key>(
selector: (context, tabProvider) => tabProvider.navigatorKeys[tabItem],
builder: (context, tabKey) => TabNavigator(
navigatorKey: tabKey,
tabItem: tabItem,
),
),
);
}
(Beware: All code is untested and contains typos)

How can I stop my change notifier provider from rebuilding my parent material app when I am rendering my child material app?

I have a app class that returns a MaterialApp() which has it's home set to TheSplashPage(). This app listens to the preferences notifier if any preferences are changed.
Then in TheSplashPage() I wait for some conditionals to be true and if they are I show them my nested material app.
Side Note: I use a material app here because it seems more logical since it has routes that the parent material app shouldn't have. And also once the user is unauthenticated or gets disconnected I want the entire nested app to shut down and show another page. This works great!
But my problem is the following. Both apps listen to ThePreferencesProvider() so when the theme changes they both get notified and rebuild. But this is a problem because whenever the parent material app rebuilds, it returns the splash page. So now I am back on TheSplashPage() whenever I change a setting on TheSettingsPage().
So my question is how can I stop my application from going back to the TheSplashPage() whenever I change a setting?
Main.dart
void main() {
runApp(App());
}
class App extends StatelessWidget {
#override
Widget build(BuildContext context) {
SystemChrome.setEnabledSystemUIOverlays([]);
return MultiProvider(
providers: [
ChangeNotifierProvider<PreferencesProvider>(create: (_) => PreferencesProvider()),
ChangeNotifierProvider<ConnectionProvider>(
create: (_) => ConnectionProvider(),
),
ChangeNotifierProvider<AuthenticationProvider>(create: (_) => AuthenticationProvider()),
],
child: Consumer<PreferencesProvider>(builder: (context, preferences, _) {
return MaterialApp(
home: TheSplashPage(),
theme: preferences.isDarkMode ? DarkTheme.themeData : LightTheme.themeData,
debugShowCheckedModeBanner: false,
);
}),
);
}
}
TheSplashPage.dart
class TheSplashPage extends StatelessWidget {
static const int fakeDelayInSeconds = 2;
#override
Widget build(BuildContext context) {
return FutureBuilder(
future: Future.delayed(new Duration(seconds: fakeDelayInSeconds)),
builder: (context, delaySnapshot) {
return Consumer<ConnectionProvider>(
builder: (BuildContext context, ConnectionProvider connectionProvider, _) {
if (delaySnapshot.connectionState != ConnectionState.done ||
connectionProvider.state == ConnectionStatus.uninitialized) return _buildTheSplashPage(context);
if (connectionProvider.state == ConnectionStatus.none) return TheDisconnectedPage();
return Consumer<AuthenticationProvider>(
builder: (BuildContext context, AuthenticationProvider authenticationProvider, _) {
switch (authenticationProvider.status) {
case AuthenticationStatus.unauthenticated:
return TheRegisterPage();
case AuthenticationStatus.authenticating:
return TheLoadingPage();
case AuthenticationStatus.authenticated:
return MultiProvider(
providers: [
Provider<DatabaseProvider>(create: (_) => DatabaseProvider()),
],
child: Consumer<PreferencesProvider>(
builder: (context, preferences, _) => MaterialApp(
home: TheGroupManagementPage(),
routes: <String, WidgetBuilder>{
TheGroupManagementPage.routeName: (BuildContext context) => TheGroupManagementPage(),
TheGroupCreationPage.routeName: (BuildContext context) => TheGroupCreationPage(),
TheGroupPage.routeName: (BuildContext context) => TheGroupPage(),
TheSettingsPage.routeName: (BuildContext context) => TheSettingsPage(),
TheProfilePage.routeName: (BuildContext context) => TheProfilePage(),
TheContactsPage.routeName: (BuildContext context) => TheContactsPage(),
},
theme: preferences.isDarkMode ? DarkTheme.themeData : LightTheme.themeData,
debugShowCheckedModeBanner: false,
)),
);
}
});
});
});
}
TheSettingsPage.dart
Switch(
value: preferences.isDarkMode,
onChanged: (isDarkmode) => preferences.isDarkMode = isDarkmode,
),
You fell for the XY problem
The real problem here is not "my widget rebuilds too often", but "when my widget rebuild, my app returns to the splash page".
The solution is not to prevent rebuilds, but instead to change your build method such that it fixes the issue, which is something that I detailed previously here: How to deal with unwanted widget build?
You fell for the same issue as in the cross-linked question: You mis-used FutureBuilder.
DON'T:
#override
Widget build(BuildContext context) {
return FutureBuilder(
// BAD: will recreate the future when the widget rebuild
future: Future.delayed(new Duration(seconds: fakeDelayInSeconds)),
...
);
}
DO:
class Example extends StatefulWidget {
#override
_ExampleState createState() => _ExampleState();
}
class _ExampleState extends State<Example> {
// Cache the future in a StatefulWidget so that it is created only once
final fakeDelayInSeconds = Future<void>.delayed(const Duration(seconds: 2));
#override
Widget build(BuildContext context) {
return FutureBuilder(
// Rebuilding the widget no longer recreates the future
future: fakeDelayInSeconds,
...
);
}
}
When using Consumer, you are forcing the widget to rebuild every time you notify listeners.
To avoid such behaviour, you can use Provider.of as stated in ian villamia's answer, as it can be used wherever you need it, and only where you need it.
The changes in your code to use Provider.of would be removing the consumer and adding Provider.of when resolving the theme as follows:
theme: Provider.of<PreferencesProvider>(context).isDarkMode ? DarkTheme.themeData : LightTheme.themeData,
HOWEVER if you want to keep using Consumer, you can do something else:
The child property on the Consumer widget is a child that is not rebuilt. You can use this to set the TheSpashScreen there, and pass it to the materialApp through the builder.
TL:DR
Use Provider.of if you need only to tap into one variable for simplicity.
Use Consumer with its child property as the child doesn't rebuild. <= Better performance
Using Provider.of
class App extends StatelessWidget {
#override
Widget build(BuildContext context) {
SystemChrome.setEnabledSystemUIOverlays([]);
return MultiProvider(
providers: [
ChangeNotifierProvider<PreferencesProvider>(create: (_) => PreferencesProvider()),
ChangeNotifierProvider<ConnectionProvider>(
create: (_) => ConnectionProvider(),
),
ChangeNotifierProvider<AuthenticationProvider>(create: (_) => AuthenticationProvider()),
],
child: Builder(
builder: (ctx) {
return MaterialApp(
home: TheSpashPage(),
theme: Provider.of<PreferencesProvider>(ctx).isDarkMode ? DarkTheme.themeData : LightTheme.themeData,
);
}),
);
}
}
Using Consumer
class App extends StatelessWidget {
#override
Widget build(BuildContext context) {
SystemChrome.setEnabledSystemUIOverlays([]);
return MultiProvider(
providers: [
ChangeNotifierProvider<PreferencesProvider>(create: (_) => PreferencesProvider()),
ChangeNotifierProvider<ConnectionProvider>(
create: (_) => ConnectionProvider(),
),
ChangeNotifierProvider<AuthenticationProvider>(create: (_) => AuthenticationProvider()),
],
child: Consumer<PreferencesProvider>(
child: TheSpashPage(),
builder: (context, preferences, child) {
return MaterialApp(
home: child,
theme: preferences.isDarkMode ? DarkTheme.themeData : LightTheme.themeData,
debugShowCheckedModeBanner: false,
);
}),
);
}
}
I hope this is helpful for you!
basically there's 2 ways in using a provider
one it the current one you're using which is the consumer type,
is using the instance of a provider
final _preferencesProvider= Provider.of<PreferencesProvider>(context, listen: false);
you can toggle the "listen:true" if you want the widget to rebuild when notifyListeners() are called... false if otherwise
also just use _preferencesProvider.someValue like any other instance

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

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.