When building Flutter apps, you need to manage the state when the apps are running. The state may change due to user interactions or background tasks. This chapter covers recipes that use different solutions for state management in Flutter.
10.1 Managing State Using Stateful Widgets
Problem
You want to have a simple way to manage state in the UI.
Solution
Create your own subclasses of StatefulWidget.
Discussion
StatefulWidget class is the fundamental way in Flutter to manage state. A stateful widget rebuilds itself when its state changes. If the state to manage is simple, using stateful widgets is generally good enough. You don’t need to use third-party libraries discussed in other recipes.
Stateful widgets use State objects to store the state. When creating your own subclasses of StatefulWidget, you need to override createState() method to return a State object. For each subclass StatefulWidget, there will be a corresponding subclass of State class to manage the state. The createState() method returns an object of the corresponding subclass of State. The actual state is usually kept as private variables of the subclass of State.
In the subclass of State, you need to implement build() method to return a Widget object. When the state changes, the build() method will be called to get the new widget to update the UI. To trigger the rebuild of the UI, you need to call setState() method explicitly to notify the framework. The parameter of setState() method is a VoidCallback function that contains the logic to update the internal state. When rebuilding, the build() method uses the latest state to create widget configurations. Widgets are not updated but replaced when necessary.
Example of stateful widget
Lifecycle methods of State
Name | Description |
---|---|
initState() | Called when this object is inserted into the widgets tree. Should be used to perform initialization of state. |
didChangeDependencies() | Called when a dependency of this object changes. |
didUpdateWidget(T oldWidget) | Called when the widget of this object changes. Old widget is passed as a parameter. |
reassemble() | Called when the app is reassembled during debugging. This method is only called during development. |
build(BuildContext context) | Called when the state changes. |
deactivate() | Called when this object is removed from the widgets tree. |
dispose() | Called when this object is removed from the widgets tree permanently. This method is called after deactivate(). |
Of the methods listed in Table 10-1, initState() and dispose() methods are easy to understand. These two methods will only be called once during the lifecycle. However, other methods may be invoked multiple times.
The didChangeDependencies() method is typically used when the state object uses inherited widgets. This method is called when an inherited widget changes. Most of the time, you don’t need to override this method, because the framework calls build() method automatically after a dependency changes. Sometimes you may need to perform some expensive tasks after a dependency changes. In this case, you should put the logic into didChangeDependencies() method instead of performing the task in build() method.
The reassemble() method is only used during development, for example, during hot reload. This method is not called in release builds. Most of the time, you don’t need to override this method.
The didUpdateWidget() method is called when the state’s widget changes. You should override this method if you need to perform cleanup tasks on the old widget or reuse some state from the old widget. For example, _TextFieldState class for TextField widget overrides didUpdateWidget() method to initialize TextEditingController object based on the value of the old widget.
The deactivate() method is called when the state object is removed from the widgets tree. This state object may be inserted back to the widgets tree at a different location. You should override this method if the build logic depends on the widget’s location. For example, FormFieldState class for FormField widget overrides deactivate() method to unregister the current form field from the enclosing form.
Pass state change function to descendant widget
10.2 Managing State Using Inherited Widgets
Problem
You want to propagate state down the widgets tree.
Solution
Create your own subclasses of InheritedWidget.
Discussion
When using stateful widgets to manage state, the state is stored in State objects. If a descendant widget needs to access the state, the state needs to be passed down to it from the root of subtree, just like how count state is passed in Listing 10-2. When the widget has a relatively deep subtree structure, it’s inconvenient to add constructor parameters for passing the state down. In this case, using InheritedWidget is a better choice.
When InheritedWidget is used, the method BuildContext.inheritFromWidgetOfExactType() can get the nearest instance of a particular type of inherited widget from the build context. Descendant widgets can easily access state data stored in an inherited widget. When inheritFromWidgetOfExactType() method is called, the build context registers itself to the inherited widget. When the inherited widget changes, the build context is rebuilt automatically to get the new values from the inherited widget. This means no manual updates are required for descendant widgets that use state from the inherited widget.
Config class for inherited widget
ConfigWidget as inherited widget
Use ConfigWidget to get the Config object
ConfigUpdater to update Config object
ConfiguredPage to use ConfigWidget
ConfigWidgetPage to build the UI
10.3 Managing State Using Inherited Model
Problem
You want to get notified and rebuild UI based on aspects of changes.
Solution
Create your own subclasses of InheritedModel.
Discussion
If we take a closer look at the ConfiguredText and ConfiguredBox widgets in Listing 10-5 of Recipe 10-2, we can see that ConfiguredBox widget only depends on the color property of the Config object. If the fontSize property changes, there is no need for ConfiguredBox widget to rebuild. These unnecessary rebuilds may cause performance issues, especially if the widget is complex.
InheritedModel widget allows you to divide a state into multiple aspects. A build context can register to get notified only for a particular aspect. When state changes in InheritedModel widget, only dependent build contexts registered to matching aspects will be notified.
InheritedModel class extends from InheritedWidget class. It has a type parameter to specify the type of aspect. ConfigModel class in Listing 10-9 is the InheritedModel subclass for Config object. The type of aspect is String. When implementing InheritedModel class, you still need to override updateShouldNotify() method to determine whether dependents should be notified. The updateShouldNotifyDependent() method determines whether a dependent should be notified based on the set of aspects it depends on. The updateShouldNotifyDependent() method is only called when updateShouldNotify() method returns true. For the ConfigModel, only “color” and “fontSize” aspects are defined. If the dependent depends on the “color” aspect, then it’s notified only when the color property of Config object changes. This is also applied to “fontSize” aspect for fontSize property.
ConfigModel as InheritedModel
Use ConfigModel to get Config object
10.4 Managing State Using Inherited Notifier
Problem
You want dependent widgets to rebuild based on notifications from Listenable objects .
Solution
Create your own subclasses of InheritedNotifier widget.
Discussion
Listenable class is typically used to manage listeners and notify clients for updates. You can use the same pattern to notify dependents to rebuild with InheritedNotifier. InheritedNotifier widget also extends from InheritedWidget class. When creating InheritedNotifier widgets, you need to provide Listenable objects. When the Listenable object sends notifications, dependents of this InheritedNotifier widget are notified for rebuilding.
ConfigNotifier as InheritedNotifier
ConfiguredNotifierPage to use ConfigNotifier
10.5 Managing State Using Scoped Model
Problem
You want to have a simple solution to handle model changes.
Solution
Use scoped_model package.
Discussion
In Recipes 10-1, 10-2, 10-3, and 10-4, you have seen the usage of StatefulWidget, InheritedWidget, InheritedModel, and InheritedNotifier widgets to manage state. These widgets are provided by Flutter framework. These widgets are low-level APIs, so they are inconvenient to use in complex apps. The scoped_model package ( https://pub.dev/packages/scoped_model ) is a library to allow easily passing a data model from a parent widget down to its descendants. It’s built on top of InheritedWidget, but with an easy-to-use API. To use this package, you need to add scoped_model: ^1.0.1 to the dependencies of pubspec.yaml file. We’ll use the same example as in Recipe 10-2 to demonstrate the usage of scoped_model package.
Config model as scoped model
ScopedModelText uses ScopedModelDescendant
ScopedModelUpdater to update Config object
ScopedModelPage uses ScopedModel
You can also use static ScopedModel.of() method to get the ScopedModel object , then use its model property to get the model object.
10.6 Managing State Using Bloc
Problem
You want to use Bloc pattern to manage state.
Solution
Use bloc and flutter_bloc packages .
Discussion
Bloc (Business Logic Component) is an architecture pattern to separate presentation from business logic. Bloc was designed to be simple, powerful, and testable. Let’s start from core concepts in Bloc.
States represent a part of the application’s state. When state changes, UI widgets are notified to rebuild based on the latest state. Each application has its own way to define states. Typically, you’ll use Dart classes to describe states.
Events are sources of changes to states. Events can be generated by user interactions or background tasks. For example, pressing a button may generate an event that describes the intended action. When the response of a HTTP request is ready, an event can also be generated to include the response body. Events are typically described as Dart classes. Events may also have payload carried with them.
When events are dispatched, handling these events may cause the current state transits to a new state. UI widgets are then notified to rebuild using the new state. An event transition consists of the current state, the event, and the next state. If all state transitions are recorded, we can easily track all user interactions and state changes. We can also implement time-travelling debugging.
Now we can have a definition of Bloc. A Bloc component transforms a stream of events into a stream of states. A Bloc has an initial state as the state before any events are received. For each event, a Bloc has a mapEventToState() function that takes a received event and returns a stream of states to be consumed by the presentation layer. A Bloc also has the dispatch() method to dispatch events to it.
In this recipe, we’ll use the GitHub Jobs API ( https://jobs.github.com/api ) to get job listings on GitHub. The user can input a keyword for search and see the results. To consume this, we will be using the http package ( https://pub.dev/packages/http ). Add this package to your pubspec.yaml file.
Bloc states
Bloc events
Bloc
GitHub jobs widget using Bloc
10.7 Managing State Using Redux
Problem
You want to use Redux as the state management solution.
Solution
Use redux and flux_redux packages.
Discussion
Redux ( https://redux.js.org/ ) is a popular library to manage state in apps. Originated for React, Redux has been ported to different languages. The redux package is a Dart implementation of Redux. The flux_redux package allows using Redux store when building Flutter widgets. If you have used Redux before, the same concepts are used in Flutter.
Redux uses a single global object as the state. This object is the single source of truth for the app, and it’s called the store. Actions are dispatched to the store to update the state. Reducer functions accept the current state and an action as the parameters and return the next state. The next state becomes the input of the next run of the reducer function. UI widgets can select partial data from the store to build the content.
To use flutter_redux package, you need to add flutter_redux: ^0.5.3 to the dependencies of pubspec.yaml file. We’ll use the same example of listing jobs on GitHub to demonstrate the usage of Redux in Flutter.
JobsState for Redux
Actions for Redux
Reducer function for Redux
Actions defined in Listing 10-22 can only be used for synchronous operations. For example, if you want to dispatch the JobLoadedAction, you need to have the List<Job> object ready first. However, the operation to load jobs data is asynchronous. You’ll need to use thunk functions as the middleware of Redux store. A thunk function takes the store as the only parameter. It uses the store to dispatch actions. A thunk action can be dispatched to the store, just like other normal actions.
Thunk function for Redux
GitHub jobs widget using Redux store
Create the store
10.8 Managing State Using Mobx
Problem
You want to use Mobx to manage state.
Solution
Use mobx and flutter_mobx packages.
Discussion
Mobx ( https://mobx.js.org ) is a state management library which connects reactive data with the UI. MobX originates from developing web apps using JavaScript. It’s also ported to Dart ( https://mobx.pub ). In Flutter apps, we can use mobx and flutter_mobx packages to build apps with Mobx. Mobx for Flutter uses build_runner package to generate code for the store. The build_runner and mobx_codegen packages need to be added as dev_dependencies to pubspec.yaml file.
Mobx uses observables to manage the state. The whole state of an app consists of core state and derived state. Derived state is computed from core state. Actions mutate observables to update the state. Reactions are observers of the state and get notified whenever an observable they track is changed. In Flutter app, the reactions are used to update the widgets.
Comparing to Redux for Flutter, Mobx uses code generation to simplify the usage of store. You don’t need to write boilerplate code to create actions. Mobx provides several annotations. You just annotate the code with these annotations. This is similar with how json_annotation and json_serialize packages work. We’ll use the same example of showing job listings on GitHub to demonstrate the usage of Mobx. Add this package to your pubspec.yaml file if it is not already present.
Mobx store
GitHub jobs widget using Mobx store
10.9 Summary
This chapter discusses different state management solutions for Flutter apps. In these solutions, StatefulWidget, InheritedWidget, InheritedModel, and InheritedNotifier widgets are provided by Flutter framework. Scoped model, Bloc, Redux, and Mobx libraries are third-party solutions. You are free to choose whatever solution that suits best for your requirement. In the next chapter, we’ll discuss animations in Flutter.