Flutter’s cognition and Thinking on state management

Time:2021-10-6

preface

fromProgramming technology exchange holy land [- Fluent group -]InitiatedState management research group, make do withState managementRelevant topics will be discussed for a period of timetwo monthsDiscussion.

Flutter's cognition and Thinking on state management

At present, only 5 people are scheduled to participate in the discussion. If you are rightState managementIf you have any unique opinions or want to participate, you can consultZhang Feng Jie te lie, welcome to communicate with us.


I wanted to write about some of the contents of this article a long time ago, but there has been no source of power, so I have been waiting for it

This time, I was urged several times by the leader of Jett, and finally finished writing this article. There are some thoughts and views on state management in the article, hoping to cause sporadic resonance in the vast crowd…

Flutter's cognition and Thinking on state management

Flutter's cognition and Thinking on state management

Flutter's cognition and Thinking on state management

Cognition of state management

changes

Decoupling is the cornerstone of many ideas or frameworks

Take the most classic MVC for example. The modules are divided into three layers

  • Model layer: Data Management
  • Controller layer: logic processing
  • View layer: view building

This classic hierarchy can cope with many scenarios

  • MVP and MVVM are also variants of MVC, which are essentially for more reasonable decoupling in appropriate scenarios
  • In fact, these patterns are very suitable for the mobile terminal. The old XML writing method of the mobile terminal is to obtain its view node and then operate on its node
  • In the era of JSP, jQuery is popular, operating DOM nodes and refreshing data; cut from the same cloth

The times are always advancing in development, and technology is constantly changing; Just like Prometheus stole fire and brought many changes to the world

The fixed application of the idea of view node operation in today’s front-end is inaccurate

Nowadays, the front end is displayed by many “states” to control the interface, which needs to be described in more refined language

Inclusive

The focus of state management is on its surface: state and management

  • With only four words, it clearly summarizes the thought and its soul

State is the soul of the page and the anchor of business logic and general logic. As long as the state is separated and managed, the page can be decoupled

Generally speaking, from the concept of state management, multiple levels can be decoupled

Minimalist mode πŸ˜ƒ

This is a very simple hierarchical division. Many popular fluent state management frameworks are also divided in this way, such as provider and geTx

  • View: interface layer
  • Logic: logic layer + status layer

Flutter's cognition and Thinking on state management

Standard mode πŸ™‚

This is already a hierarchy similar to MVC, and this hierarchy is also very common, such as cube (provider and geTx can easily divide this structure)

  • View: Interface
  • Logic: logic layer
  • State: state layer

Flutter's cognition and Thinking on state management

Strict mode 😐

For the standard vertebral model, it has been well divided, but there is still a certain level that has not been divided:User program interaction behavior

Note: to divide this level, the cost must be great, which will further increase the complexity of the framework

  • Later, we will analyze why the division of this level will lead to great cost

Common state management frameworks: bloc, Redux, fish_ redux

  • View: interface layer
  • Logic: logic layer
  • State: state layer
  • Action: behavior layer

Flutter's cognition and Thinking on state management

Obsessive compulsive disorder model πŸ˜‘

Common state management frameworks: Redux, fish_ redux

From the figure, this structure is a little complex. In order to decouple the level of data refresh, it has paid a huge cost

  • View: interface layer
  • Logic: logic layer
  • State: state layer
  • Action: behavior layer
  • Reducer: this level is dedicated to handling data changes

Flutter's cognition and Thinking on state management

reflection

Should we fear and resist changing things and ideas?

I often think:Excellent thoughts witness changes. They will not decline in time, but become more and more bright

For example: design pattern

Decoupling cost

Separation logic + state layer

A mature state management framework must divide the logic from the interface layer, which is the most simple original intention of a state management framework

Some views

In fact, the cost paid at this time is for the framework developers, who need to choose an appropriate technical scheme for reasonable decoupling

To implement a state management framework, I may say:

  • This is not such a difficult thing
  • A few files can implement a reasonable and powerful state management framework

At this point, you may think in front of the screen: this hair can really boast and make you laugh

Flutter's cognition and Thinking on state management

I’m not bragging about the above. After reading several state management source codes, I found that the idea of state management is actually very simple. Of course, the code of the open source framework is not so simple. It basically makes a lot of abstractions to facilitate function expansion, which will basically cause great trouble to readers, especially the provider

After extracting several typical ideas of state management, I reproduced its operation mechanism with minimalist code. I found that I used the idea of observation mode. After understanding it, I don’t think how mysterious the state management framework is

I have no idea of contempt:They are all great pioneers in that reckless era!

