© Fu Cheng 2019
F. ChengFlutter Recipeshttps://doi.org/10.1007/978-1-4842-4982-6_10

10. State Management

Fu Cheng1 
(1)
Sandringham, Auckland, New Zealand
 

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.

SelectColor widget in Listing 10-1 is a typical example of stateful widget. _SelectColorState class is the State implementation for SelectColor widget. _selectedColor is the internal variable that maintains the current selected color. The value of _selectedColor is used by the DropdownButton widget to determine the selected option to render and the Text widget to determine the text to display. In the onChanged handler of DropdownButton, setState() method is called to update the value of _selectedColor variable, which notifies the framework to run _SelectColorState.build() method again to get the new widget configuration to update the UI.
class SelectColor extends StatefulWidget {
  @override
  _SelectColorState createState() => _SelectColorState();
}
class _SelectColorState extends State<SelectColor> {
  final List<String> _colors = ['Red', 'Green', 'Blue'];
  String _selectedColor;
  @override
  Widget build(BuildContext context) {
    return Column(
      children: <Widget>[
        DropdownButton(
          value: _selectedColor,
          items: _colors.map((String color) {
            return DropdownMenuItem(
              value: color,
              child: Text(color),
            );
          }).toList(),
          onChanged: (value) {
            setState(() {
              _selectedColor = value;
            });
          },
        ),
        Text('Selected: ${_selectedColor ?? "}'),
      ],
    );
  }
}
Listing 10-1

Example of stateful widget

State objects have their own lifecycle. You can override different lifecycle methods in subclasses of State to perform actions on different stages. Table 10-1 shows these lifecycle methods.
Table 10-1

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.

In Listing 10-1, the whole content of the widget is built in the build() method, so you can simply call setState() method in the onPressed callback of DropdownButton. If the widget has a complex structure, you can pass down a function that updates the state to the children widgets. In Listing 10-2, the onPressed callback of RaisedButton is set by the constructor parameter of CounterButton. When the CounterButton is used in Counter widget, the provided handler function uses setState() to update the state.
class Counter extends StatefulWidget {
  @override
  _CounterState createState() => _CounterState();
}
class _CounterState extends State<Counter> {
  int count = 0;
  @override
  Widget build(BuildContext context) {
    return Column(
      children: <Widget>[
        CounterButton(() {
          setState(() {
            count++;
          });
        }),
        CounterText(count),
      ],
    );
  }
}
class CounterText extends StatelessWidget {
  CounterText(this.count);
  final int count;
  @override
  Widget build(BuildContext context) {
    return Text('Value: ${count ?? "}');
  }
}
class CounterButton extends StatelessWidget {
  CounterButton(this.onPressed);
  final VoidCallback onPressed;
  @override
  Widget build(BuildContext context) {
    return RaisedButton(
      child: Text('+'),
      onPressed: onPressed,
    );
  }
}
Listing 10-2

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.

The Config class in Listing 10-3 represents the state. It has color and fontSize properties. Config class overrides == operator and hashCode property to implement correct equality check. The copyWith() method can be used to create new instances of Config class by updating a partial set of properties. The Config.fallback() constructor creates a Config object with default values.
class Config {
  const Config({this.color, this.fontSize});
  const Config.fallback()
      : color = Colors.red,
        fontSize = 12.0;
  final Color color;
  final double fontSize;
  Config copyWith({Color color, double fontSize}) {
    return Config(
      color: color ?? this.color,
      fontSize: fontSize ?? this.fontSize,
    );
  }
  @override
  bool operator ==(other) {
    if (other.runtimeType != runtimeType) return false;
    final Config typedOther = other;
    return color == typedOther.color && fontSize == typedOther.fontSize;
  }
  @override
  int get hashCode => hashValues(color, fontSize);
}
Listing 10-3

Config class for inherited widget

