What Is State Management?

Whether you are developing a mobile app or a web application, state management is crucial. It speaks about controlling the state of one or more user interface controls. Text fields, radio buttons, checkboxes, dropdown menus, toggles, forms, and many other UI elements can be used as controls.

State management reflects the efficiency of the system and the care taken by the developer to build that system so that every functionality and feature performs smoothly and precisely. State management helps to align and integrate the core business logic inside the application with servers and databases. Without proper state management, the burden on users will increase and that would certainly decrease the effectiveness of an application.

In a single statement — If you want to develop stable applications, then you must properly manage your state.

Different approaches of state management in Flutter?

  1. Getx
  2. Bloc
  3. Provider
  4. Mobx
  5. Riverpod
  6. Redux

Each option has its own pros and cons and is better suited for certain use cases. Let’s take a closer look at each option.

1 ) Getx:

Getx architecture

GetX is not only a state management library, but instead, it is a microframework combined with route management and dependency injection. It aims to deliver top-of-the-line development experience in an extra lightweight but powerful solution for Flutter. GetX has three basic principles on which it is built:

  1. Performance: focused on minimum consumption of memory and resources.
  2. Productivity: intuitive and efficient tool combined with simplicity and straightforward syntax that ultimately saves development time.
  3. Organization: decoupling business logic from view and presentation logic cannot get better than this. You do not need context to navigate between routes, nor do you need stateful widgets.

The three pillars of GetX

  1. State management: GetX has two state managers. One is a simple state manager used with the GetBuilder function, and the other is a reactive state manager used with Getx or Obx. We will be talking about it in detail below.
  2. Route management: whether navigating between screens, showing SnackBars, popping dialog boxes, or adding bottom sheets without the use of context, GetX has you covered. I will not write details on route management because it is beyond the scope of this article, but indeed a few examples to get an idea of how GetX syntax simplicity works.
  3. Dependency management: GetX has a simple yet powerful solution for dependency management using controllers. With just a single line of code, it can be accessed from the view without using an inherited widget or context. Typically, you would instantiate a class within a class, but with GetX, you are instantiating with the Get instance, which will be available throughout your application.

Value-added features of GetX

GetX has some great features out of the box, making it even easier to develop mobile applications in Flutter without any boilerplate code:

  1. Internationalization: translations with key-value maps, various language support, using translations with singulars, plurals, and parameters. Changing the application’s locale using only the Get word throughout the app.
  2. Validation: email and password validations are also covered by GetX. Now you do not need to install a separate validation package.
  3. Storage: GetX also provides fast and extra light synchronous key-value memory backup of data entirely written in Dart that easily integrates with the GetX core package.
  4. Themes: switching between light and dark themes is made simple with GetX.
  5. Responsive view: if you are building an application for different screen sizes, you just need to extend with GetView, and you can quickly develop your UI, which will be responsive for desktop, tablet, phone, and watch.

2 ) Bloc :

Bloc architecture

Bloc stands for Business Logic Components. It’s a state management system for Flutter recommended by Google developers. It helps in managing the state and makes access to data from a central place in your project.

There are four main layers of application in the Bloc pattern:

UI :The UI contains all of the application’s components that the user can see and interact with.

Bloc : The bloc is the layer between the data and the UI components. The bloc receives events from an external source and emits a state in response to the received event.

Repository : The repository is designed to be the single source of truth, it is responsible for organizing data from the data source(s) to be presented by the UI.

Data providers : The data providers are responsible for fetching the application data, they are characterized by network calls and database interactions.

Flutter Bloc — notes to remember

The Bloc architecture is a versatile and easy-to-maintain pattern. This architecture allows you to create a reactive app using streams and sinks, so I recommend it for any Flutter project — to make your job easier, I named a few go-to types below.