How to decouple the logic + state layer from the interface?

I have summarized several classic implementation mechanisms of state management. Because each implementation source code is a little long, it will be put in the second half of the article. If you are interested, you can have a look; The code of each implementation mode is complete and can run independently

  • Decouple the logic layer interface

    • The cost is at the framework end, which requires more complex implementation
    • Generally speaking, only two layers are decoupled, which is generally simple to use

Flutter's cognition and Thinking on state management

  • Decoupling state layer

    • Generally speaking, it is not difficult to separate the logic layer and decouple the state layer; You can simply divide it manually. Several idea plug-ins I wrote generate template code and divide this layer
    • You can also directly force conventions within the framework, such as bloc mode and cube mode in bloc, Redux series…
    • The division cost is not high, the use cost is not high, and the decoupling of this layer has a far-reaching impact

Flutter's cognition and Thinking on state management

Cost of action layer

What is the action layer? Just like its name, the behavior layer, user and interaction events on the interface can be divided into this layer

  • For example: click button events, input events, pull-up and pull-down events, etc
  • These events are generated on the user interface, and we also need to make corresponding logic to respond

Why divide the action layer?

  • If you have a good time writing the code of the shuttle doll, you may find that the interactive portals of many click events are in the widget mountain
  • Interaction events are scattered in a large number of interface codes. If you need to adjust the parameters of jump events, it will be a headache to find them
  • There is another important aspect: in fact, the entry of interactive events is the business entry. It is also troublesome to find the corresponding business code during demand adjustment!

Based on the consideration of business meeting, some frameworks have divided the action layer to uniformly manage all interaction events

cost

Frame side cost

It is not very difficult to manage all interactive events uniformly

  • In general, we can directly call the methods of the logic layer in the view layer to execute the relevant business logic
  • Now we need to manage the behavior of calling logical layer methods in a unified way
  • Therefore, you need to add an intermediate layer in the middle of the call to transfer all events
  • This transit layer is the action layer, which can manage all interaction events

Let’s look at the implementation ideas

Flutter's cognition and Thinking on state management

The implementation cost of the framework side is not high, mainly the acceptance and distribution of events

In fact, we generally don’t care about the cost of the framework side. No matter how complex the internal implementation of the framework is, the usage should be concise and clear

If the internal design is very exquisite, but it is obscure and cumbersome to use, it will undoubtedly increase the mental burden on users

Use side cost

Dividing the action layer will increase the user’s use cost, which is inevitable

  • Event definition cost: because the event layer is divided, each interaction must be defined in the action layer
  • Sending event cost: in the view layer, we need to send the defined events with different API, which is not very different from the previous call, and the cost is very low.
  • Processing cost of logic layer: there must be one more module or method in the logic layer. It will be a little cumbersome to accept the distribution method for classification processing

The modules in the red box in the figure are additional use costs

Flutter's cognition and Thinking on state management

External performance

Bloc does not use action

  • View layer, code abbreviation, just look at its external performance
class BlBlocCounterPage extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return BlocProvider(
      create: (BuildContext context) => BlBlocCounterBloc()..init(),
      child: Builder(builder: (context) => _buildPage(context)),
    );
  }

  Widget _buildPage(BuildContext context) {
    final bloc = BlocProvider.of<BlBlocCounterBloc>(context);

    return Scaffold(
      ...
      floatingActionButton: FloatingActionButton(
        //Call business method
        onPressed: () => bloc.increment(),
        child: Icon(Icons.add),
      ),
    );
  }
}
  • Bloc layer
class BlBlocCounterBloc extends Bloc<BlBlocCounterEvent, BlBlocCounterState> {
  BlBlocCounterBloc() : super(BlBlocCounterState().init());

  void init() async {
    ///Process logic and call the emit method to refresh
    emit(state.clone());
  }
    
  ...
}

State layer: in this demonstration, this layer is not important and will not be written

Bloc uses action

  • View layer, code abbreviation, just look at its external performance
class BlBlocCounterPage extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return BlocProvider(
      create: (BuildContext context) => BlBlocCounterBloc()..add(InitEvent()),
      child: Builder(builder: (context) => _buildPage(context)),
    );
  }

  Widget _buildPage(BuildContext context) {
    final bloc = BlocProvider.of<BlBlocCounterBloc>(context);

    return Scaffold(
      ...
      floatingActionButton: FloatingActionButton(
        onPressed: () => bloc.add(AEvent()),
        child: Icon(Icons.add),
      ),
    );
  }
}
  • Bloc layer