The ConfigWidget in Listing 10-4 is an inherited widget. It keeps a Config object as its internal state. The updateShouldNotify() method is called to check whether registered build contexts should be notified after the inherited widget changes. This is a performance optimization to avoid unnecessary updates. The static of() method is a common practice to get the inherited widget or the state associated with the inherited widget. The of() method of ConfigWidget uses inheritFromWidgetOfExactType() to get the nearest enclosing ConfigWidget instance from build context and gets config property from the widget. If no ConfigWidget object is found, the default Config instance is returned.
class ConfigWidget extends InheritedWidget {
  const ConfigWidget({
    Key key,
    @required this.config,
    @required Widget child,
  }) : super(key: key, child: child);
  final Config config;
  static Config of(BuildContext context) {
    final ConfigWidget configWidget =
        context.inheritFromWidgetOfExactType(ConfigWidget);
    return configWidget?.config ?? const Config.fallback();
  }
  @override
  bool updateShouldNotify(ConfigWidget oldWidget) {
    return config != oldWidget.config;
  }
}
Listing 10-4

ConfigWidget as inherited widget

In Listing 10-5, both ConfiguredText and ConfiguredBox widgets use ConfigWidget.of(context) to get the Config object and use its properties when building the UI.
class ConfiguredText extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    Config config = ConfigWidget.of(context);
    return Text(
      'Font size: ${config.fontSize}',
      style: TextStyle(
        color: config.color,
        fontSize: config.fontSize,
      ),
    );
  }
}
class ConfiguredBox extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    Config config = ConfigWidget.of(context);
    return Container(
      decoration: BoxDecoration(color: config.color),
      child: Text('Background color: ${config.color}'),
    );
  }
}
Listing 10-5

Use ConfigWidget to get the Config object

ConfigUpdater widget in Listing 10-6 is used to update the Config object. It also uses ConfigWidget.of(context) to get the Config object to update. The onColorChanged and onFontSizeIncreased callbacks are used to trigger update of Config object.
typedef SetColorCallback = void Function(Color color);
class ConfigUpdater extends StatelessWidget {
  const ConfigUpdater({this.onColorChanged, this.onFontSizeIncreased});
  static const List<Color> _colors = [Colors.red, Colors.green, Colors.blue];
  final SetColorCallback onColorChanged;
  final VoidCallback onFontSizeIncreased;
  @override
  Widget build(BuildContext context) {
    Config config = ConfigWidget.of(context);
    return Column(
      children: <Widget>[
        DropdownButton(
          value: config.color,
          items: _colors.map((Color color) {
            return DropdownMenuItem(
              value: color,
              child: Text(color.toString()),
            );
          }).toList(),
          onChanged: onColorChanged,
        ),
        RaisedButton(
          child: Text('Increase font size'),
          onPressed: onFontSizeIncreased,
        )
      ],
    );
  }
}
Listing 10-6

ConfigUpdater to update Config object

Now we can put these widgets together to build the whole UI. In Listing 10-7, ConfiguredPage is a stateful widget with a Config object as its state. ConfigUpdater widget is a child of ConfiguredPage to update the Config object. ConfiguredPage constructor also has child parameter to provide child widget that uses ConfigWidget.of(context) to get the correct Config object. For the onColorChanged and onFontSizeIncreased callbacks of ConfigWidget, setState() method is used to update the state of ConfiguredPage widget and triggers update of ConfigWidget. The framework notifies ConfigUpdater and other widgets to update with latest value of Config object.
class ConfiguredPage extends StatefulWidget {
  ConfiguredPage({Key key, this.child}) : super(key: key);
  final Widget child;
  @override
  _ConfiguredPageState createState() => _ConfiguredPageState();
}
class _ConfiguredPageState extends State<ConfiguredPage> {
  Config _config = Config(color: Colors.green, fontSize: 16);
  @override
  Widget build(BuildContext context) {
    return ConfigWidget(
      config: _config,
      child: Column(
        children: <Widget>[
          ConfigUpdater(
            onColorChanged: (Color color) {
              setState(() {
                _config = _config.copyWith(color: color);
              });
            },
            onFontSizeIncreased: () {
              setState(() {
                _config = _config.copyWith(fontSize: _config.fontSize + 1.0);
              });
            },
          ),
          Container(
            decoration: BoxDecoration(border: Border.all()),
            padding: EdgeInsets.all(8),
            child: widget.child,
          ),
        ],
      ),
    );
  }
}
Listing 10-7

