Flutter State Management: Provider, BLoC, GetX, Riverpod, GetIt and MobX
In this article we are going to review a general overview of the state management and then the most interesting state managements: Provider, BLoC, GetX, Riverpod, GetIt and MobX.
General Overview
As we work developing mobile apps, there comes a time when we need to share app state between screens or across the entire app.
In this example we have 3 Screens, MyLoginScreen, MyCatalog and MyCart. In MyCatalog we need to know the status of the application to see if the product is already in the shopping cart. And in MyCart we want to see all the added products and the total purchase. In both screens we need to know the status of the cart. This is an example of the status of the application, later we will review it in more detail.
- Many assumptions you might have coming from Android or iOS development don’t apply to Flutter. For example, in Flutter it’s okay to rebuild parts of the UI from scratch instead of modifying it. Flutter is fast enough to do this, even every frame if necessary.
- Flutter is declarative. This means that Flutter build its UI to reflect the current state application.
- When the app state changes, for example, we suppose that we have a settings screen and the user tap a switch, what it does is change the state and this triggers a UI redesign. There isn’t an imperative UI change, what it changes is the state and the UI is rebuilt from scratch.
- The declarative style of the UI programming has a lot of benefits. We describe how the UI should look for any state, once and that’s it.
Differences between Ephemeral State and App State
The app state is all that exists in the memory when the application is running. This includes the assets, variables, animation state, fonts and so on.
The state that we manage can be separated into 2 types: ephemeral state and app state.
Ephemeral State
Is the state that can contain a single widget. For example:
- Current page into a widget
- Current progress of an animation
- Selected tab in a BottomNavigationBar
In other words, there isn’t need to use state management techniques in this type of state. All we need is a StatefulWidget
App state
Obviously, it is a state that isn’t ephemeral, that we want to share in many parts of the application and we want to keep among user sessions. Application state examples:
- User’s preferences
- Login information
- The shopping cart in the e-commerce application
Here is where we may need state management.
This is the example that we are going to develop for all state managements.
Provider
If you are a beginner in Flutter and don’t have a solid reason to choose another approach, likely this is the approach you should start with. The Provider package is easy to understand and doesn’t use a lot of code. It also uses concepts that are used in any other approach.
This application has 2 separate screens: a catalog and cart (represented by MyCart and MyCatalog widgets). The catalog screen includes an app bar and a list of items.
So we have some widgets. Many of them need access to a state that “belongs” elsewhere. For example, each item in the catalog can be added to the cart. We may also want to see if the currently displayed item is already in the cart.
This brings us to our first question: where should we put the current state of the cart?
- In Flutter, makes sense to keep the state above the widgets that use it. Why? In declarative frameworks like flutter, if we want to change the UI, we must rebuild it. In other words, its difficult to imperatively change a widget from the outside by calling a method on it.
- You would need to take into consideration the current state of the UI and apply the new data to it. It’s hard to avoid bugs this way.
- In flutter we build a new widget every time that it content’s change
To answer the previous question, we should put the cart state at the app level. With this we can access the state in MyCart and MyCatalog. if we put the state at the MyCatalog level, we don’t have acces in MyCart. In Flutter it’s good practice to put the app state as low in the widget tree as posible to avoid unnecesary UI rebuilds.
Now let’s review the code.
First, we have 3 states: Initial, loading and success.
enum Status {
initial,
loading,
success,
}
With provider you do need to understand 3 concepts: ChangeNotifier, ChangeNotifierProvider and Consumer.
ChangeNotifier
ChangeNotifier is a simple class included in the Flutter SDK which provides change notification to its listeners. In other words, if something is a ChangeNotifier, you can subscribe to its changes.
In provider, ChangeNotifier is one way to encapsulate your application state. For very simple apps, you get by with a single ChangeNotifier. In complex ones, you’ll have several models, and therefore several ChangeNotifiers.
The only code that is specific to ChangeNotifier is the call to notifyListeners(). Call this method any time the model changes in a way that might change your app’s UI. Everything else in CartModel is the model itself and its business logic.
Our DataProvider is the following:
class DataProvider extends ChangeNotifier {
/// Internal, private state of the dataProvider.
Status _state = Status.initial;
/// State of the dataProvider.
Status get state => _state;
/// Update State of the dataProvider. This is the only way to modify the
/// dataProvider from the outside.
void fecthData() async {
_state = Status.loading;
// This call tells the widgets that are listening to this model to rebuild.
notifyListeners();
await Future.delayed(const Duration(seconds: 2));
_state = Status.success;
notifyListeners();
}
}
ChangeNotifierProvider
ChangeNotifierProvider is the widget that provides an instance of a ChangeNotifier to its descendants. It comes from the provider package.
We already know where to put ChangeNotifierProvider: above the widgets that need to access it. In the case of CartModel, that means somewhere above both MyCart and MyCatalog.
In our example, we have a ProviderPage that has a ChangeNotifierProvider and HomeProvider (Our UI) as a child.
class ProviderPage extends StatelessWidget {
static const route = 'provider-page';
const ProviderPage({super.key});
@override
Widget build(BuildContext context) {
return ChangeNotifierProvider(
create: (context) => DataProvider(),
child: const HomeProvider(),
);
}
}
Consumer
It is best practice to put your Consumer widgets as deep in the tree as possible. You don’t want to rebuild large portions of the UI just because some detail somewhere changed.
class HomeProvider extends StatefulWidget {
const HomeProvider({super.key});
@override
State<HomeProvider> createState() => _HomeProviderState();
}
class _HomeProviderState extends State<HomeProvider> {
late DataProvider provider;
@override
void initState() {
provider = Provider.of<DataProvider>(context, listen: false);
super.initState();
}
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(title: const Text('Provider Page')),
//It is best practice to put your Consumer widgets as deep in the tree as possible.
//You don’t want to rebuild large portions of the UI just because some detail somewhere changed.
body: Consumer<DataProvider>(
builder: (context, data, child) {
if (data.state == Status.initial) {
return const Center(child: Text('Press the Button'));
}
if (data.state == Status.loading) {
return const Center(child: CircularProgressIndicator());
}
if (data.state == Status.success) {
return const Center(child: Text('Success'));
}
return Container();
},
),
floatingActionButton: Column(
crossAxisAlignment: CrossAxisAlignment.end,
mainAxisAlignment: MainAxisAlignment.end,
children: <Widget>[
FloatingActionButton(
child: const Icon(Icons.play_arrow),
onPressed: () => provider.fecthData(),
),
],
),
);
}
}
And as a result:
Pros
- If you are new to Flutter this is probably the approach you should start with. Is easy to understand and it doesn’t use much code.
- Devtool friendly — using Provider, the state of your application will be visible in the Flutter devtool
- Simplify data allocation and disposal of resources (data).
Cons
- May accidentally call unnecessary updates. Not every change in the state of an object needs to trigger an update. However, if you are using Provider then they will trigger an update all the time when there is a change.
- Scalability
BLoC
- The purpose of this package is to facilitate the separation of UI and business logic.
- This package abstracts the reactive aspects of the pattern, allowing developers to focus on writing business logic
- A Bloc is a more advanced class that is based on events to trigger state changes instead of methods. Blocs receive events and convert the incoming events to outgoing events.
To better understand this part, events are going to be added from the UI and the BLoC processes them and will respond with state changes to these events. Using the bloc library allows us to separate our application into three layers: Presentation, Business Logic and Data.
State changes in bloc begin when events are added which triggers onEvent. The events are then funnelled through an EventTransformer. By default, each event is processed concurrently but a custom EventTransformer can be provided to manipulate the incoming event stream. All registered EventHandlers for that event type are then invoked with the incoming event. Each EventHandler is responsible for emitting zero or more states in response to the event. Lastly, onTransition is called just before the state is updated and contains the current state, event, and next state.
In the BLoC implementation we will have 3 files: data_bloc, data_event and data_state.
In the Bloc State, we are going to have 3 states: Initial, Loading and Success.
part of 'data_bloc.dart';
abstract class DataState {}
class Initial extends DataState {}
class Loading extends DataState {}
class Success extends DataState {}
Besides, in the Bloc Event, we are only going to have FecthDataEvent.
part of 'data_bloc.dart';
abstract class DataEvent {}
class FetchDataEvent extends DataEvent {}
In the DataBloc, we have an _onFetchDataEvent method that is called when the event FetchDataEvent is added.
import 'package:flutter_bloc/flutter_bloc.dart';
part 'data_event.dart';
part 'data_state.dart';
class DataBloc extends Bloc<DataEvent, DataState> {
DataBloc() : super(Initial()) {
on<FetchDataEvent>(_onFetchDataEvent);
}
void _onFetchDataEvent(
FetchDataEvent event,
Emitter<DataState> emit,
) async {
emit(Loading());
await Future.delayed(const Duration(seconds: 2));
emit(Success());
}
}
For this example, we have a BlocPage that has a BlocProvider and HomeBloc(Our UI) as a child.
class BlocPage extends StatelessWidget {
static const route = 'bloc-page';
const BlocPage({super.key});
@override
Widget build(BuildContext context) {
return BlocProvider(
create: (context) => DataBloc(),
child: const HomeBloc(),
);
}
}
class HomeBloc extends StatefulWidget {
const HomeBloc({super.key});
@override
State<HomeBloc> createState() => _HomeBlocState();
}
class _HomeBlocState extends State<HomeBloc> {
late DataBloc bloc;
@override
void initState() {
bloc = BlocProvider.of<DataBloc>(context);
super.initState();
}
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(title: const Text('BLoC Page')),
body: BlocBuilder<DataBloc, DataState>(
builder: (context, state) {
if (state is Initial) {
return const Center(child: Text('Press the Button'));
}
if (state is Loading) {
return const Center(child: CircularProgressIndicator());
}
if (state is Success) {
return const Center(child: Text('Success'));
}
return Container();
},
),
floatingActionButton: Column(
crossAxisAlignment: CrossAxisAlignment.end,
mainAxisAlignment: MainAxisAlignment.end,
children: <Widget>[
FloatingActionButton(
child: const Icon(Icons.play_arrow),
onPressed: () => bloc.add(FetchDataEvent()),
),
],
),
);
}
}
And as a Bloc result:
Pros
- Know what state our application is in at any point in time.
- Easily test every case to make sure our app is responding appropriately.
- Record every single user interaction in our application so that we can make data-driven decisions.
- Develop fast and reactive apps.
- Better performance for large data sizes.
Cons
- Effective only if you have a big application.
- You need to use streams in both directions which may create more boilerplate than Provider.
- It can be too limiting in complex scenarios because it allows creating BLoCs that deal only one input and one output.
- May lead to code duplication, especially if you need to implement similar business logic in multiple parts of your app.
GetX
- Is an extra-light and powerful solution for Flutter. It combines high-performance state management, intelligent dependency injection, and route management quickly and practically.
- Is focused on performance and minimum consumption of resources. GetX does not use Streams or ChangeNotifier.
- Uses an easy and pleasant syntax.
- Allows the total decoupling of the View, presentation logic, business logic, dependency injection, and navigation.
In the following image we can see all the functions of GetX:
But what matters most to us is the Reactive State Manager.
Reactive State Manager
Reactive programming can alienate many people because it is said to be complicated. GetX turns reactive programming into something quite simple:
- You won’t need to create StreamControllers.
- You won’t need to create a StreamBuilder for each variable
- You will not need to create a class for each state.
- You will not need to create a get for an initial value.
Let’s imagine that you have a name variable and want that every time you change it, all widgets that use it are automatically changed.
This is your count variable:
var name = 'Jonatas Borges';
To make it observable, you just need to add “.obs” to the end of it:
var name = 'Jonatas Borges'.obs;
And in the UI, when you want to show that value and update the screen whenever the values changes, simply do this:
Obx(() => Text("${controller.name}"));
That’s all. It’s that simple.
Now we are going to the code. In GetX Controllers are defined, in this case we create a Controller class that extends GetxController.
enum Status {
initial,
loading,
success,
}
class Controller extends GetxController {
//Getx is reactive, so when a variable changes,
//it will automatically change on the screen.
//You just need to add a ".obs" in front of your variable,
//and that's it, it's already reactive.
var state = Status.initial.obs;
/// Update State of the Controller. This is the only way to modify the
/// Controller from the outside.
void fecthData() async {
state.value = Status.loading;
//update();
await Future.delayed(const Duration(seconds: 2));
state.value = Status.success;
}
}
In our UI, we have a GetXPage that has a simple HomeGetX widget as a child. It was defined in this way, to see the differences with Provider and Bloc, where you had to define a parent widget as ChangeNotifierProvider or BlocProvider to provide an instance of the Provider or Bloc to the child widgets.
class GetXPage extends StatelessWidget {
static const route = 'getx-page';
const GetXPage({super.key});
@override
Widget build(BuildContext context) {
return const HomeGetX();
}
}
class HomeGetX extends StatefulWidget {
const HomeGetX({super.key});
@override
State<HomeGetX> createState() => _HomeGetXState();
}
class _HomeGetXState extends State<HomeGetX> {
late Controller c;
@override
void initState() {
c = Get.put(Controller());
super.initState();
}
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(title: const Text('GetX Page')),
//It is best practice to put your Consumer widgets as deep in the tree as possible.
//You don’t want to rebuild large portions of the UI just because some detail somewhere changed.
body: GetX<Controller>(
builder: (context) {
if (c.state.value == Status.initial) {
return const Center(child: Text('Press the Button'));
}
if (c.state.value == Status.loading) {
return const Center(child: CircularProgressIndicator());
}
if (c.state.value == Status.success) {
return const Center(child: Text('Success'));
}
return Container();
},
),
floatingActionButton: Column(
crossAxisAlignment: CrossAxisAlignment.end,
mainAxisAlignment: MainAxisAlignment.end,
children: <Widget>[
FloatingActionButton(
child: const Icon(Icons.play_arrow),
onPressed: () => c.fecthData(),
),
],
),
);
}
}
And the result is the following:
Pros
- It’s a Simple State Updater, made in just a few lines of code. It was made simple, to have the least CPU impact, and just to fulfill a single purpose and spend the minimum resources possible.
- It’s a powerful State Manager, it doesn’t work with variables, but flows, everything in it are Streams under the hood.
- It is literally a BLoC approach, without code generators or decorations. You can turn anything into an “Observable” with just a .obs .
Cons
- Can exists a lot of issues which can be also duplicit. Here it is obviously that nobody cares about issues like: solving, answering, tagging, remove duplicity, etc.
- GetX is doing too much things and this project is too big for only one person.
- Problems with hot reload — GetX has their own dependency injection system and that is used for almost every module in GetX, but it is not stable yet.
- Write unit and widget tests is really very hard with GetX and in some cases is imposible test some features.
Riverpod
- Is similar to Provider and is compile-safe and testable.
- Riverpod is inspired by Provider but solves some of it’s key issues such as supporting multiple providers of the same type; awaiting asynchronous providers; adding providers from anywhere.
- No need to jump between your main.dart and your UI files anymore.
- Place the code of your shared state where it belongs, be it in a separate package or right next to the Widget that needs it, without losing testability.
Providers are the most important part of a Riverpod application. A provider is an object that encapsulates a piece of state and allows listening to that state.
enum Status {
initial,
loading,
success,
}
class RiverpodProvider extends StateNotifier<Status> {
RiverpodProvider() : super(Status.initial);
Future<void> fetchData() async {
state = Status.loading;
await Future.delayed(const Duration(seconds: 2));
state = Status.success;
}
}
final riverpodProvider =
StateNotifierProvider.autoDispose((ref) => RiverpodProvider());
In the UI we have:
class RiverpodPage extends StatelessWidget {
static const route = 'riverpod-page';
const RiverpodPage({super.key});
@override
Widget build(BuildContext context) {
return const ProviderScope(
child: HomeRiverpod(),
);
}
}
For widgets to be able to read providers, we need to wrap the entire application in a “ProviderScope” widget. This is where the state of our providers will be stored.
class HomeRiverpod extends ConsumerWidget {
const HomeRiverpod({super.key});
@override
Widget build(BuildContext context, WidgetRef ref) {
final data = ref.read(riverpodProvider.notifier);
return Scaffold(
appBar: AppBar(title: const Text('Riverpod Page')),
//It is best practice to put your Consumer widgets as deep in the tree as possible.
//You don't want to rebuild large portions of the UI just because some detail somewhere changed.
body: Consumer(
builder: (context, ref, child) {
final state = ref.watch(riverpodProvider);
if (state == Status.initial) {
return const Center(child: Text('Press the Button'));
}
if (state == Status.loading) {
return const Center(child: CircularProgressIndicator());
}
if (state == Status.success) {
return const Center(child: Text('Success'));
}
return Container();
},
),
floatingActionButton: Column(
crossAxisAlignment: CrossAxisAlignment.end,
mainAxisAlignment: MainAxisAlignment.end,
children: <Widget>[
FloatingActionButton(
child: const Icon(Icons.play_arrow),
onPressed: () => data.fetchData(),
),
],
),
);
}
}
Using ref to interact with providers
There are three primary usages for “ref”:
- Obtaining the value of a provider and listening to changes, such that when this value changes, this will rebuild the widget or provider that subscribed to the value. This is done using ref.watch
- Adding a listener on a provider, to execute an action such as navigating to a new page or showing a modal whenever that provider changes.
This is done using ref.listen. - Obtaining the value of a provider while ignoring changes. This is useful when we need the value of a provider in an event such as “on click”. This is done using ref.read.
And the result is the following:
Pros
- Riverpod is stable and actively maintained.
- Riverpod does not directly depend on the widget tree. The providers are declared globally and can be used anywhere in the application
- Write testable code and keeping logic outside the widget tree
Cons
- Riverpod give you too much freedom to implement state on your app. that can be a challenge for new developers to choose best approach.
- It encourages a bad antipattern of scattering shared state all throughout your widget tree. This can lead to code being very hard to find (non-centralized), and makes debugging harder to follow as there are excessive dependency chains and couplings between providers.
GetIt
GetIt isn’t a state management solution! It’s a locator for your objects so you need some other way to notify your UI about changes like Streams or ValueNotifiers. But together with the get_it_mixin it gets a full featured easy state management solution that integrates with the Objects registered in get_it.
If you are not familiar with the concept of Service Locators, it’s a way to decouple the interface (abstract base class) from a concrete implementation, and at the same time allows to access the concrete implementation from everywhere in your App over the interface.
GetIt is:
- Extremely fast
- Easy to learn/use
- Doesn’t clutter your UI tree with special Widgets to access your data like provider or Redux does.
Our implementation is the following:
enum Status {
initial,
loading,
success,
}
class GetItProvider extends ChangeNotifier {
/// Internal, private state of the GetItProvider.
Status _state = Status.initial;
/// State of the GetItProvider.
Status get state => _state;
/// Update State of the GetItProvider. This is the only way to modify the
/// GetItProvider from the outside.
void fecthData() async {
_state = Status.loading;
// This call tells the widgets that are listening to this model to rebuild.
notifyListeners();
await Future.delayed(const Duration(seconds: 2));
_state = Status.success;
notifyListeners();
}
}
If we notice, the GetItProvider is the same as the DataProvider that we implemented when we reviewed Provider.
Before you can access your objects, you must register them within GetIt, usually directly in your initState code.
class GetItPage extends StatefulWidget {
static const route = 'get-it-page';
const GetItPage({super.key});
@override
State<GetItPage> createState() => _GetItPageState();
}
class _GetItPageState extends State<GetItPage> {
@override
void initState() {
//At your start-up you register all the objects
//you want to access later like this:
GetIt.I.registerSingleton<GetItProvider>(GetItProvider());
super.initState();
}
@override
Widget build(BuildContext context) {
return HomeGetIt();
}
}
Reading data is already quite easy with GetIt, but it gets even easier with the mixin. Just add a GetItMixin to a StatelessWidget and call get<T>:
class HomeGetIt extends StatelessWidget with GetItMixin {
HomeGetIt({super.key});
@override
Widget build(BuildContext context) {
//You could tell your view to rebuild
//any time state changes with a simple call to watchOnly:
final state =
watchOnly((GetItProvider getItProvider) => getItProvider.state);
return Scaffold(
appBar: AppBar(title: const Text('GetIt Page')),
body: Builder(
builder: (context) {
if (state == Status.initial) {
return const Center(child: Text('Press the Button'));
}
if (state == Status.loading) {
return const Center(child: CircularProgressIndicator());
}
if (state == Status.success) {
return const Center(child: Text('Success'));
}
return Container();
},
),
floatingActionButton: Column(
crossAxisAlignment: CrossAxisAlignment.end,
mainAxisAlignment: MainAxisAlignment.end,
children: <Widget>[
FloatingActionButton(
child: const Icon(Icons.play_arrow),
//After that you can access your GetItProvider class
//from anywhere like this:
onPressed: () => get<GetItProvider>().fecthData(),
),
],
),
);
}
}
There are various watch methods, for common types of data sources, including ChangeNotifier, ValueNotifier, Stream and Future.
The primary benefit to the watch methods is that they eliminate the need for ValueListenableBuilders, StreamBuilder etc. Each binding consumes only one line and there is no nesting.
The final result:
Pros
- Is straightforward, easier to use, and not dependant on Flutter, so can be used with any Dart code.
- Extremely fast, easy to learn/use, doesn’t clutter your UI tree with special Widgets to access your data.
Cons
- You may have to manually dispose or unregister items as they are not intrinsically part of the widget tree.
- Suffers from the issue of providing instances of the same type, however it does expose a name field when registering, which offers a reasonable workaround for the issue.
- Some developers may not like the global nature of GetIt singletons, preferring the more restrictive scoping models of riverpod or Provider, while other developers may view this as a benefit.
MobX
MobX is a state-management library that makes it simple to connect the reactive data of your application with the UI. This wiring is completely automatic and feels very natural. As the application-developer, you focus purely on what reactive-data needs to be consumed in the UI (and elsewhere) without worrying about keeping the two in sync.
It’s not really magic but it does have some smarts around what is being consumed (observables) and where (reactions), and automatically tracks it for you. When the observables change, all reactions are re-run. What’s interesting is that these reactions can be anything from a simple console log, a network call to re-rendering the UI.
At the heart of MobX are three important concepts: Observables, Actions and Reactions.
Observables
- Observables represent the reactive-state of your application. They can be simple scalars to complex object trees. By defining the state of the application as a tree of observables, you can expose a reactive-state-tree that the UI (or other observers in the app) consume.
Actions
- Actions are how you mutate the observables. Rather than mutating them directly, actions add a semantic meaning to the mutations. For example, instead of just doing value++, firing an increment() action carries more meaning. Besides, actions also batch up all the notifications and ensure the changes are notified only after they complete. Thus the observers are notified only upon the atomic completion of the action.
Reactions
- Reactions complete the MobX triad of observables, actions and reactions. They are the observers of the reactive-system and get notified whenever an observable they track is changed. Reactions come in few flavors as listed below. All of them return a ReactionDisposer, a function that can be called to dispose the reaction. One striking feature of reactions is that they automatically track all the observables without any explicit wiring. The act of reading an observable within a reaction is enough to track it!
A store in MobX is a way of collecting the related observable state under one class. The store allows us to use annotations and keeps the code simple.
enum Status {
initial,
loading,
success,
}
// This is the class used by rest of your codebase
// ignore: library_private_types_in_public_api
class DataStore = _DataStore with _$DataStore;
// The store-class
abstract class _DataStore with Store {
@observable
Status state = Status.initial;
@action
Future<void> fecthData() async {
state = Status.loading;
await Future.delayed(const Duration(seconds: 2));
state = Status.success;
}
}
Note the use of annotations to mark the observable properties of the class. Annotations are available via the mobx_codgen package.
The interesting parts here are:
- The abstract class _DataStore that includes the Store mixin. All of your store-related code should be placed inside this abstract class. We create a DataStore class to blend in the code from the build_runner.
- The generated code will be inside the part file: data_store.g.dart, which we include with the part directive. Without this, the build_runner will not produce any output. The generated file contains the _$DataStore mixin.
- The @observable annotation to mark the value as observable.
- Use of @action to mark the increment() method as an action.
Run the following command inside your project folder. This generates the code in counter.g.dart, which we have already included as part file.
flutter pub run build_runner build
Now let’s look the UI:
final dataStore = DataStore(); // Instantiate the store
class MobXPage extends StatelessWidget {
static const route = 'mobx-page';
const MobXPage({super.key});
@override
Widget build(BuildContext context) {
return const HomeMobX();
}
}
The Observer widget (which is part of the flutter_mobx), provides a granular observer of the observables used in its builder function. Whenever these observables change, Observer rebuilds and renders.
class HomeMobX extends StatelessWidget {
const HomeMobX({super.key});
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(title: const Text('MobX Page')),
//It is best practice to put your Consumer widgets as deep in the tree as possible.
//You don't want to rebuild large portions of the UI just because some detail somewhere changed.
body: Observer(
builder: (_) {
if (dataStore.state == Status.initial) {
return const Center(child: Text('Press the Button'));
}
if (dataStore.state == Status.loading) {
return const Center(child: CircularProgressIndicator());
}
if (dataStore.state == Status.success) {
return const Center(child: Text('Success'));
}
return Container();
},
),
floatingActionButton: Column(
crossAxisAlignment: CrossAxisAlignment.end,
mainAxisAlignment: MainAxisAlignment.end,
children: <Widget>[
FloatingActionButton(
child: const Icon(Icons.play_arrow),
onPressed: () => dataStore.fecthData(),
),
],
),
);
}
}
As a result:
Pros
- Easy to connect the reactive app data with the UI.
- Implements the observer pattern with a friendly syntax and simple core API that makes learning easier.
- It is scalable and can be used in large projects.
- Good performance and easy to test.
- Less boilerplate beacause code generation.
Cons
- It takes some time to generate the code, and You’ll probably need to run the code generator for every change to your store
- You only know that state changes, but not the exact event that caused it. In more complex apps this lack of traceability may pose a problem, since it makes debugging and managing state changes more obscure.
- The Mobx generators are great in increasing simplicity, but at the same time add another level of abstraction. They make it harder to really see and understand what goes on under the hood.
Conclusion
- State management is one of the most crucial aspects. It’s a way to keep track of all the changes to the UI that a user makes.
- The state management libraries in Flutter make it easy to develop and manage applications, regardless of the type of state changes.
- Choosing the right Flutter state manager is just as important as using it. There are many options of state managers, but the choice of which one to use will depend on us, on our likes, with which we feel comfortable, also on the type of project, on the needs, if it is a small project we can use an approach, or if we want it to be scalable, another approach.
Finally what you were waiting for, here you can find the complete example:
If you like it, you can Buy Me A Coffee!