class BlBlocCounterBloc extends Bloc<BlBlocCounterEvent, BlBlocCounterState> {
  BlBlocCounterBloc() : super(BlBlocCounterState().init());

  @override
  Stream<BlBlocCounterState> mapEventToState(BlBlocCounterEvent event) async* {
    if (event is InitEvent) {
      yield await init();
    } else if (event is AEvent) {
      yield a();
    } else if (event is BEvent) {
      yield b();
    } else if (event is CEvent) {
      yield c();
    } else if (event is DEvent) {
      yield d();
    } else if (event is EEvent) {
      yield e();
    } else if (event is FEvent) {
      yield f();
    } else if (event is GEvent) {
      yield g();
    } else if (event is HEvent) {
      yield h();
    } else if (event is IEvent) {
      yield i();
    } else if (event is JEvent) {
      yield j();
    } else if (event is KEvent) {
      yield k();
    }
  }

  ///Corresponding business method
  ...
}
  • Event layer: if you need to pass parameters, you need to define relevant variables in the event class, implement its constructor, and transfer the view layer data to the bloc layer
abstract class BlBlocCounterEvent {}

class InitEvent extends BlBlocCounterEvent {}

class AEvent extends BlBlocCounterEvent {}

class BEvent extends BlBlocCounterEvent {}

class CEvent extends BlBlocCounterEvent {}

.......

class KEvent extends BlBlocCounterEvent {}

State layer: in this demonstration, this layer is not important and will not be written

fish_ Performance of Redux

  • view
Widget buildView(MainState state, Dispatch dispatch, ViewService viewService) {
  return Scaffold(
    //Top AppBar
    appBar: mainAppBar(
      onTap: () => dispatch(MainActionCreator.toSearch()),
    ),
    //Side drawer module
    drawer: MainDrawer(
      data: state,
      onTap: (String tag) => dispatch(MainActionCreator.clickDrawer(tag)),
    ),
    //Page body
    body: MainBody(
      data: state,
      onChanged: (int index) => dispatch(MainActionCreator.selectTab(index)),
    ),
    //Bottom navigation
    bottomNavigationBar: MainBottomNavigation(
      data: state,
      onTap: (int index) => dispatch(MainActionCreator.selectTab(index)),
    ),
  );
}
  • Action layer
enum MainAction {
  //Switch tab
  selectTab,
  //Click item in the sidebar
  clickDrawer,
  //Search
  toSearch,
  //Unified refresh event
  onRefresh,
}

class MainActionCreator {
  static Action toSearch() {
    return Action(MainAction.toSearch);
  }

  static Action selectTab(int index) {
    return Action(MainAction.selectTab, payload: index);
  }

  static Action onRefresh() {
    return Action(MainAction.onRefresh);
  }

  static Action clickDrawer(String tag) {
    return Action(MainAction.clickDrawer, payload: tag);
  }
}
  • Effect
Effect<MainState> buildEffect() {
  return combineEffects(<Object, Effect<MainState>>{
    //Initialization
    Lifecycle.initState: _init,
    //Switch tab
    MainAction.selectTab: _selectTab,
    //Select the item inside the corresponding drawer
    MainAction.clickDrawer: _clickDrawer,
    //Jump to search page
    MainAction.toSearch: _toSearch,
  });
}

///Numerous business methods
void _init(Action action, Context<MainState> ctx) async {
  ...
}
  • The reducer and state layers are not important, so they are not written here

fish_ Redux’s division of action layer and event distribution are obviously much more sophisticated than bloc

fish_ Redux uses enumeration and one class to define many events; Bloc needs to inherit classes, one class and one event

To be honest, I used both frameworks. It’s really troublesome to write bloc like this. Especially when it comes to parameter passing, you need to define many variables in the class

summary

By comparing the above forms, we can find that the difference is quite big

The action layer is added, which makes the use cost inevitably soar

Many people may make complaints about it at this time.

Flutter's cognition and Thinking on state management

Thinking and evolution of action layer

By analyzing the design essence of separating the action layer, we will find an unavoidable reality!

  • By adding the action layer, the cost of the user side cannot be avoided
  • Because the increased cost of the user end is the core of the design of the frame side

Flutter's cognition and Thinking on state management

When the business becomes more and more complex, it is imperative to divide the action layer. We must summarize the event entry; When the business is frequently adjusted, you need to be able to quickly locate the corresponding business!

Is there a way to simplify?

The division of action layer will increase the burden of users to a certain extent. What can be done to simplify it? At the same time, it can achieve the effect of managing the event entrance?

I have done a lot of thinking about the widget of crazy dolls on the view layer and tried to split it