ConfiguredPage to use ConfigWidget

In Listing 10-8, ConfigWidgetPage widget uses ConfiguredPage widget to wrap ConfiguredText and ConfiguredBox widgets.
class ConfigWidgetPage extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: Text('Inherited Widget'),
      ),
      body: ConfiguredPage(
        child: Column(
          children: <Widget>[
            ConfiguredText(),
            ConfiguredBox(),
          ],
        ),
      ),
    );
  }
}
Listing 10-8

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.

The static of() method has an extra aspect parameter to specify the aspect the build context depends on. The static InheritedModel.inheritFrom() method is used to make the build context depend on specified aspect. When aspect is null, this method is the same as using BuildContext.inheritFromWidgetOfExactType() method .
class ConfigModel extends InheritedModel<String> {
  const ConfigModel({
    Key key,
    @required this.config,
    @required Widget child,
  }) : super(key: key, child: child);
  final Config config;
  static Config of(BuildContext context, String aspect) {
    ConfigModel configModel =
        InheritedModel.inheritFrom(context, aspect: aspect);
    return configModel?.config ?? Config.fallback();
  }
  @override
  bool updateShouldNotify(ConfigModel oldWidget) {
    return config != oldWidget.config;
  }
  @override
  bool updateShouldNotifyDependent(
      ConfigModel oldWidget, Set<String> dependencies) {
    return (config.color != oldWidget.config.color &&
            dependencies.contains('color')) ||
        (config.fontSize != oldWidget.config.fontSize &&
            dependencies.contains('fontSize'));
  }
}
Listing 10-9

ConfigModel as InheritedModel

In Listing 10-10, ConfiguredModelText widget uses null as the aspect, because it depends on both “color” and “fontSize” aspects. ConfiguredModelBox widget uses color as the aspect. If font size is updated, only ConfiguredModelText widget is rebuilt.
class ConfiguredModelText extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    Config config = ConfigModel.of(context, null);
    return Text(
      'Font size: ${config.fontSize}',
      style: TextStyle(
        color: config.color,
        fontSize: config.fontSize,
      ),
    );
  }
}
class ConfiguredModelBox extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    Config config = ConfigModel.of(context, 'color');
    return Container(
      decoration: BoxDecoration(color: config.color),
      child: Text('Background color: ${config.color}'),
    );
  }
}
Listing 10-10

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.

In Listing 10-11, ConfigNotifier uses ValueNotifier<Config> as the type of Listenable. The static of() method gets the Config object from ConfigNotifier object.
class ConfigNotifier extends InheritedNotifier<ValueNotifier<Config>> {
  ConfigNotifier({
    Key key,
    @required notifier,
    @required Widget child,
  }) : super(key: key, notifier: notifier, child: child);
  static Config of(BuildContext context) {
    final ConfigNotifier configNotifier =
        context.inheritFromWidgetOfExactType(ConfigNotifier);
    return configNotifier?.notifier?.value ?? Config.fallback();
  }
}
Listing 10-11

ConfigNotifier as InheritedNotifier

To use ConfigNotifier widget, you need to create a new instance of ValueNotifier<Config>. To update the Config object, you can simply set the value property to a new value. ValueNotifier object will send notifications, which notify dependent widgets to rebuild.
class ConfiguredNotifierPage extends StatelessWidget {
  ConfiguredNotifierPage({Key key, this.child}) : super(key: key);
  final Widget child;
  final ValueNotifier<Config> _notifier =
      ValueNotifier(Config(color: Colors.green, fontSize: 16));
  @override
  Widget build(BuildContext context) {
    return ConfigNotifier(
      notifier: _notifier,
      child: Column(
        children: <Widget>[
          ConfigUpdater(
            onColorChanged: (Color color) {
              _notifier.value = _notifier.value.copyWith(color: color);
            },
            onFontSizeIncreased: () {
              Config oldConfig = _notifier.value;
              _notifier.value =
                  oldConfig.copyWith(fontSize: oldConfig.fontSize + 1.0);
            },
          ),
          Container(
            decoration: BoxDecoration(border: Border.all()),
            padding: EdgeInsets.all(8),
            child: child,
          ),
        ],
      ),
    );
  }
}
Listing 10-12

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.

