Simplify Flutter State Management with Provider and BLoC

An Lam
FlutterVN
Published in
4 min readAug 21, 2019

Nowadays, Flutter becomes popular since cross-platform application becomes a trend. Many developers choose Flutter as a solution for developing application quickly and high efficiency. And I am not an exception.

Begin with Flutter, I’m sure that you will be confused because there are a lot of patterns and libraries for state management: BLoC Architecture, MobX, ScopedModel, Redux, Provider,

Since Google announced Provider at Google I/O, Provider becomes the best choice for many developers. Me too!. I’ve tried my app with Provider and got some experiences. In my opinion, it’s good, but there is something which is not suitable for me. Let me explain the details with my demo.

Provider

Provider helps you inject your logic into application easily. For instance, I can add ChangeNotifierProvider with Tab1Bloc, Tab2Bloc (ChangeNotifierProvider is also a part of Provider), and Provider with Tab3Bloc as below:

class MyApp extends StatelessWidget {
// This widget is the root of your application.
@override
Widget build(BuildContext context) {
return MultiProvider(
providers: [
ChangeNotifierProvider(builder: (_) => Tab1Bloc()),
ChangeNotifierProvider(builder: (_) => Tab2Bloc()),
Provider(builder: (_) => Tab3Bloc()),
],
child: MaterialApp(
title: 'Flutter Demo',
theme: ThemeData(
primarySwatch: Colors.blue,
),
home: TabContainer(),
),
);
}
}

And you can update your logic to widget easily:

class Tab1Bloc extends ChangeNotifier {
TodoModel todoModel;
bool loading = false;

Future getTodoData() async {
final dio = Dio();
var todoTask = GetTodoTask(dio: dio, id: 1);
final worker = Worker(poolSize: 2);
loading = true;
notifyListeners();
todoModel = (await worker.handle(todoTask)) as TodoModel;
loading = false;
notifyListeners();
}
}

Let’s dig into Tab1Bloc, I used isolate_worker to get Todo data from service. Before calling network, I set:

loading = true;
notifyListeners();

After this call, the ‘loading’ value will be notified to the widget. After getting data from service, I reset the value to ‘false’ and notify again. The widget will be updated inside Consumer as below:

Consumer<Tab1Bloc>(builder: (context, bloc, child) {
var value = 'No data';
if (bloc.loading) {
value = 'Loading...';
} else {
if (bloc.todoModel != null) {
value = bloc.todoModel.toString();
}
}
return Text(value);
})

What outside Consumer will not be updated. You can check the ‘build’ method in file tab1.dart. I put:

print('build Tab1');

But it won’t be called when you update ‘loading’. It’s great! Right?

However, if you have 2 values, for instances, ‘loading’ is used to trigger updating ‘TodoWidget’, and ‘count’ is used to trigger updating ‘CounterWidget’. What will happen?

Issue with ChangeNotifierProvider

In Tab2Bloc, what you want is, ‘loading’ only make ‘TodoWidget’ updated and ‘count’ make ‘CounterWidget’ updated. But when you change ‘loading’ or ‘count’, both ‘TodoWidget’ and ‘CounterWidget’ updated. How to solve this?

StreamProvider or StreamBuilder?

There are 2 ways to solve above issue: StreamProvider or StreamBuilder.

StreamProvider is a part of Provider, used same as ChangeNotifierProvider, but you don’t need to call ‘notifyListeners()’. The widget will be updated immediately after you change value. However, in my opinion, you need to inject each value in logic class. The situation, in this case, is ‘loading’ and ‘count’ in Tab2Bloc like:

//For loadingStreamProvider.controller(builder: (_) => StreamController<bool>()),//For countStreamProvider.controller(builder: (_) => StreamController<int>()),

I want to control all logics inside Tab2Bloc. So you also need to inject Tab2Bloc into your app. Look like StreamBuilder?

StreamBuilder also uses stream like StreamProvider, it’s used effectively with BLoC pattern. You have to create StreamController of value instead of value. For instance, loadingStreamController and countStreamController. I use StreamBuilder combine with Provider.

Inject:

Provider(builder: (_) => Tab3Bloc()),

Logic:

class Tab3Bloc {
TodoModel todoModel;
FlStreamController<bool> loadingStreamController = FlStreamController();
FlStreamController<int> countStreamController = FlStreamController();
int _count = 0;

Future getTodoData() async {
final dio = Dio();
var todoTask = GetTodoTask(dio: dio, id: 1);
final worker = Worker(poolSize: 2);
loadingStreamController.setData(true);
todoModel = (await worker.handle(todoTask)) as TodoModel;
loadingStreamController.setData(false);
}

void increaseCount() {
countStreamController.setData(_count++);
}
}

Note: FlStreamController is a custom class for easily using. You can check it in tab3_bloc.dart.

TodoWidget now becomes:

class TodoWidget extends StatelessWidget {
@override
Widget build(BuildContext context) {
print('build TodoWidget');

var bloc = Provider.of<Tab3Bloc>(context, listen: false);
return StreamBuilder<bool>(
stream: bloc.loadingStreamController.dataStream,
initialData: false,
builder: (context, snapshot) {
var loading = snapshot.data;

var value = 'No data';
if (loading) {
value = 'Loading...';
} else {
if (bloc.todoModel != null) {
value = bloc.todoModel.toString();
}
}

return Text(value);
});
}
}

And CounterWidget:

class CounterWidget extends StatelessWidget {
@override
Widget build(BuildContext context) {
print('build CounterWidget');
var bloc = Provider.of<Tab3Bloc>(context, listen: false);
return StreamBuilder<int>(
stream: bloc.countStreamController.dataStream,
initialData: 0,
builder: (context, snapshot) {
return Text('${snapshot.data}');
});
}
}

Now run your app and go to Tab3, if you increase count, only CounterWidget updated, and if you get Todo data, only TodoWidget updated.

Conclusion

After trying with many patterns and libraries, I decide to use ChangeNotifierProvider combine with BLoC (StreamBuilder) for my best choice. Until now, I haven’t found any other solution better than these. If you guys have any suggestions, please suggest me. Thanks!

Demo

You can download and check my demo on Github:

--

--