The split effect combines the view layer and action well. The specific operations are as follows:Flutter improves the problem of taowa hell (example of imitation Himalayan PC page)

  • Take a look at the code effect after splitting

    • Because the view is clearly divided into modules, the external exposure method is business events, which can easily locate the corresponding business
    • After such division, the corresponding page structure becomes very clear, and it is easy to modify the corresponding module of the page

Flutter's cognition and Thinking on state management

  • After relevant transformation of view layer

    • It is very convenient to locate business and interface modules
    • At the same time, it also avoids a series of slightly cumbersome operations in the action layer

Flutter's cognition and Thinking on state management

summary

The agreement of the framework can regulate many developers with different behavior habits

The splitting of the view layer I proposed later can only rely on the awareness of the developers themselves

Here, I give you a different way. The choice can only be decided by you

At present, I always use the split of view layer. I feel very friendly to the maintenance of later complex modules~~

Make complaints about Reducer layer

Maybe I’m too delicious to feel the beauty of this layer of differentiation

I use fish_ Redux has also written many pages (it has been used for a year). Previously, it will transfer relevant data to reducer through the action layer, and then refresh accordingly, which leads to a problem!

  • Every time I refresh the data of different behaviors, I need to create an action
  • Then I analyze the transmitted data in the reducer layer and assign values to the clone object. When I want to modify the data, I must first go to the effect layer to see the logic, and then go to the reducer to modify the assignment
  • Jump back and forth, trouble to explode!

After being around for many times and getting annoyed for many times, I directly wrote the reducer layer as a refresh method!

Reducer<WebViewState> buildReducer() {
  return asReducer(
    <Object, Reducer<WebViewState>>{
      WebViewAction.onRefresh: _onRefresh,
    },
  );
}

WebViewState _onRefresh(WebViewState state, Action action) {
  return state.clone();
}

Even in complex modules, I don’t feel the benefits it brings to me, so I can only weaken it infinitely into a refresh method

Flutter's cognition and Thinking on state management

Several implementations of state management

This is the source code I saw some state management

  • Several refresh mechanisms of state management are summarized
  • Either way, you can work out your own state management framework

Several previous source code analysis articles have been written, sorted out and summarized

Flutter's cognition and Thinking on state management

Realization of rotten Street

Minimum implementation difficulty ⭐

This is a very common implementation

  • This is a simple, easy-to-use and powerful implementation
  • At the same time, due to the low difficulty, it is also a realization of rotten street

realization

We need to implement a middleware for managing logical layer instances: the implementation of dependency injection

You can also use inheritedwidget to save and pass logical layer instances (bloc does this); However, self-management can greatly broaden the use scenario. Here, we will implement a middleware for managing instances

  • Only three basic APIs are implemented here
///Dependency injection: external instances can be injected into this class and managed by this class
class Easy {
  ///Injection instance
  static T put<T>(T dependency, {String? tag}) =>
      _EasyInstance().put(dependency, tag: tag);

  ///Get injected instance
  static T find<T>({String? tag, String? key}) =>
      _EasyInstance().find<T>(tag: tag, key: key);

  ///Delete instance
  static bool delete<T>({String? tag, String? key}) =>
      _EasyInstance().delete<T>(tag: tag, key: key);
}

///Concrete logic
class _EasyInstance {
  factory _EasyInstance() => _instance ??= _EasyInstance._();

  static _EasyInstance? _instance;

  _EasyInstance._();

  static final Map<String, _InstanceInfo> _single = {};

  ///Injection instance
  T put<T>(T dependency, {String? tag}) {
    final key = _getKey(T, tag);
    //Save only the first injection: optimized for the automatic refresh mechanism, the data will not be reset each time it is hot overloaded
    _single.putIfAbsent(key, () => _InstanceInfo<T>(dependency));
    return find<T>(tag: tag);
  }

  ///Get injected instance
  T find<T>({String? tag, String? key}) {
    final newKey = key ?? _getKey(T, tag);
    var info = _single[newKey];

    if (info?.value != null) {
      return info!.value;
    } else {
      throw '"$T" not found. You need to call "Easy.put($T())""';
    }
  }

  ///Delete instance
  bool delete<T>({String? tag, String? key}) {
    final newKey = key ?? _getKey(T, tag);
    if (!_single.containsKey(newKey)) {
      print('Instance "$newKey" already removed.');
      return false;
    }

    _single.remove(newKey);
    print('Instance "$newKey" deleted.');
    return true;
  }

  String _getKey(Type type, String? name) {
    return name == null ? type.toString() : type.toString() + name;
  }
}