Listing 10-13 shows the Config model using scoped_model package. The Config class extends from Model class. It has private fields to store the state. The setColor() and increaseFontSize() methods update _color and _fontSize fields, respectively. These two methods use notifyListeners() internally to notify descendant widgets to rebuild.
import 'package:scoped_model/scoped_model.dart';
class Config extends Model {
  Color _color = Colors.red;
  double _fontSize = 16.0;
  Color get color => _color;
  double get fontSize => _fontSize;
  void setColor(Color color) {
    _color = color;
    notifyListeners();
  }
  void increaseFontSize() {
    _fontSize += 1;
    notifyListeners();
  }
}
Listing 10-13

Config model as scoped model

In Listing 10-14, ScopedModelText widget shows how to use the model in descendant widgets. ScopedModelDescendant widget is used to get the nearest enclosing model object. The type parameter determines the model object to get. The builder parameter specified the build function to build the widget. The build function has three parameters. The first parameter of type BuildContext is common for build functions. The last parameter is the model object. If a portion of the widget UI doesn’t rely on the model and should not be rebuilt when model changes, you can specify it as the child parameter of ScopedModelDescendant widget and access it in the second parameter of the build function.
class ScopedModelText extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return ScopedModelDescendant<Config>(
      builder: (BuildContext context, Widget child, Config config) {
        return Text(
          'Font size: ${config.fontSize}',
          style: TextStyle(
            color: config.color,
            fontSize: config.fontSize,
          ),
        );
      },
    );
  }
}
Listing 10-14

ScopedModelText uses ScopedModelDescendant

In Listing 10-15, ScopedModelUpdater widget simply uses setColor() and increaseFontSize() methods to update the state.
class ScopedModelUpdater extends StatelessWidget {
  static const List<Color> _colors = [Colors.red, Colors.green, Colors.blue];
  @override
  Widget build(BuildContext context) {
    return ScopedModelDescendant<Config>(
      builder: (BuildContext context, Widget child, Config config) {
        return Column(
          children: <Widget>[
            DropdownButton(
              value: config.color,
              items: _colors.map((Color color) {
                return DropdownMenuItem(
                  value: color,
                  child: Text(color.toString()),
                );
              }).toList(),
              onChanged: (Color color) {
                config.setColor(color);
              },
            ),
            RaisedButton(
              child: Text('Increase font size'),
              onPressed: () {
                config.increaseFontSize();
              },
            )
          ],
        );
      },
    );
  }
}
Listing 10-15

ScopedModelUpdater to update Config object

ScopedModel widget in Listing 10-16 is the last piece to put Model and ScopedModelDescendant together. The model parameter specifies the model object managed by the ScopedModel object. All the ScopedModelDescendant widgets under the ScopedModel object get the same model object.
class ScopedModelPage extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: Text('Scoped Model'),
      ),
      body: ScopedModel(
        model: Config(),
        child: Column(
          children: <Widget>[
            ScopedModelUpdater(),
            ScopedModelText()
          ],
        ),
      ),
    );
  }
}
Listing 10-16

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.

Let’s start from the states. Listing 10-17 shows classes for different states. JobsState is the abstract base class for all state classes. JobsState class extends from Equatable class in the equatable package. Equatable class is used to provide implantations for == operator and hashCode property. JobsEmpty is the initial state. JobsLoading means the job listing data is still loading. JobsLoaded means job listing data is loaded. The payload type of JobsLoaded event is List<Job>. JobsError means an error occurred when fetching the data.
import 'package:http/http.dart' as http;
abstract class JobsState extends Equatable {
  JobsState([List props = const []]) : super(props);
}
class JobsEmpty extends JobsState {}
class GetJobsEvent extends JobsEvent {
  GetJobsEvent({@required this.keyword})
      : assert(keyword != null),
        super([keyword]);
  final String keyword;
}
class GitHubJobsClient {
  Future<List<Job>> getJobs(keyword) async {
    final response = await http.get('https://jobs.github.com/positions.json?description=${keyword}');
    if (response.statusCode != 200) {
      throw new Exception("Unable to fetch data");
    }else{
      var result = new List<Job>();
      final rawResult = json.decode(response.body);
      for(final jsonJob in rawResult){
        result.add(Job.fromJson(jsonJob));
      }
    }
  }
}
class JobsLoading extends JobsState {}
class JobsLoaded extends JobsState {
  JobsLoaded({@required this.jobs})
      : assert(jobs != null),
        super([jobs]);
  final List<Job> jobs;
}
class JobsError extends JobsState {}
Listing 10-17