Consider the use of the Bloc pattern:

  • When creating a StatefulWidget or writing setState ().
  • If your widget needs the data layer to communicate. (e. g. a repository, a service, an API call)
  • When a widget logic is managed by the widget itself and you feel it’s too complex.
  • If you want to make the logic fully independent of the widget.
  • If you want to automatically test the logic of your widget.
  • When you start a project, you know that there will be more than a few screens and that it needs to be scalable and durable.

3 ) Provider:

Provider architecture

The Provider package, created by Remi Rousselet, aims to handle the state as cleanly as possible. In Provider, widgets listen to changes in the state and update as soon as they are notified.

Therefore, instead of the entire widget tree rebuilding when there is a state change, only the affected widget is changed, thus reducing the amount of work and making the app run faster and more smoothly.

Three major components make all of this possible: the ChangeNotifier class in Flutter, the ChangeNotifierProvider (primarily used in our sample app), and the Consumer widgets.

Whatever change in the state observed from the ChangeNotifier class causes the listening widget to rebuild. The Provider package offers different types of providers – listed below are some of them:

  • The Provider class takes a value and exposes it, regardless of the value type
  • ListenableProvider is the specific provider used for listenable objects. It will listen, then ask widgets depending on it and affected by the state change to rebuild any time the listener is called
  • ChangeNotifierProvider is similar to ListenableProvider but for ChangeNotifier objects, and calls ChangeNotifier.dispose automatically when needed
  • ValueListenableProvider listens to a ValueListenable and exposes the value
  • StreamProvider listens to a stream, exposes the latest value emitted, and asks widgets dependent on the stream to rebuild
  • FutureProvider takes a Future class and updates the widgets depending on it when the future is completed

Important points to remember while using provider:

  • Provider is built using widgets. It literally creates new widget subclasses, allowing you to use all the objects in provider as if they’re just part of Flutter. This also means that provider is not cross platform. (By cross platform, I mean outside of a Flutter project. i.e. AngularDart)
  • Separating this business logic makes your code much easier to test and maintain.
  • Again, Provider is not opinionated about state management. It is not going to force consistency like Redux would.

4) Mobx:

Mobx architecture

With the help of the state-management library MobX, you can easily link the reactive data in your application with the user interface (or any observer). It feels quite natural and is entirely automatic. As the application’s developer, you don’t have to worry about keeping the two in sync; instead, you concentrate solely on what reactive data needs to be consumed in the UI (and elsewhere).

Although it isn’t really magic, it does have some intelligence regarding what is being consumed (observables) and where (reactions), and it monitors it automatically for you. All reactions are done again when the observables change. It’s intriguing that these responses might range from a straightforward console log to a network request to re-rendering the user interface.

Principles of MobX

MobX has four principle concepts that we will learn and understand how they work: observablescomputed valuesreactions and actions.

Observables

Observables in MobX allow us to add observable capabilities to our data structures — like classes, objects, arrays — and make our properties observables. This means that our property stores a single value, and whenever the value of that property changes, MobX will keep track of the value of the property for us.

For example, let’s imagine we have a variable called counter. We can easily create an observable by using the @observable decorator, like this:

By using the @observable decorator, we’re telling MobX that we want to keep track of the counter’s value, and every time the counter changes, we’ll get the updated value.

class counter {
@observable
int value = 0;

@computed
int get doubleValue => value * 2;
}

Computed values

In MobX, we can understand computed values as values that can be derived from our state, so the name computed values makes total sense. They are functions derived from our state, so their return values will change whenever our state changes.

You must remember about computed values that the get syntax is required, and the derivation that it makes from your state is automatic, you don’t need to do anything to get the updated value.

class counter {
@observable
int value = 0;

@computed
int get doubleValue => value * 2;
}

Actions

Actions in MobX are a very important concept because they’re responsible for modifying our state. They’re responsible for changing and modifying our observables.

class counter {
@observable
int value = 0;

@action
void increment() {
value++;
}

@action
void decrement() {
value--;
}
}

Reaction

Reaction is similar to autorun, but it gives us more control when tracking observables’ values. It receives two arguments: the first is a simple function to return the data used in the second argument.