class _InstanceInfo<T> {
  _InstanceInfo(this.value);
  T value;
}

Define a listener and base class

  • You can also use changenotifier; Here we simply define one
class EasyXNotifier {
  List<VoidCallback> _listeners = [];

  void addListener(VoidCallback listener) => _listeners.add(listener);

  void removeListener(VoidCallback listener) {
    for (final entry in _listeners) {
      if (entry == listener) {
        _listeners.remove(entry);
        return;
      }
    }
  }

  void dispose() => _listeners.clear();

  void notify() {
    if (_listeners.isEmpty) return;

    for (final entry in _listeners) {
      entry.call();
    }
  }
}
  • I wrote the minimalist in this place and didn’t add the relevant life cycle. For the sake of code simplicity, this is not listed for the time being
class EasyXController {
  EasyXNotifier xNotifier = EasyXNotifier();

  ///Refresh control
  void update() => xNotifier.notify();
}

Let’s take a look at the core easybuilder control: it’s done!

  • The implementation code is extremely simple. I hope you can have a clear idea
///Refresh the control with its own recycling mechanism
class EasyBuilder<T extends EasyXController> extends StatefulWidget {
  final Widget Function(T logic) builder;
  final String? tag;
  final bool autoRemove;

  const EasyBuilder({
    Key? key,
    required this.builder,
    this.autoRemove = true,
    this.tag,
  }) : super(key: key);

  @override
  _EasyBuilderState<T> createState() => _EasyBuilderState<T>();
}

class _EasyBuilderState<T extends EasyXController> extends State<EasyBuilder<T>> {
  late T controller;

  @override
  void initState() {
    super.initState();
    
    ///Here is the soul code of the whole class
    controller = Easy.find<T>(tag: widget.tag);
    controller.xNotifier.addListener(() {
      if (mounted) setState(() {});
    });
  }

  @override
  void dispose() {
    if (widget.autoRemove) {
      Easy.delete<T>(tag: widget.tag);
    }
    controller.xNotifier.dispose();

    super.dispose();
  }

  @override
  Widget build(BuildContext context) => widget.builder(controller);
}

use

  • It’s easy to use. Let’s look at the logic layer first
class EasyXCounterLogic extends EasyXController {
  var count = 0;

  void increase() {
    ++count;
    update();
  }
}
  • Interface layer
class EasyXCounterPage extends StatelessWidget {
  final logic = Easy.put(EasyXCounterLogic());

  @override
  Widget build(BuildContext context) {
    return BaseScaffold(
      AppBar: AppBar (Title: const text ('easyx customize easybuilder refresh mechanism '),
      body: Center(
        child: EasyBuilder<EasyXCounterLogic>(builder: (logic) {
          return Text(
            'clicked ${logic. Count} times',
            style: TextStyle(fontSize: 30.0),
          );
        }),
      ),
      floatingActionButton: FloatingActionButton(
        onPressed: () => logic.increase(),
        child: Icon(Icons.add),
      ),
    );
  }
}
  • design sketch

Flutter's cognition and Thinking on state management

Implementation of inheritedwidget

It is difficult to implement ⭐⭐

More detailed analysis can be viewed:The other side of the fluent provider

Let’s take a look at the inherited widget, which has some functions

  • Store data, and the data can be transmitted with parent and child nodes
  • Built in local refresh mechanism

Data transmission

Flutter's cognition and Thinking on state management

Local refresh

Inheritedwidget has a powerful operation function for the elements of child nodes

  • You can store the element instance of the child widget in its inheritedelement_ In the dependencies variable
  • Calling its notifyclients method will traverse_ The sub Element in dependents is called, and then the markNeedsBuild method of sub Element is invoked to complete the operation of fixed-point refreshing child nodes.

Flutter's cognition and Thinking on state management

With the above two key knowledge, you can easily implement a powerful state management framework. Let’s take a look at the implementation

realization

  • Changenotifiereasyp: analogy to changenotifierprovider of provider
class ChangeNotifierEasyP<T extends ChangeNotifier> extends StatelessWidget {
  ChangeNotifierEasyP({
    Key? key,
    required this.create,
    this.builder,
    this.child,
  }) : super(key: key);

  final T Function(BuildContext context) create;

  final Widget Function(BuildContext context)? builder;
  final Widget? child;

  @override
  Widget build(BuildContext context) {
    assert(
      builder != null || child != null,
      '$runtimeType  must specify a child',
    );

    return EasyPInherited(
      create: create,
      child: builder != null
          ? Builder(builder: (context) => builder!(context))
          : child!,
    );
  }
}

class EasyPInherited<T extends ChangeNotifier> extends InheritedWidget {
  EasyPInherited({
    Key? key,
    required Widget child,
    required this.create,
  }) : super(key: key, child: child);