Bloc states

Listing 10-18 shows the events. JobsEvent is the abstract base class for event classes. GetJobsEvent class represents the event to get jobs data.
abstract class JobsEvent extends Equatable {
  JobsEvent([List props = const []]) : super(props);
}
class GetJobsEvent extends JobsEvent {
  GetJobsEvent({@required this.keyword})
      : assert(keyword != null),
        super([keyword]);
  final String keyword;
}
Listing 10-18

Bloc events

Listing 10-19 shows the Bloc. JobsBloc class extends from Bloc<JobsEvent, JobsState> class. Type parameters of Bloc are event and state classes. JobsEmpty is the initial state. In the mapEventToState() method, if the event is GetJobsEvent, a JobsLoading state is emitted first to the stream. Then GitHubJobsClient object is used to fetch the data. If the data is fetched successfully, a JobsLoaded state is emitted with the loaded data. Otherwise, a JobsError state is emitted instead.
class JobsBloc extends Bloc<JobsEvent, JobsState> {
  JobsBloc({@required this.jobsClient}) : assert(jobsClient != null);
  final GitHubJobsClient jobsClient;
  @override
  JobsState get initialState => JobsEmpty();
  @override
  Stream<JobsState> mapEventToState(JobsEvent event) async* {
    if (event is GetJobsEvent) {
      yield JobsLoading();
      try {
        List<Job> jobs = await jobsClient.getJobs(event.keyword);
        yield JobsLoaded(jobs: jobs);
      } catch (e) {
        yield JobsError();
      }
    }
  }
}
Listing 10-19

Bloc

GitHubJobs class in Listing 10-20 is the widget to use the JobsBloc class in Listing 10-19. The JobsBloc object is created in initState() method and disposed in dispose() method . In the KeywordInput widget, when user inputs the keyword in the text field and presses the search button, a GetJobsEvent is dispatched to the JobsBloc object. In the JobsView widget, BlocBuilder widget is used to build UI based on the state in the Bloc. Here we check the actual type of JobsState and return different widgets.
class GitHubJobs extends StatefulWidget {
  GitHubJobs({Key key, @required this.jobsClient})
      : assert(jobsClient != null),
        super(key: key);
  final GitHubJobsClient jobsClient;
  @override
  _GitHubJobsState createState() => _GitHubJobsState();
}
class _GitHubJobsState extends State<GitHubJobs> {
  JobsBloc _jobsBloc;
  @override
  void initState() {
    super.initState();
    _jobsBloc = JobsBloc(jobsClient: widget.jobsClient);
  }
  @override
  Widget build(BuildContext context) {
    return Column(
      children: <Widget>[
        Padding(
          padding: const EdgeInsets.all(8.0),
          child: KeywordInput(
            jobsBloc: _jobsBloc,
          ),
        ),
        Expanded(
          child: JobsView(
            jobsBloc: _jobsBloc,
          ),
        ),
      ],
    );
  }
  @override
  void dispose() {
    _jobsBloc.dispose();
    super.dispose();
  }
}
class KeywordInput extends StatefulWidget {
  KeywordInput({this.jobsBloc});
  final JobsBloc jobsBloc;
  @override
  _KeywordInputState createState() => _KeywordInputState();
}
class _KeywordInputState extends State<KeywordInput> {
  final GlobalKey<FormFieldState<String>> _keywordFormKey = GlobalKey();
  @override
  Widget build(BuildContext context) {
    return Row(
      children: <Widget>[
        Expanded(
          child: TextFormField(
            key: _keywordFormKey,
          ),
        ),
        IconButton(
          icon: Icon(Icons.search),
          onPressed: () {
            String keyword = _keywordFormKey.currentState?.value ?? ";
            if (keyword.isNotEmpty) {
              widget.jobsBloc.dispatch(GetJobsEvent(keyword: keyword));
            }
          },
        ),
      ],
    );
  }
}
class JobsView extends StatelessWidget {
  JobsView({this.jobsBloc});
  final JobsBloc jobsBloc;
  @override
  Widget build(BuildContext context) {
    return BlocBuilder(
      bloc: jobsBloc,
      builder: (BuildContext context, JobsState state) {
        if (state is JobsEmpty) {
          return Center(
            child: Text('Input keyword and search'),
          );
        } else if (state is JobsLoading) {
          return Center(
            child: CircularProgressIndicator(),
          );
        } else if (state is JobsError) {
          return Center(
            child: Text(
              'Failed to get jobs',
              style: TextStyle(color: Colors.red),
            ),
          );
        } else if (state is JobsLoaded) {
          return JobsList(state.jobs);
        }
      },
    );
  }
}
Listing 10-20

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.

