Flutter status Management

There are many state management schemes for FLUTTER. Here is a summary of the relevant knowledge.

Transfer the State

Flutter divides components into statefulWidgets and statelessWidgets. Stateful components usually inherit from statefulWidgets to manage their State through State.

When the business is simple, StatefulWidget can be used to manage the state of the entire application.

But when the business gets complicated, there are some problems:

As shown in the figure, two root nodes need to share a certain state, so this state needs to be saved to their common parent node, and then passed down step by step. When the state changes, the entire sub-tree below the common parent node will be rebuilt, and the rebuild of most components is unnecessary, resulting in poor performance.

In addition, this approach may make the parent component’s state bloated, and some data may not fit in its state but must be placed there for sharing purposes.

Therefore, we need further means for state management.

To add a few more words, some client developers do not accept the React paradigm when they touch Flutter. They tend to drop the state into a manager singleton and use it when creating a page /Widget. If a state change requires a page refresh, throw a notification or something similar and let the component update it.

In the world of Flutter, singletons of some global state or state are not out of the question, but generally prefer to retain the benefits of responsiveness (Flutter itself is not fully responsive because setState is explicit, but at least partially responsive). Components maintain listening relationships when using states. Notify subscribers when status changes. This notification approach is as natural as setState or even without the explicit notification step, where a component subscribs to a read state rather than explicitly subscribs to a notification. In short, the previous ideas are too prescriptive.

InheritedWidget

An official basic solution for sharing data.

InheritedWidget is a special functional component that provides a way to transfer state from top to bottom.

For example, Theme management in the Material component is used in this way. In a MaterialApp, we can use ThemeData data = theme.of (context) to retrieve the current Theme data when building any widget. It also makes the current Widget depend on the Theme (specifically, the _InheritedTheme component), and all widgets that depend on it will be updated when the Theme changes. This solves the problem of passing State directly causing redundant component updates.

Define a ShareDataWidget InheritedWidget as follows:

class ShareDataWidget extends InheritedWidget {
  ShareDataWidget({
    @required this.data,
    Widget child
  }) :super(child: child);

  final int data;
  static ShareDataWidget of(BuildContext context) {
    return context.inheritFromWidgetOfExactType(ShareDataWidget);
  }

  @override
  bool updateShouldNotify(ShareDataWidget old) {
    return old.data != data;
  }
}
Copy the code

In the parent component:

class _InheritedWidgetTestRouteState extends State<InheritedWidgetTestRoute> {
  int count = 0;

  @override
  Widget build(BuildContext context) {
    return  Center(
      child: ShareDataWidget( / / use ShareDataWidget
        data: count,
        child: Column(
          mainAxisAlignment: MainAxisAlignment.center,
          children: <Widget>[
            Padding(
              padding: const EdgeInsets.only(bottom: 20.0),
              child: _TestWidget(),// Child widgets rely on ShareDataWidget
            ),
            RaisedButton(
              child: Text("Increment"), onPressed: () => setState(() => ++count), ) ], ), ), ); }}Copy the code

The child component gets the data in the InheritedWidget:

class __TestWidgetState extends State<_TestWidget> {
  @override
  Widget build(BuildContext context) {
    return Text(ShareDataWidget
        .of(context)
        .data
        .toString());
  }

Copy the code

As you can see, the data is still managed by the parent component’s state, and the InheritedWidget simply wraps the data around it, providing a channel for the child component to get the data.

Here is the key point is the realization of the inheritFromWidgetOfExactType, which determines the performance of the component for sharing Model, look at the implementation:

  @override
  InheritedWidget inheritFromWidgetOfExactType(Type targetType, { Object aspect }) {
    assert(_debugCheckStateIsActiveForAncestorLookup());
    final InheritedElement ancestor = _inheritedWidgets == null ? null : _inheritedWidgets[targetType];
    if(ancestor ! =null) {
      assert(ancestor is InheritedElement);
      return inheritFromElement(ancestor, aspect: aspect);
    }
    _hadUnsatisfiedDependencies = true;
    return null;
  }
Copy the code

_inheritedWidgets is a Map, and the Map in DART is a LinkedHashMap by default, where the search operation is O(1). So performance is guaranteed.

InheritedWidget has obvious drawbacks. On the one hand, when InheritedWidget is used directly, the code is verbose, and on the other hand, it only provides data transfer from top to bottom. If InheritedWidget wants to change data, it also needs to use Notification mechanism, which is even heavier.

Because the InheritedWidget is high-performance but inconvenient to use, the community has built a lot of encapsulation on top of it.

ScopedModel

The ScopedModel is a relatively successful state management component encapsulated by the early Flutter community. It seals the state into the Model, and the logic to modify the data is also in the Model, passing the Model to its child components in a similar way to inheritedWidgets, which can either take data from the Model or modify the data by calling Model methods directly.

Later, however, Flutter officials chose a better and equally convenient state management framework provider to promote Flutter. The ScopedModel is not described here.

provider

Provider is an officially authorized application status management component.

Pragmatic State Management in Flutter (Google I/O’19)

Official documentation: Simple application status management

Watch a demo

class CounterModel with ChangeNotifier {
  int _count = 0;
  int get value => _count;

  voidincrement() { _count++; notifyListeners(); }}void main() {
  final counter = CounterModel();

  runApp(
    ChangeNotifierProvider.value(
        value: counter,
        child: MaterialApp(
          home:FirstScreen()
        ),
      ),
  );
}
class FirstScreen extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    final _counter = Provider.of<CounterModel>(context);
    final textSize = Provider.of<int>(context).toDouble();

    return Scaffold(
      appBar: AppBar(
        title: Text('FirstPage'),
      ),
      body: Center(
        child: Text(
          'Value: ${_counter.value}', style: TextStyle(fontSize: textSize), ), ), floatingActionButton: FloatingActionButton( onPressed: _counter.increment, child: Icon(Icons.navigate_next), ), ); }}Copy the code

Define a CounterModel and inject it into the component tree through the Provider component. The subcomponent can get the model through the Provider. Of

(context) during build and can get data from the Model. You can also modify the data directly by calling model’s method (CounterModel.increment).

You can see how much more convenient this is than using an InheritedWidget.

The ChangeNotifierProvider used here is basically the same as the ScopedModel, and it is the most commonly used Provider. However, it is superior to ScopedModel in that the ScopedModel must inherit its Model to use, so ScopedModel is more intrusive, while Provider is much better.

In addition to ChangeNotifierProvider, providers provide several other ways to manage data:

  1. Provider
    • Data is simply shared to the child components, but the child components are not notified when the data is updated.
  2. ListenableProvider
    • The main difference with ChangeNotifierProvider is that it is not called automaticallyChangeNotifier.disposeRelease resources. Not usually.
  3. ValueListenableProvider
    • It can be considered as a special case of ChangeNotifierProvider. When listening to only one data, ValueListenableProvider does not need to be called when modifying datanotifyListeners().
  4. StreamProvider
    • Listen for a Stream
  5. FutureProvider
    • Provides a Future to its descendant node and notifies the dependent descendant node to refresh when the Future is complete

Redux (flutter_redux)

The previous libraries, by and large, provide basic data management capabilities, that is, focus on your data channel responsibilities without limiting the implementation too much.

By contrast, Redux is much heavier. It not only makes a data channel, but also imposes strong constraints on the operation of the entire data layer, which is equivalent to providing a more complete data layer design pattern. If the scenario is simple, this can be cumbersome; However, if the project is larger and more complex, these constraints can prevent code from rotting.

Let’s look at the core concepts of Redux:

Redux has a Store for storing data. Our Model is stored in the property Store.state. The get operation for data is not much different from the previous framework. Redux puts high demands on data set operations.

The default Model of the Provider framework is a heavy Model (although you can split it up further), where data comes out of the Model, and the View calls Model methods whenever it needs to change the data. It can even change the properties of the Model and throw notifyListeners.

This is not allowed in Redux’s mode. Redux’s reasoning was that in this unconstrained situation, the model could be changed everywhere, and as the business became more complex, the change in state here was likely to become difficult to trace: when, why, and how state changed out of control. That’s where all of Redux’s design starts: to make changes in state predictable.

Therefore, Redux highly encapsulates the set of state. In order to document all state changes, Redux introduced the concept of Action, which is somewhat like the Notification commonly used by clients, with each Notification having its own identity.

Actions can be of any type. If you do not need to carry data, you can use an enum for each Action. You can also define a class for each Action, as in:

class SearchLoadingAction {}
class SearchErrorAction {}
class SearchResultAction {
  final SearchResult result;
  SearchResultAction(this.result);
}
Copy the code

In summary, actions should be type sensitive and can carry data, usually with simple parameters.

When the View layer receives user input, it generates an Action to distribute store.dispatch(actions.increment) via Store.

Therefore, we need a place to process the actions and update the state, which again introduces a Reducer concept. It’s just a function that handles Action update State.

Of course, it has a special point, you can think of State as a dictionary, every time Reducer State changes, a shallow copy of State will be made and some value will be modified, the Store will Store the new State, and all contents in the old State are unchanged, this is called immutable object. The straightforward advantage is that the State can be modified by comparing the pointer to the previous State to see if the pointer is the same as the pointer to the previous State. In addition, this method restrains the modification of data and improves the security of data processing. In addition, the sequence of State changes and actions that trigger them can be easily recorded, facilitating debugging and tracing the process of data changes.

To summarize the three principles of Redux, some of which are already mentioned above.

The first principle is a single data source. Redux suggests that the entire application’s State be stored in an object tree that exists in only one store. Redux believes that centralized stores are easier to manage and debug, and avoid the problem of synchronizing data between multiple stores. Multi-store is convenient for componentized and modular split, different businesses in charge of their own Store is also more consistent with the general development habits.

The second principle is that state is read-only. As has been clearly stated before, in order to make State changes predictable, State in Redux is read-only. If you want to modify it, you must modify it through Action and Reducer.

The third principle is to use pure functions to make changes. That is, newState = oldState + Action, as explained clearly in the previous Reducer section.

fish-redux

Alibaba has just announced the open source Application framework for Flutter, Fish Redux!

Flutter_redux is a FLUTTER implementation that follows the Redux specification almost completely. In practice, the free fish carried out further encapsulation based on flutter- Redux.

There is no problem with pure Flutter applications using flutter-redux, but many applications, like Xianyu, integrate Flutter with Native applications for hybrid development on a page-by-page basis. Therefore, there is a strong need for divide-and-conquer and pluggable componentization of business logic. In this context, Fish-Redux provides a complete framework for how Redux should practice.

Take a look at the Fish-Redux demo

Fish-redux proposed the concept of Component, which can be understood as “Redux Component”. Component itself has complete REDUx capabilities (state/ Action/Reducer) and can be easily embedded into the global REdux system. The design of Fish-Redux’s self-proclaimed pluggable component architecture preserves Redux’s principles while providing the ability to divide and conquer business code.

There are so many new concepts involved in fish-Redux that you should have extensive experience with Redux if you want to use it, and such a heavy framework is not recommended for simple applications.

BloC

Stream-based responsive state management was first proposed by Google in DartConf 2018.

There is no doubt that Stream or further ReactiveX is purely reactive programming, but Redux is also reactive through actions and Reducer (maybe not pure to some extent, maybe less asynchronous than Stream). So BloC and Redux actually have some similarities when used in practice.

BLoC stands for Business Logic Component. Its idea is very simple, basically two points. The first is to extract Business Logic and encapsulate it into an independent BLoC Component, which is what every data flow framework should do. The second is to construct a responsive data flow using streams.

As shown in the figure above, BLoC encapsulates all of the business logic, which is output to the Widget via Stream, and the events generated by the Widget are thrown to BLoC, which then sinks the data into the Stream to trigger updates from subscribers.

BLoC itself is only a design model, and there is no specific implementation. At present, the implementation of all parties is slightly different. BLoC can be injected into the component tree by InheritedWidget to be obtained by sub-components, or BLoC can be obtained as a singleton. Parts of the Stream can be native Stream or RxDart. On top of that, there are some further encapsulation that can reduce some of the duplicate code.

Here we look at the BLoC implementation based on the flutter_bloc library:

class App extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return MaterialApp(
        title: 'Flutter Demo', home: BlocProvider( create: (context) => CounterBloc(), child: CounterPage(), ) ); }}class CounterPage extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(title: Text('Counter')),
      body: BlocBuilder<CounterBloc, int>(
        builder: (context, count) {
          return Center(
            child: Text(
              '$count',
              style: TextStyle(fontSize: 24.0),),); }, ), floatingActionButton: Column( crossAxisAlignment: CrossAxisAlignment.end, mainAxisAlignment: MainAxisAlignment.end, children: <Widget>[ Padding( padding: EdgeInsets.symmetric(vertical:5.0),
            child: FloatingActionButton(
              child: Icon(Icons.add),
              onPressed: () => BlocProvider.of<CounterBloc>(context)
                  .add(CounterEvent.increment),
            ),
          ),
          Padding(
            padding: EdgeInsets.symmetric(vertical: 5.0), child: FloatingActionButton( child: Icon(Icons.remove), onPressed: () => BlocProvider.of<CounterBloc>(context) .add(CounterEvent.decrement), ), ) ], ), ); }}enum CounterEvent { increment, decrement }

class CounterBloc extends Bloc<CounterEvent.int> {
  @override
  int get initialState => 0;

  @override
  Stream<int> mapEventToState(CounterEvent event) async* {
    switch (event) {
      case CounterEvent.decrement:
        yield state - 1;
        break;
      case CounterEvent.increment:
        yield state + 1;
        break; }}}Copy the code

The sub-component gets the BLoC instance via BlocProvider. Of, distributes the event to BLoC via Action, and the component gets the data through BlocBuilder to build.

Horizontal contrast

Selection of the framework

The current state management of Flutter appears to be a bloom of flowers. To summarize.

Only using state itself can be implemented in small applications, but medium-sized applications cannot be carried out. There is no effective means of optimization in performance, and it is difficult to maintain data layer architecture.

InheritedWidget is too cumbersome to use on its own, and is used more as a base capability for the upper-layer framework than directly.

ScopedModel and Provider are almost the same product. In terms of capabilities, Provider is richer and has official endorsement. Generally, small and medium-sized applications tend to prefer Provider.

Redux is a more complicated data layer scheme, which can be compared with BLoC. Both of them are further than the original State (perhaps BLoC is purer) in the way of response. The advantage of Redux is that the State changes are more controllable, and the design of Action/Reducer makes the State changes reasonable and reasonable. The advantage of BLoC is that it is better for asynchronous processing.

Finally, fish-Redux is a custom application framework based on Redux, and many ideas are for hybrid applications rather than pure Flutter applications, although there are many stars on Github… However, I feel that there are not many applications suitable for it. Small and medium sized applications are not suitable. For large applications, mixed applications, people often have their own considerations of customization.


Performance optimization

That’s about it, and then performance tuning.

Regardless of which way to inject state into widget Tree, except BLoC, there are generally two ways to get state

One is to use provider.of

(context) in the build method to get the Model. This usually causes the current component to subscribe to the Model, and causes the current component to rebuild when the Model changes. The other is to use a component of the Consumer class to get the state and pass it to the child components, as in:

Foo(
  child: Consumer<A>(
    builder: (_, a, child) {
      return Bar(a: a, child: child);
    },
    child: Baz(),
  ),
)
Copy the code

In general, Comsumer class components can do much more optimization because they can use the Builder method to generate child components. Foo -> Bar -> Baz (‘ ‘Provider. Of’); Child components are necessarily fully rebuilt.

In addition, providers provide a way to subscribe to a Selector:

Selector<List.int>(
  selector: (_, list) => list.length,
  builder: (_, length, __) {
    return Text('$length'); });Copy the code

The refresh is triggered only when the selector value changes, in this case list.length.

The Flutter redux provides a similar solution:

StoreConnector<AppState, AppViewModel>(
         distinct: true,
         builder: (context, vm) {
           return App(vm.isLogin, vm.key);
         },
         converter: (store) => AppViewModel.fromStore(store))
Copy the code

Use StoreConnector to inject state, but explicitly specify Distinct: True to rely on the VM for filtering state changes. For a mid-sized application using Redux, this should be a must-do optimization, since Redux is a single Store. So far, it seems that the framer didn’t even mention this in the Readme. It seems that many people have serious performance issues with flutter- Redux.

Providers themselves recommend multiple models. Most widgets rely on their own models, and the potential performance risk is much lower, not to mention the performance optimization capabilities that providers provide.

conclusion

To sum up, I recommend small and medium-sized applications to use Provider, which has rich functions, is easy to use, and provides relatively complete performance optimization capabilities.