  final T Function(BuildContext context) create;

  @override
  bool updateShouldNotify(InheritedWidget oldWidget) => false;

  @override
  InheritedElement createElement() => EasyPInheritedElement(this);
}

class EasyPInheritedElement<T extends ChangeNotifier> extends InheritedElement {
  EasyPInheritedElement(EasyPInherited<T> widget) : super(widget);

  bool _firstBuild = true;
  bool _shouldNotify = false;
  late T _value;
  late void Function() _callBack;

  T get value => _value;

  @override
  void performRebuild() {
    if (_firstBuild) {
      _firstBuild = false;
      _value = (widget as EasyPInherited<T>).create(this);

      _value.addListener(_callBack = () {
        //Processing refresh logic, notifyclients cannot be called directly here
        //Will result in owner_ Debugcurrentbuildtarget is null, triggering assertion condition and cannot be executed backwards
        _shouldNotify = true;
        markNeedsBuild();
      });
    }

    super.performRebuild();
  }

  @override
  Widget build() {
    if (_shouldNotify) {
      _shouldNotify = false;
      notifyClients(widget);
    }
    return super.build();
  }

  @override
  void notifyDependent(covariant InheritedWidget oldWidget, Element dependent) {
    //Here, you can directly refresh the added listening sub element instead of all kinds of super
    dependent.markNeedsBuild();
    // super.notifyDependent(oldWidget, dependent);
  }

  @override
  void unmount() {
    _value.removeListener(_callBack);
    _value.dispose();
    super.unmount();
  }
}
  • Easyp: provider class analogous to provider
class EasyP {
  ///Get easyp instance
  ///When obtaining an instance, the listener parameter is always written incorrectly. Here, two methods are used to distinguish it directly
  static T of<T extends ChangeNotifier>(BuildContext context) {
    return _getInheritedElement<T>(context).value;
  }

  ///Register listener control
  static T register<T extends ChangeNotifier>(BuildContext context) {
    var element = _getInheritedElement<T>(context);
    context.dependOnInheritedElement(element);
    return element.value;
  }

  ///Gets the component that inherits inheritedelement < T > closest to the current element
  static EasyPInheritedElement<T>
      _getInheritedElement<T extends ChangeNotifier>(BuildContext context) {
    var inheritedElement = context
            .getElementForInheritedWidgetOfExactType<EasyPInherited<T>>()
        as EasyPInheritedElement<T>?;

    if (inheritedElement == null) {
      throw EasyPNotFoundException(T);
    }

    return inheritedElement;
  }
}

class EasyPNotFoundException implements Exception {
  EasyPNotFoundException(this.valueType);

  final Type valueType;

  @override
  String toString() => 'Error: Could not find the EasyP<$valueType>';
}
  • Build: just the last whole build class
class EasyPBuilder<T extends ChangeNotifier> extends StatelessWidget {
  const EasyPBuilder(
    this.builder, {
    Key? key,
  }) : super(key: key);

  final Widget Function() builder;

  @override
  Widget build(BuildContext context) {
    EasyP.register<T>(context);
    return builder();
  }
}

The above three classes have implemented a set of state management framework based on the functions of inheritedwidget

  • Local refresh function is realized
  • The logic layer instance is implemented, and the functions can be transferred with the parent and child nodes of the widget

use

The usage is basically the same as that of provider

  • view
class CounterEasyPPage extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return ChangeNotifierEasyP(
      create: (BuildContext context) => CounterEasyP(),
      builder: (context) => _buildPage(context),
    );
  }

  Widget _buildPage(BuildContext context) {
    final easyP = EasyP.of<CounterEasyP>(context);

    return Scaffold(
      AppBar: AppBar (Title: text ('custom status management framework - easyp example '),
      body: Center(
        child: EasyPBuilder<CounterEasyP>(() {
          return Text(
            'clicked ${easyp. Count} times',
            style: TextStyle(fontSize: 30.0),
          );
        }),
      ),
      floatingActionButton: FloatingActionButton(
        onPressed: () => easyP.increment(),
        child: Icon(Icons.add),
      ),
    );
  }
}
  • easyP
class CounterEasyP extends ChangeNotifier {
  int count = 0;

  void increment() {
    count++;
    notifyListeners();
  }
}
  • design sketch:

Flutter's cognition and Thinking on state management

Implementation of automatic refresh

Implementation requires some inspiration ⭐⭐⭐

