My Flutter app has multiple BloCs (via the bloc and flutter_bloc packages) which has caused some technical difficulties that I solved using a workaround, but I was wondering if there was a better solution out there.
I am using BlocBuilder() when listening to a bloc where each bloc has its own BlocBuilder() call. At first, I was nesting the BlocBuilder() calls as follows:
Widget build(BuildContext context) {
return BlocBuilder (
bloc: bloc1,
builder: (ctx, snapshot1) {
do_something1(snapshot1);
return BlocBuilder(ctx2, snapshot2) {
bloc: bloc2,
builder: (ctx2, snapshot2) {
do_something2(snapshot2);
return renderWidget();
}
}
}
);
}
The issue I have with this nested BlocBuilder() calls is that if data comes in for bloc1, the BlocBuilder() for bloc2 will be re-called, causing the current state for bloc2 to be re-read() and causing difficulties for do_something2() which ideally should be called only when there is new data for bloc2.
So what I did was to create a separate widget for each BlocBuilder() call, resulting with the following new build():
Widget build(BuildContext context) {
return Column(
children: [
WidgetBlocBuilder1(),
WidgetBlocBuilder2(),
renderWidget(),
],
);
}
What this does is that any data coming in for either bloc1 or bloc2 would be localized in WidgetBlocBuilder1() or WidgetBlocBuilder2() respectively and more importantly, incoming bloc data would NOT cause the BlocBuilder() for the other bloc to be re-called() as was the case in my nested BlocBuilder() approach.
Here is the build() for WidgetBlocBuilder1():
Widget build(BuildContext context) {
return BlocBuilder(
bloc: bloc,
builder: (ctx, snapshot) {
if (snapshot is CommandEditorSaveFile) {
_saveDocumentFile(ctx);
}
return Visibility(
child: Text('test'),
visible: false,
);
},
);
}
Note that WidgetBlocBuilder1() is an invisible widget as shown by the Visibility( visible:false) wrapper. This is because the widget itself should not render anything on the screen; the widget is simply a wrapper for the BlocBuilder() call. If incoming bloc data is supposed to change the visible state of the parent widget, then logic needs to be written to implement that.
This workaround seems to work, but I was wondering if there was a better solution out there.
Any suggestions would be greatly appreciated.
/Joselito
As per a suggestion from pskink, another solution is to use one StreamBuilder() to listen to multiple blocs. To do this, I used a package called multiple_streambuilder.
Here is a sample build() using this package:
Widget build(BuildContext context) {
return StreamBuilder2<int, int>(
streams: Tuple2(bloc1!.stream, bloc2!.stream),
builder: (contex, snapshot) {
if (snapshot.item1.hasData) {
print(snapshot.item1.data);
}
if (snapshot.item2.hasData) {
print(snapshot.item2.data);
}
return Scaffold(
appBar: AppBar(title: Text("test title")),
body: WidgetTest(),
);
},
);
}
In the build() above, I was listening to 2 blocs, both returning an int hence the call to StreamBuilder2<int,int>(). To find out which bloc has emitted data, you call snapshot.item1.hasdata or snapshot.item2.hasdata.
Related
I am confusing when using provider. Some developers use FutureBuilder to fetch API when open app's screen as following
var provider = locator<AppIntroProvider>(); //GetIt Dependancy Infection
#override
void initState() {
super.initState();
}
#override
Widget build(BuildContext context) {
return Scaffold(
body: FutureBuilder(
future: provider.getAppIntroItems(),
builder: (context, AsyncSnapshot<dynamic> snapshot) => snapshot
.data ==
null
? Container(
color: Colors.red,
) : MyDataListWidget(),
),
);
}
But I implemented API call in InitState and listen data using Consumer, Like this.
var provider = locator<AppIntroProvider>(); //GetIt DI
#override
void initState() {
provider.getAppIntroItems();
super.initState();
}
#override
Widget build(BuildContext context) {
//provider = Provider.of<AppIntroProvider>(context); //If I don't use GetIt DI
return Scaffold(
body: Consumer<AppIntroProvider>(
builder: (context, appInfoProvider, child) => appInfoProvider
.appIntroItemsDao ==
null
? Container(
color: Colors.red,
)
: MyDataListWidget(),
),
);
}
My Question is
What is the purpose of using FutureBuilder instead of using Consumer?
What is the different?
What is right and more efficient way to implement API call?
FutureBuilder is just a simple widget that handles some of the operations such as fetching the future in init state in the end you can use both of them.
I usually use FutureBuilder if I want to deal with different states such as Loading, Error, HasData it makes it easier to control them in the builder function
Consumer just allows you to access your state management data.
Also if you don't use FutureBuilder you have to maintain a loading state such as setting it to loading when you first start the request and then setting it to false after it's done. This can be useful if you want to use the loading state in somewhere
I have two BLoCs and need to listen for state changes from one in order to add events to the other and build the UI.
I think it's easier to explain in code:
#override
Widget build(BuildContext context) {
return BlocProvider<BlocA>(
create: (context) => BlocA(), <------ Create BlocA
child: BlocProvider<BlocB>(
create: (context) => BlocB(), <---- Create BlocB
child: BlocListener<BlocA, StateA>(
listener: (context, state) {
if (state is StateA) {
context.read<BlocB>().add(EventB()); <--- When BlocA is ready, add EventB to BlocB
}
},
child: BlocBuilder<BlocB, StateB>(
builder: (context, state) {
if (state is StateBInitial) {
return Container(color: Colors.yellow); <-- shows initial as expected
}
if (state is StateBSuccess) {
return Container(color: Colors.green); <-- success is never triggered
}
return Container(color: Colors.red);
),
),
),
);
}
In the example above I create 2 BLoCs, BlocA and BlocB.
I add them in providers and then create a BlocListener that listen for BlocA changes and add events to BlocB.
The problem is that the BlocBuilder never triggers after the initial state.
I can see in debugging that the Event added by the the BlocListener is registered and data is fetched and yielded, but the BlocBuilder never triggers even though the state has changed.
It is as if the BlocListener is calling a different BlocB.
If I'm right, you are trying to make communication with two blocs?
There is a really well-explained video tutorial about it: https://www.youtube.com/watch?v=ricBLKHeubM
I'm a Flutter newbie and I have a basic understanding question:
I built a google_maps widget where I get the location of the user and also would like to show the current location in a TextWidget.
So I'm calling a function in initState querying the stream of the geolocator package:
class _SimpleMapState extends State<SimpleMap> {
Position userPosStreamOutput;
void initPos() async {
userPosStream = geoService.getStreamLocation();
userPosStream.listen((event) {
print('event:$event');
setState(() {
userPosStreamOutput = event;
});
});
setState(() {});
}
#override
void initState() {
initPos();
super.initState();
}
#override
Widget build(BuildContext context) {
return Scaffold( //(very simplified)
body: Text(userPosStreamOutput.toString()),
This works just fine. But would it make sense to use a Streamprovider instead? Why?
Thanks
Joerg
An advantage to use StreamProvider is to optimize the process of re-rendering. As setState is called to update userPosStreamOutput value, the build method will be called each time your stream yields an event.
With a StreamProvider, you can apply the same logic of initializing a stream but then, you will use a Consumer which will listen to new events incoming and provide updated data to children widgets without triggering other build method calls.
Using this approach, the build method will be called once and it also makes code more readable in my opinion.
#override
Widget build(BuildContext context) {
return Scaffold(
body: StreamProvider(
create: (_) => geoService.getStreamLocation(),
initialData: null,
child: Consumer<Position>(
builder: (context, userPosStreamOutput, _) {
return Text(userPosStreamOutput.toString());
},
),
),
);
}
In short, the question is: How do I reuse the state of an entire widget subtree?
This is what my code currently looks like:
...
BlocBuilder<AuthBloc, AuthState>(
builder: (context, state) {
if (state is Authenticating) {
return AppLoading();
} else if (state is NotAuthenticated) {
return AppOnboarding();
} else if (state is Authenticated) {
return AppMain();
} else {
return null;
}
}
),
...
Nothing fancy here, just a BlocBuilder rebuilding its child whenever the state of the underlying Bloc changes.
Now, take for instance the following state transitions: NotAuthenticated => Authenticating => NotAuthenticated because there was something wrong with the inputed information. This would result in the AppOnboarding() widget being rebuild completely from scratch with all the information lost. How can I reuse the state from the old AppOnboarding() widget to make it look like the widget was never rebuild?
What I've already tried
I've already tried using a GlobalKey for this which I would pass to the key property of my AppOnboarding widget. This is my code:
_AuthenticatingState extends State<Authenticating> {
GlobalKey<AppOnboardingState> _appOnboardingKey;
#override
initState() {
super.initState();
_appOnboardingKey = GlobalKey();
}
#override
Widget build(BuildContext context) {
return BlocBuilder<...>(
builder: (context, state) {
...
if (state is NotAuthenticated) {
return AppOnboarding(key: _appOnboardingKey);
}
...
}
),
}
}
I was a little surprised this didn't work. Do global keys not maintain the state of the entrie widget-subtree?
Flutter runs at 60 frames per second. The key tells Flutter that some widget is the same one that existed in the last frame. If you remove some widget, even for a single frame, it will dispose the widget (call the dispose method and get rid of the widget).
In other words, the key doesn't "save" any state for later.
You have two options:
Don't dispose the widget at all.
builder: (context, state) {
return Stack(children: [
Offstage(child:AppLoading(), offstage: state is! Authenticating),
Offstage(child:AppOnboarding()), offstage: state is! NotAuthenticated),
Offstage(child:AppMain()), offstage: state is! Authenticated),
]) }
}
Save yourself that widget state, so that you can rebuild the widget later with that same information.
When using flutter bloc what is the recommendation, is it recomended for each page to have its own bloc or can i reuse one block for multiple pages, if so how?
I think that the best solution is to have one BLoC per page. It helps you to always know in which state each screen is just by looking at its BLoC. If you want to show the state of each of your tabs independently you should create one BLoC for each tab, and create one Repository which will handle fetching the data. But if the state of every tab will be the same, (for example you fetch data only once for all of the screens, so you don't show loading screen on every tab) then I think that you could create just one BLoC for all of this tabs.
It is also worth to add, that BLoCs can communicate with each other. So you can get state of one BLoC from another, or listen to its state changes. That could be helpful when you decide to create separate BLoCs for tabs.
I have addressed this topic in my latest article. You can check it out if you want to dive deeper.
There are no hard-set rules about this. It depends on what you want to accomplish.
An example: if each page is "radically" from each other, then yes, a BLoC per page makes sense. You can still share an "application-wide" BLoC between those pages if some kind of sharing or interaction is required between the pages.
In general, I've noticed that usually a BLoC "per page" is useful as there are always specific things related for each page that you handle within their BLoC. You can the use a general BLoC to share data or some other common thing between them.
You can combine the BLoC pattern with RxDart to handle somewhat more complex interaction scenarios between a BLoC and the UI.
Sharing a BLoC is fairly simple, just nest them or use a MultiProvider (from the provider package):
runApp(
BlocProvider(
builder: (_) => SettingsBloc(),
child: BlocProvider(
builder: (_) => ApplicationBloc(),
child: MyApp()
)
)
);
and then you can just retrieve them via the Provider:
class MyApp extends ... {
#override
Widget build(BuildContext context) {
final settingsBloc = Provider.of<SettingsBloc>(context);
final appBloc = Provider.of<ApplicationBloc>(context);
// do something with the above BLoCs
}
}
You can share different bloc's in different pages using BlocProvider.
Let's define some RootWidget that will be responsible for holding all Bloc's.
class RootPage extends StatefulWidget {
#override
_RootPageState createState() => _RootPageState();
}
class _RootPageState extends State<RootPage> {
NavigationBloc _navigationBloc;
ProfileBloc _profileBloc;
ThingBloc _thingBloc;
#override
void initState(){
_navigationBloc = NavigationBloc();
_thingBloc = ThingBloc();
_profileBloc = ProfileBloc();
super.initState();
}
#override
Widget build(BuildContext context) {
return MultiBlocProvider(
providers: [
BlocProvider<NavigationBloc>(
builder: (BuildContext context) => _navigationBloc
),
BlocProvider<ProfileBloc>(
builder: (BuildContext context) => _profileBloc
),
BlocProvider<ThingBloc>(
builder: (BuildContext context) => _thingBloc
),
],
child: BlocBuilder(
bloc: _navigationBloc,
builder: (context, state){
if (state is DrawProfilePage){
return ProfilePage();
} else if (state is DrawThingsPage){
return ThingsPage();
} else {
return null
}
}
)
)
}
}
And after that, we can use any of bloc from parent and all widgets will share the same state and can dispatch event on the same bloc
class ThingsPage extends StatefulWidget {
#override
_ThingsPageState createState() => _ThingsPageState();
}
class _ThingsPageState extends State<ThingsPage> {
#override
void initState(){
_profileBloc = BlocProvider.of<ProfileBloc>(context);
super.initState();
}
#override
Widget build(BuildContext context) {
return Container(
child: BlocBuilder(
bloc: _profileBloc,
builder: (context, state){
if (state is ThingsAreUpdated){
return Container(
Text(state.count.toList())
);
} else {
return Container()
}
}
)
);
}
}