Let’s start from the state. JobsState class in Listing 10-21 represents the global state. The state has three properties, loading represents whether the data is still loading, error represents whether an error occurred when loading the data, and data presents the list of data. By using the copyWith() method, we can new JobsState objects by updating some properties.
class JobsState extends Equatable {
  JobsState({bool loading, bool error, List<Job> data})
      : _loading = loading,
        _error = error,
        _data = data,
        super([loading, error, data]);
  final bool _loading;
  final bool _error;
  final List<Job> _data;
  bool get loading => _loading ?? false;
  bool get error => _error ?? false;
  List<Job> get data => _data ?? [];
  bool get empty => _loading == null && _error == null && _data == null;
  JobsState copyWith({bool loading, bool error, List<Job> data}) {
    return JobsState(
      loading: loading ?? this._loading,
      error: error ?? this._error,
      data: data ?? this._data,
    );
  }
}
Listing 10-21

JobsState for Redux

Listing 10-22 shows the actions. These actions trigger state changes.
abstract class JobsAction extends Equatable {
  JobsAction([List props = const []]) : super(props);
}
class LoadJobAction extends JobsAction {
  LoadJobAction({@required this.keyword})
      : assert(keyword != null),
        super([keyword]);
  final String keyword;
}
class JobLoadedAction extends JobsAction {
  JobLoadedAction({@required this.jobs})
      : assert(jobs != null),
        super([jobs]);
  final List<Job> jobs;
}
class JobLoadErrorAction extends JobsAction {}
Listing 10-22

Actions for Redux

Listing 10-23 shows the reducer function to update state according to the action.
JobsState jobsReducers(JobsState state, dynamic action) {
  if (action is LoadJobAction) {
    return state.copyWith(loading: true);
  } else if (action is JobLoadErrorAction) {
    return state.copyWith(loading: false, error: true);
  } else if (action is JobLoadedAction) {
    return state.copyWith(loading: false, data: action.jobs);
  }
  return state;
}
Listing 10-23

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.

The getJobs() function in Listing 10-24 takes a GitHubJobsClient object and a search keyword as the parameters. This function returns a thunk function of type ThunkAction<JobsState>. ThunkAction comes from redux_thunk package. In the thunk function, a LoadJobAction is dispatched first. Then GitHubJobsClient object is used to get the jobs data. Depending on the result of data loading, a JobLoadedAction or JobLoadErrorAction is dispatched.
ThunkAction<JobsState> getJobs(GitHubJobsClient jobsClient, String keyword) {
  return (Store<JobsState> store) async {
    store.dispatch(LoadJobAction(keyword: keyword));
    try {
      List<Job> jobs = await jobsClient.getJobs(keyword);
      store.dispatch(JobLoadedAction(jobs: jobs));
    } catch (e) {
      store.dispatch(JobLoadErrorAction());
    }
  };
}
Listing 10-24

Thunk function for Redux