Implementation of automatic refresh

  • A connection is established between a single state variable and the refresh component
  • Once the variable value changes, the refresh component will refresh automatically
  • When a state changes, only its refresh component will be triggered automatically, and other refresh components will not be triggered

realization

Similarly, middleware that manages its logical classes is needed; To complete the example, write down the dependency management class

///Dependency injection: external instances can be injected into this class and managed by this class
class Easy {
  ///Injection instance
  static T put<T>(T dependency, {String? tag}) =>
      _EasyInstance().put(dependency, tag: tag);

  ///Get injected instance
  static T find<T>({String? tag, String? key}) =>
      _EasyInstance().find<T>(tag: tag, key: key);

  ///Delete instance
  static bool delete<T>({String? tag, String? key}) =>
      _EasyInstance().delete<T>(tag: tag, key: key);
}

///Concrete logic
class _EasyInstance {
  factory _EasyInstance() => _instance ??= _EasyInstance._();

  static _EasyInstance? _instance;

  _EasyInstance._();

  static final Map<String, _InstanceInfo> _single = {};

  ///Injection instance
  T put<T>(T dependency, {String? tag}) {
    final key = _getKey(T, tag);
    //Save only the first injection: optimized for the automatic refresh mechanism, the data will not be reset each time it is hot overloaded
    _single.putIfAbsent(key, () => _InstanceInfo<T>(dependency));
    return find<T>(tag: tag);
  }

  ///Get injected instance
  T find<T>({String? tag, String? key}) {
    final newKey = key ?? _getKey(T, tag);
    var info = _single[newKey];

    if (info?.value != null) {
      return info!.value;
    } else {
      throw '"$T" not found. You need to call "Easy.put($T())""';
    }
  }

  ///Delete instance
  bool delete<T>({String? tag, String? key}) {
    final newKey = key ?? _getKey(T, tag);
    if (!_single.containsKey(newKey)) {
      print('Instance "$newKey" already removed.');
      return false;
    }

    _single.remove(newKey);
    print('Instance "$newKey" deleted.');
    return true;
  }

  String _getKey(Type type, String? name) {
    return name == null ? type.toString() : type.toString() + name;
  }
}

class _InstanceInfo<T> {
  _InstanceInfo(this.value);
  T value;
}
  • Customize a listening class
class EasyXNotifier {
  List<VoidCallback> _listeners = [];

  void addListener(VoidCallback listener) => _listeners.add(listener);

  void removeListener(VoidCallback listener) {
    for (final entry in _listeners) {
      if (entry == listener) {
        _listeners.remove(entry);
        return;
      }
    }
  }

  void dispose() => _listeners.clear();

  void notify() {
    if (_listeners.isEmpty) return;

    for (final entry in _listeners) {
      entry.call();
    }
  }
}

In the automatic refresh mechanism, the basic type needs to be encapsulated

  • The main logic is in Rx < T >
  • Set value and get value are the key
///Extension function
extension IntExtension on int {
  RxInt get ebs => RxInt(this);
}

extension StringExtension on String {
  RxString get ebs => RxString(this);
}

extension DoubleExtension on double {
  RxDouble get ebs => RxDouble(this);
}

extension BoolExtension on bool {
  RxBool get ebs => RxBool(this);
}

///Package each type
class RxInt extends Rx<int> {
  RxInt(int initial) : super(initial);

  RxInt operator +(int other) {
    value = value + other;
    return this;
  }

  RxInt operator -(int other) {
    value = value - other;
    return this;
  }
}

class RxDouble extends Rx<double> {
  RxDouble(double initial) : super(initial);

  RxDouble operator +(double other) {
    value = value + other;
    return this;
  }

  RxDouble operator -(double other) {
    value = value - other;
    return this;
  }
}

class RxString extends Rx<String> {
  RxString(String initial) : super(initial);
}

class RxBool extends Rx<bool> {
  RxBool(bool initial) : super(initial);
}

///Subject logic
class Rx<T> {
  EasyXNotifier subject = EasyXNotifier();

  Rx(T initial) {
    _value = initial;
  }

  late T _value;

  bool firstRebuild = true;

  String get string => value.toString();

  @override
  String toString() => value.toString();

  set value(T val) {
    if (_value == val && !firstRebuild) return;
    firstRebuild = false;
    _value = val;

    subject.notify();
  }

  T get value {
    if (RxEasy.proxy != null) {
      RxEasy.proxy!.addListener(subject);
    }
    return _value;
  }
}

You need to write a very important transit class, which will also store the listening object of responsive variables

  • This class has very core logic: it associates responsive variables with refresh controls!