The second argument will be the effect function; this effect function will only react to data passed in the first function argument. This effect function will only be triggered when the data you passed in the first argument has changed.

reaction((_) {
return counter.value;
}, (_) {
print('Counter changed to ${counter.value}');
});

5) Riverpod:

Riverpod architecture

Riverpod is a state management library for Flutter apps. It offers a different approach to handle state in Flutter apps and is based on the Provider package. The state’s source of truth, riverpod flutter, allows you to define providers. These providers, which can be scoped to particular widget subtrees or inherited throughout the widget tree, allow for fine-grained control over how the state is accessed and transferred.

It also introduced the concept of ‘Watching’ that allows to watch a provider without triggering rebuilds on all the widgets. This is particularly useful when you have a large number of widgets that depend on the same provider.

By using Riverpod, developers can efficiently handle state changes, segregate UI elements from business logic, and minimise boilerplate code.

Advantages of use Riverpod :

Scalability: Riverpod makes it simple to scale your app as it expands and manage complicated application states.
Performance: Riverpod offers optimised performance with little memory usage and effective widget re-rendering.
Flexibility: Bloc, Redux, and MVVM are just a few of the several Flutter architectures that Riverpod may be utilised with.
Dependency Injection: Riverpod makes it simple to insert dependencies into your application, increasing its modularity and testing efficiency.
Flutter Web and Desktop Support: Riverpod is a fantastic option for cross-platform projects because it features built-in support for Flutter online and desktop applications.

Disadvantages of using Riverpod:

Steep Learning Curve: Due to its complexity and unclear documentation, Riverpod might be difficult for beginners to understand.
not yet commonly used: Despite growing in acceptance among the Flutter community, alternative state management libraries like Provider and GetX are still more extensively used than Riverpod.

6) Redux:

Redux architecture

Data is repetitively distributed between widgets with success using Redux, a state management architectural framework. Through the use of a unidirectional data flow, it controls the state of an application.

Previously, you either had to create singleton classes to store the data or pass all the necessary objects through a whole hierarchy of screens, with both solutions making your application difficult to test and to adapt to future changes. Fortunately, Redux is here to solve those problems for you.

It uses a unidirectional data flow model to create an immutable data stream and follows the logic of asynchronous and reactive programming. It means there’s a single source of truth (called store) which provides the information across your whole application and keeps the data globally accessible. With every modification in the store, a completely new application state is generated and passed to the subscribed components. This way we can make the data immutable between refresh cycles and avoid inconsistent states.

Redux elements

Usually, components are UI elements presenting data on the screen and initiating changes of the application’s state by responding to user interactions (for example, when the user presses the login button or types in its login credentials). Components cannot communicate directly with the global store. Instead, they need to encapsulate the information (in our previous example, a new username value) required for the changes and dispatch them using actions.

1) Components

Usually, components are UI elements presenting data on the screen and initiating changes of the application’s state by responding to user interactions (for example, when the user presses the login button or types in its login credentials). Components cannot communicate directly with the global store. Instead, they need to encapsulate the information (in our previous example, a new username value) required for the changes and dispatch them using actions.

2) Actions

Actions carry the information in their payload and pass it to reducers. In Dart programming language the actions can be represented by different classes. It’s a good practice to keep the actions separated by features into separate files to prevent your objects growing too large.

3) Reducer

Reducers receive the information through actions and implement the business logic layer to handle data and generate the next state of the application. It’s important to remember that the reducers always create a new copy of the state, keeping it immutable between the refresh cycles.

4) Middleware

Middleware adds more layers to your application by intercepting the actions before they reach the reducers and do additional data processing. Middleware provides better code separation and makes the layers easy to replace. For example, if you‘re using a database engine in your application you’ll be able to only swap the DB related parts in the future without touching any other parts of the code. Good examples of popular middleware are loggers, databases, thunks or sagas.

5) Store

The store represents the current global state of your application. It can only be changed by the reducers and when it happens the components which are observing particular values in the store will be automatically notified.

Comments

Popular Posts