Now we can use the Redux store to build the widgets. You can use two helper widgets to access data in the store. In Listing 10-25, StoreBuilder widget is used to provide direct access to the store. The store is available as the second parameter of the build function. StoreBuilder widget is usually used when you need to dispatch actions. StoreConnector widget allows using a converter function to transform the state first. When the search icon is pressed, the getJobs() function in Listing 10-24 is called first to create the thunk function, then dispatches the thunk function to the store. When using StoreConnector widget, the converter function simply gets the current state from the store. The state object is then used in build function.
class GitHubJobs extends StatefulWidget {
  GitHubJobs({
    Key key,
    @required this.store,
    @required this.jobsClient,
  })  : assert(store != null),
        assert(jobsClient != null),
        super(key: key);
  final Store<JobsState> store;
  final GitHubJobsClient jobsClient;
  @override
  _GitHubJobsState createState() => _GitHubJobsState();
}
class _GitHubJobsState extends State<GitHubJobs> {
  @override
  Widget build(BuildContext context) {
    return StoreProvider<JobsState>(
      store: widget.store,
      child: Column(
        children: <Widget>[
          Padding(
            padding: const EdgeInsets.all(8.0),
            child: KeywordInput(
              jobsClient: widget.jobsClient,
            ),
          ),
          Expanded(
            child: JobsView(),
          ),
        ],
      ),
    );
  }
}
class KeywordInput extends StatefulWidget {
  KeywordInput({this.jobsClient});
  final GitHubJobsClient jobsClient;
  @override
  _KeywordInputState createState() => _KeywordInputState();
}
class _KeywordInputState extends State<KeywordInput> {
  final GlobalKey<FormFieldState<String>> _keywordFormKey = GlobalKey();
  @override
  Widget build(BuildContext context) {
    return Row(
      children: <Widget>[
        Expanded(
          child: TextFormField(
            key: _keywordFormKey,
          ),
        ),
        StoreBuilder<JobsState>(
          builder: (BuildContext context, Store<JobsState> store) {
            return IconButton(
              icon: Icon(Icons.search),
              onPressed: () {
                String keyword = _keywordFormKey.currentState?.value ?? ";
                if (keyword.isNotEmpty) {
                  store.dispatch(getJobs(widget.jobsClient, keyword));
                }
              },
            );
          },
        ),
      ],
    );
  }
}
class JobsView extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return StoreConnector<JobsState, JobsState>(
      converter: (Store<JobsState> store) => store.state,
      builder: (BuildContext context, JobsState state) {
        if (state.empty) {
          return Center(
            child: Text('Input keyword and search'),
          );
        } else if (state.loading) {
          return Center(
            child: CircularProgressIndicator(),
          );
        } else if (state.error) {
          return Center(
            child: Text(
              'Failed to get jobs',
              style: TextStyle(color: Colors.red),
            ),
          );
        } else {
          return JobsList(state.data);
        }
      },
    );
  }
}
Listing 10-25

GitHub jobs widget using Redux store

The last step is to create the store. The store in Listing 10-26 is created with the reducer function, the initial state, and the thunk middleware from redux_thunk package.
final store = new Store<JobsState>(
  jobsReducers,
  initialState: JobsState(),
  middleware: [thunkMiddleware],
);
Listing 10-26

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.