class RxEasy {
  EasyXNotifier easyXNotifier = EasyXNotifier();

  Map<EasyXNotifier, String> _listenerMap = {};

  bool get canUpdate => _listenerMap.isNotEmpty;

  static RxEasy? proxy;

  void addListener(EasyXNotifier notifier) {
    if (!_listenerMap.containsKey(notifier)) {
      //Refresh in variable listening
      notifier.addListener(() {
        //Refresh listening added in ebx
        easyXNotifier.notify();
      });
      //Add to map
      _listenerMap[notifier] = '';
    }
  }
}

Refresh control ebx

typedef WidgetCallback = Widget Function();

class Ebx extends StatefulWidget {
  const Ebx(this.builder, {Key? key}) : super(key: key);

  final WidgetCallback builder;

  @override
  _EbxState createState() => _EbxState();
}

class _EbxState extends State<Ebx> {
  RxEasy _rxEasy = RxEasy();

  @override
  void initState() {
    super.initState();

    _rxEasy.easyXNotifier.addListener(() {
      if (mounted) setState(() {});
    });
  }

  Widget get notifyChild {
    final observer = RxEasy.proxy;
    RxEasy.proxy = _rxEasy;
    final result = widget.builder();
    if (!_rxEasy.canUpdate) {
      throw 'Widget lacks Rx type variables';
    }
    RxEasy.proxy = observer;
    return result;
  }

  @override
  Widget build(BuildContext context) {
    return notifyChild;
  }

  @override
  void dispose() {
    _rxEasy.easyXNotifier.dispose();

    super.dispose();
  }
}

In the automatic refresh mechanism, recycling dependent instances requires specific processing

Here I wrote a recycling control, which can complete the automatic recycling of instances

  • The meaning of naming is to bind the instance to the control. When the control is recycled, the logical layer instance will also be recycled automatically
class EasyBindWidget extends StatefulWidget {
  const EasyBindWidget({
    Key? key,
    this.bind,
    this.tag,
    this.binds,
    this.tags,
    required this.child,
  })  : assert(
          binds == null || tags == null || binds.length == tags.length,
          'The binds and tags arrays length should be equal\n'
          'and the elements in the two arrays correspond one-to-one',
        ),
        super(key: key);

  final Object? bind;
  final String? tag;

  final List<Object>? binds;
  final List<String>? tags;

  final Widget child;

  @override
  _EasyBindWidgetState createState() => _EasyBindWidgetState();
}

class _EasyBindWidgetState extends State<EasyBindWidget> {
  @override
  Widget build(BuildContext context) {
    return widget.child;
  }

  @override
  void dispose() {
    _closeController();
    _closeControllers();

    super.dispose();
  }

  void _closeController() {
    if (widget.bind == null) {
      return;
    }

    var key = widget.bind.runtimeType.toString() + (widget.tag ?? '');
    Easy.delete(key: key);
  }

  void _closeControllers() {
    if (widget.binds == null) {
      return;
    }

    for (var i = 0; i < widget.binds!.length; i++) {
      var type = widget.binds![i].runtimeType.toString();

      if (widget.tags == null) {
        Easy.delete(key: type);
      } else {
        var key = type + (widget.tags?[i] ?? '');
        Easy.delete(key: key);
      }
    }
  }
}

use

  • Logical layer
class EasyXEbxCounterLogic {
  RxInt count = 0.ebs;

  ///Self increasing
  void increase() => ++count;
}
  • Interface layer: an easybindwidget is set in the top node of the page to ensure that the dependency injection instance can be recycled automatically
class EasyXEbxCounterPage extends StatelessWidget {
  final logic = Easy.put(EasyXEbxCounterLogic());

  @override
  Widget build(BuildContext context) {
    return EasyBindWidget(
      bind: logic,
      child: BaseScaffold(
        AppBar: AppBar (Title: const text ('easyx custom ebx refresh mechanism '),
        body: Center(
          child: Ebx(() {
            return Text(
              'clicked ${logic. Count. Value} times',
              style: TextStyle(fontSize: 30.0),
            );
          }),
        ),
        floatingActionButton: FloatingActionButton(
          onPressed: () => logic.increase(),
          child: Icon(Icons.add),
        ),
      ),
    );
  }
}
  • design sketch

Flutter's cognition and Thinking on state management

last

On the whole, this paper makes some thoughts and personal opinions on the division of various levels of state management. The latter half of the article also gives some implementation schemes of state management

The content in the article should be helpful to the beautiful people who want to design state management; If you have different opinions, please discuss them in the comment area

Flutter's cognition and Thinking on state management

Relevant address