Listing 10-27 shows the basic code of jobs_store.dart file for the Mobx store. This file uses the generated part file jobs_store.g.dart. _JobsStore is the abstract class of the store for jobs. It implements Store class from Mobx. Here we defined two observables using @observable annotation. The first observable keyword is a simple string that manages the current search keyword. The getJobsFuture observable is an ObservableFuture<List<Job>> object that manages the asynchronous operation to get the jobs using API. Those properties marked using @computed annotation are derived observables to check the status of data loading. We also define two actions using @action annotation. The setKeyword() action sets the getJobsFuture observable to an empty state and keyword observable to the provided value. The getJobs() action uses GitHubJobsClient.getJobs() method to load the data. The getJobsFuture observable is updated to an ObservableFuture object wrapping the returned future.
import 'package:meta/meta.dart';
import 'package:mobx/mobx.dart';
part 'jobs_store.g.dart';
class JobsStore = _JobsStore with _$JobsStore;
abstract class _JobsStore implements Store {
  _JobsStore({@required this.jobsClient}) : assert(jobsClient != null);
  final GitHubJobsClient jobsClient;
  @observable
  String keyword = ";
  @observable
  ObservableFuture<List<Job>> getJobsFuture = emptyResponse;
  @computed
  bool get empty => getJobsFuture == emptyResponse;
  @computed
  bool get hasResults =>
      getJobsFuture != emptyResponse &&
      getJobsFuture.status == FutureStatus.fulfilled;
  @computed
  bool get loading =>
      getJobsFuture != emptyResponse &&
      getJobsFuture.status == FutureStatus.pending;
  @computed
  bool get hasError =>
      getJobsFuture != emptyResponse &&
      getJobsFuture.status == FutureStatus.rejected;
  static ObservableFuture<List<Job>> emptyResponse = ObservableFuture.value([]);
  List<Job> jobs = [];
  @action
  Future<List<Job>> getJobs() async {
    jobs = [];
    final future = jobsClient.getJobs(keyword);
    getJobsFuture = ObservableFuture(future);
    return jobs = await future;
  }
  @action
  void setKeyword(String keyword) {
    getJobsFuture = emptyResponse;
    this.keyword = keyword;
  }
}
Listing 10-27

Mobx store

The flutter packages pub run build_runner build command is required to generate code. JobsStore class is the store to use. Listing 10-28 shows the widget that uses the store. In the onPressed callback of the search button, setKeyword() method is called first to update the keyword, then getJobs() method is called to trigger the data loading. The Observer widget uses a build function to build the UI using computed observables and fields in JobsStore object. Whenever these observables change, Observer widget rebuilds to update the UI.
class GitHubJobs extends StatefulWidget {
  GitHubJobs({Key key, @required this.jobsStore})
      : assert(jobsStore != null),
        super(key: key);
  final JobsStore jobsStore;
  @override
  _GitHubJobsState createState() => _GitHubJobsState();
}
class _GitHubJobsState extends State<GitHubJobs> {
  @override
  Widget build(BuildContext context) {
    JobsStore jobsStore = widget.jobsStore;
    return Column(
      children: <Widget>[
        Padding(
          padding: const EdgeInsets.all(8.0),
          child: KeywordInput(
            jobsStore: jobsStore,
          ),
        ),
        Expanded(
          child: JobsView(
            jobsStore: jobsStore,
          ),
        ),
      ],
    );
  }
}
class KeywordInput extends StatefulWidget {
  KeywordInput({this.jobsStore});
  final JobsStore jobsStore;
  @override
  _KeywordInputState createState() => _KeywordInputState();
}
class _KeywordInputState extends State<KeywordInput> {
  final GlobalKey<FormFieldState<String>> _keywordFormKey = GlobalKey();
  @override
  Widget build(BuildContext context) {
    return Row(
      children: <Widget>[
        Expanded(
          child: TextFormField(
            key: _keywordFormKey,
          ),
        ),
        IconButton(
          icon: Icon(Icons.search),
          onPressed: () {
            String keyword = _keywordFormKey.currentState?.value ?? ";
            if (keyword.isNotEmpty) {
              widget.jobsStore.setKeyword(keyword);
              widget.jobsStore.getJobs();
            }
          },
        ),
      ],
    );
  }
}
class JobsView extends StatelessWidget {
  JobsView({this.jobsStore});
  final JobsStore jobsStore;
  @override
  Widget build(BuildContext context) {
    return Observer(
      builder: (BuildContext context) {
        if (jobsStore.empty) {
          return Center(
            child: Text('Input keyword and search'),
          );
        } else if (jobsStore.loading) {
          return Center(
            child: CircularProgressIndicator(),
          );
        } else if (jobsStore.hasError) {
          return Center(
            child: Text(
              'Failed to get jobs',
              style: TextStyle(color: Colors.red),
            ),
          );
        } else {
          return JobsList(jobsStore.jobs);
        }
      },
    );
  }
}
Listing 10-28

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.

..................Content has been hidden....................

You can't read the all page of ebook, please click here login for view all page.
Reset