This is the 10th day of my participation in Gwen Challenge
Flutter is a new cross-platform, open source UI framework developed by Google that supports iOS and Android. Flutter is the default development kit for Fuchsia, the future operating system. It is also the most popular cross-platform solution.
preface
Before we can figure out how to use the various state management libraries, we first need to have a clear understanding of why my application needs state management.
When it’s simple enough to develop Flutter as a declarative framework, you might just need to map data to views. You may not need state management, but as functionality increases, your application will have dozens or even hundreds of states.
With applications needing to share multiple unified states, it’s hard to test and maintain our state clearly because it seems so complex! There are also multiple pages that share the same status. For example, when you enter a “like” post and exit to the external thumbnail display, the external thumbnail display also needs to show the number of likes, so you need to synchronize the two states.
Flutter actually provides us with a way to manage our state from the get-go — the StatefulWidget. However, we found that it is only suitable for maintaining state within a single Widget. When we need to use state across components, StatefulWidget is no longer a good choice.
State belongs to a specific StatefulWidget, and while you can use callback when communicating between widgets, it’s easy to increase code coupling if the nesting is deep enough.
At this point, we urgently need a framework to help us clarify these relationships, hence the emergence of various state management frameworks. This article mainly introduces two kinds of providers: officially recommended Provider and Fish-Redux of Ali team
I. State management
Those who are familiar with the front end should be familiar with the state management of Vue and React, which I have compared in detail in my previous articles. Common state management includes Vuex, Flux, Redux, MobX, Bloc, Stamen and so on. Redux is a JS library. Redux is a functional data management framework. It can be applied not only to React, but also to other frameworks and platforms. Redux is a predictable, centralized, easy to debug, flexible data management framework. All operations such as adding, deleting, modifying and checking data are centralized by Redux.
But for Flutter, Redux’s strengths are also its weaknesses. The Redux core only cares about data management, not the specific scenarios to use it in. Therefore, using Redux in Flutter will face two specific problems: the contradiction between the concentration of Redux and the partition and conquer of Component; Reducer of Redux requires layers of manual assembly, resulting in complexity and error-prone.
Today (March 5, 2019), Xiefu announced the open source of Fish Redux on GitHub. From now on, Flutter has a complete state management framework.
Second, the old – Flutter – dojo.provide
Fish Redux is the perfect state management framework for Flutter in the true sense of the word, because state management -Flutter-provide does exist before Flutter. (Official address github.com/google/flut…
Provide, like Scoped_model, uses an InheritWidget to place shared state on top of the top-level MaterialApp. The underlying component gets the state through Provier and refreshes the components that depend on the state through a ChangeNotifier blend.
Here’s an example:
Both pages rely on two different states at the same time: Counter and Switcher. And when one page changes state, another changes state.
1. Add dependencies
Add a dependency on Provide in pubspec.yaml.
dependencies:
provide: ^1.02.
Copy the code
2. To create a Model
Here it actually assumes the responsibility of State, but is called model to distinguish it from the official State.
import 'package:flutter/material.dart';
class Counter with ChangeNotifier{
int value = 0; increment(){ value++; notifyListeners(); }}Copy the code
Here we can see that the data and the means to manipulate the data are all in the Model, and we can clearly separate the business. By comparing Scoped_model, we can see that in the Provide mode, the Model does not need to inherit the Model class, but only needs to implement Listenable. We have included ChangeNotifier to avoid managing the audience. NotifyListeners are notified of updates.
3. Put states on top
void main() {
var counter = Counter();
var providers = Providers();
// Add the counter object to providers
providers.provide(Provider.value(counter));
runApp(
ProviderNode(
child: MyApp(),
providers: providers),
);
}
Copy the code
ProviderNode encapsulates the InheritWidget and provides a providers container for placing state. Providers are stored internally via Map>, where you can specify the key by passing ProviderScope(“name”). Provider.value wraps counter as _ValueProvider. A StreamController is provided internally to enable streaming operations on data.
4. Obtain the status
The same Provide also provides two ways to get State. Let’s start with the first, which is obtained through the Provide widget.
Provide(
builder: (context, child, counter) {
return Text(
'${counter.value}', style: Theme.of(context).textTheme.display1, ); },),Copy the code
The Builder will rebuild the widget each time a data refresh is notified. The Builder method takes three parameters, the second and third of which are covered here. Second argument child: If the widget is complex enough that there are internal widgets that won’t change, we can improve performance by writing those widgets in the Child property Provide so that the Builder doesn’t create those widgets repeatedly. Third parameter counter: This parameter represents the state of the top-level providers we get.
Scope: Gets the state corresponding to this key by specifying ProviderScope. Used when you need to use multiple states of the same type.
Final currentCounter = Provide. Value (context); This way actually call context. InheritFromWidgetOfExactType _InheritedProviders find top to get to the top will of the state.
How do I organize multiple states
Unlike scoped_model, you can easily organize multiple states in the Provide mode. Just provide the state into the provider.
void main() {
var counter = Counter();
var switcher = Switcher();
varproviders = Providers(); providers .. provide(Provider.value(counter)) .. provide(Provider.value(switcher)); runApp( ProviderNode( child: MyApp(), providers: providers) ); }Copy the code
Get data stream
There is a wrapper around adding counter to providers. We just learned from analyzing the source code that this operation allows us to work with streaming data. The data stream is retrieved by providing. Stream (context).
StreamBuilder(
initialData: currentCounter,
stream: Provide.stream(context)
.where((counter) => counter.value % 2= =0),
builder: (context, snapshot) =>
Text('Last even value: ${snapshot.data.value}')),
Copy the code
New -Fish Redux
Fish Redux is a tailormade improvement based on Redux by the Idle Fish team. Through Redux, it does centralized and observable data management. FR is an assembled FLUTTER application framework based on Redux data management. It is especially suitable for building medium and large complex applications. It has made significant improvements to the two disadvantages of traditional Redux in the usage level. Firstly, a component needs to define a data (Struct) and a Reducer, and there is a parent dependent child relationship between components. Through this dependency relationship, the contradiction between centralization and divide and conquer was solved, and the manual Combine of Reducer was automatically completed by the framework, which simplified the difficulty of using Redux, and at the same time, the ideal centralization effect and divide and conquer code were obtained.
sample_page
- State
- Action
- Reducer
- Store
- Middleware
Copy the code
A component page directory structure can be designed this way, with the benefit that these concepts are consistent with ReduxJS and retain the benefits of Redux. Fish Redux’s most notable features are its functional programming model, predictable state management, pluggable component architecture, and optimal performance. It features configuration-type assembly. On the one hand, we will be a big page to view and data layers of dismantling for independent Component | Adapter, the upper is responsible for the assembly, the lower is responsible for the implementation; On the other hand, Component | Adapter into the View, Reducer, independent context-free function Effect, etc.
Fish Redux was inspired by great frameworks like Redux, Elm, and Dva. So in the words of the Idle Fish Flutter team, Fish Redux stands on the shoulders of giants, taking centralization, divide and conquer, reuse and segregation one step further.
Interpretation of FR source code
1. Action
The Action contains two fields
-
type
-
payload
The recommended way to write this is to create an action.dart file for a component or adapter that contains two classes
-
Create an enumeration class for the Type field
-
Create an ActionCreator class for your Action to help constrain the type of payload.
-
Effect The Action to be processed, named on{Verb}
-
Reducer receives actions that are named {verb}
enum MessageAction {
onShare,
shared,
}
class MessageActionCreator {
static Action onShare(Map<String.Object> payload) {
return Action(MessageAction.onShare, payload: payload);
}
static Action shared() {
return constAction(MessageAction.shared); }}Copy the code
2. Adapter
We’ve added a componentized abstract Adapter to the basic Component concept. It aims to solve three problems with the Component model in the ListView scenario:
-
1) Putting a “big-cell” in the ListView will not benefit from the performance optimization of the ListView code.
-
2) Component cannot distinguish appear | disappear and init | dispose event.
-
3) The coupling between the life cycle of Effect and View does not meet intuitive expectations in some scenarios of ListView.
An Adapter and Component are almost identical, except for the following:
-
Component generates a Widget, Adapter generates a ListAdapter, and ListAdapter has the ability to generate a set of widgets.
-
Instead of generating widgets, you create a ListAdapter that can greatly improve the frame rate and smoothness of a page.
-
Effect-Lifecycle-Promote
-
Component effects follow the Widget’s life cycle, and Adapter effects follow the Widget’s life cycle.
-
Effect improves, greatly uncoupling the business logic and view life, and the ability of other modules to invoke it through dispatch-API, even if its presentation is not yet available.
-
Notice to appear | disappear
-
Due to the Effect the life cycle of ascension, we can be more careful to distinguish the init | the dispose and appear or disappear. This is indistinguishable in the Component model.
-
Reducer is long-lived, Effect is medium-lived, View is short-lived.
3. Auto-Dispose
It is a very easy way to manage life cycle objects. An auto-Dispose object can release itself voluntarily or when the managed object it follows is released. Context used in Effect, as well as effectParts in HigherEffect, are auto-Dispose objects. So we can easily host custom life cycle management objects to them.
class ItemWidgetBindingObserver extends WidgetsBindingObserver
with AutoDispose {
ItemWidgetBindingObserver() : super() {
WidgetsBinding.instance.addObserver(this);
}
@override
void didChangeAppLifecycleState(AppLifecycleState state) {
if(AppConfig.flutterBinding.framesEnabled && state == AppLifecycleState.resumed) { AppConfig.flutterBinding.performReassemble(); }}@override
void dispose() {
super.dispose();
WidgetsBinding.instance.removeObserver(this); }}void _init(Action action, Context<ItemPageContainerState> ctx) {
final ItemWidgetBindingObserver observer = ItemWidgetBindingObserver();
observer.follow(ctx);
}
Copy the code
4. Connector
It expresses how small data is read from a big data, and how changes to the small data are synchronized to the big data, such data connections. It is the key to assemble a centralized Reducer automatically from multi-level and multi-module small Reducer. It greatly reduces the complexity of using Redux. We are no longer concerned with the assembly process, we are concerned with what actions at the core drive how the data changes. It is used in the configuration Dependencies, where we fix the connection between the big component and the widget (data pipeline), so we don’t need to pass in any dynamic parameters when we use the widget.
class DetialState {
Profile profile;
String message;
}
Connector<DetialState, String> messageConnector() {
return Connector<DetialState, String> (get: (DetialState state) => state.message,
set: (DetialState state, String message) => state.message = message,
);
}
Copy the code
5. Component
A component is an encapsulation of view presentation and logical functionality.
Facing the present, from the perspective of Redux, we divided the components into Reducer and state modification functions. Going forward, from the perspective of UI-Automation, we divide components into presentation and others. Combining the above two perspectives, we have obtained three parts, View, Effect and Reducer, which are called the three elements of the component, responsible for the display of the component, the behavior of non-modifying data and the operation of modifying data respectively. We register widgets and adapters that large components depend on in an explicit configuration called Dependencies.
Component = View + Effect + Reduce (optional)
2. From the point of view of components:
2. From the point of view of Store:
6. CustomAdapter
For custom implementations of large cells, elements are similar to Component, except that the view part of the Adapter returns a ListAdapter.
class CommentAdapter extends Adapter<CommentState> {
CommentAdapter()
: super(
adapter: buildCommentAdapter,
effect: buildCommentEffect(),
reducer: buildCommentReducer(),
);
}
ListAdapter buildCommentAdapter(CommentState state, Dispatch dispatch, ViewService service) {
final List<IndexedWidgetBuilder> builders = Collections.compact(<IndexedWidgetBuilder>[] .. add((BuildContext buildContext,intindex) => _buildDetailCommentHeader(state, dispatch, service)) .. addAll(_buildCommentViewList(state, dispatch, service)) .. add(isEmpty(state.commentListRes?.items) ? (BuildContext buildContext,int index) =>
_buildDetailCommentEmpty(state.itemInfo, dispatch)
: null)
..add(state.commentListRes?.getHasMore() == true
? (BuildContext buildContext, int index) => _buildLoadMore(dispatch)
: null));
return ListAdapter(
(BuildContext buildContext, int index) =>
builders[index](buildContext, index),
builders.length,
);
}
Copy the code
7. Dependencies
Dependencies are a structure that expresses Dependencies between components. It accepts two fields
-
slots
-
string
It contains three main aspects of information
-
Slots, component-dependent slots.
-
Adapter, the specific adapter that the component depends on to build a high-performance ListView.
-
Dependent is a combination of subComponent or subAdapter + connector.
-
The Reducer of a Component is automatically compounded from the Reducer configured by the Component itself and all the sub-reducers under its Dependencies.
// register in component
class ItemComponent extends ItemComponent<ItemState> {
ItemComponent()
: super(
view: buildItemView,
reducer: buildItemReducer(),
dependencies: Dependencies<ItemState>(
slots: <String, Dependent<ItemState>>{
'appBar': AppBarComponent().asDependent(AppBarConnector()),
'body': ItemBodyComponent().asDependent(ItemBodyConnector()),
'ad_ball': ADBallComponent().asDependent(ADBallConnector()),
'bottomBar': BottomBarComponent().asDependent(BottomBarConnector()),
},
),
);
}
// call in view
Widget buildItemView(ItemState state, Dispatch dispatch, ViewService service) {
return Scaffold(
body: Stack(
children: <Widget>[
service.buildComponent('body'),
service.buildComponent('ad_ball'),
Positioned(
child: service.buildComponent('bottomBar'),
left: 0.0,
bottom: 0.0,
right: 0.0,
height: 100.0,
),
],
),
appBar: AppbarPreferSize(child: service.buildComponent('appBar')));
}
Copy the code
8. Dependent
Dependent = connector + subComponent | subAdapter combination, it expresses the widget | how the small adapter is connected to the Component.
9. Directory
The recommended directory structure would look like this:
sample_page
-- action.dart
-- page.dart
-- view.dart
-- effect.dart
-- reducer.dart
-- state.dart
components
sample_component
-- action.dart
-- component.dart
-- view.dart
-- effect.dart
-- reducer.dart
-- state.dart
Copy the code
The upper layer is responsible for assembly and the lower layer is responsible for implementation.
Optimization of Fish Redux
Redux is a framework focused on state management; Fish Redux is an application framework for state management based on Redux. Application frameworks should not only solve the problem of state management, but also solve the problem of divide-and-conquer, communication, data drive, decoupling and so on.
Redux completes the merging process from small Reducer to main Reducer through the form of user’s manual organization code. By explicitly expressing the dependencies between components, Fish Redux automatically completes the merging process from fine Reducer to main Reducer by the framework.
Fish Redux provides an Adapter abstract component model, in addition to the basic component model, to solve the performance problems of large cells on the ListView. Through the upper level abstraction, you can get a logical ScrollView and a performance ListView.
Six, MobX library
I believe that when people started using Flutter, most projects were written in Flutter. One day you will encounter the mountain of setState(), and you won’t be able to escape. It handles a lot of classes at once, loads up on dynamic data, and makes code ugly and snacky to write; And it can be a major drag on application performance because you have to constantly rebuild the widget tree from top to bottom, even if the variable values change slightly.
Flutter-provide and Fish Redux state management libraries have been introduced previously. Those interested in Fish Redux can refer to my previous analysis article: Design Principles of Fish-Redux (Part 1). MobX today… Huh??????? This is not the usual mobX state management for front-end React. Yes, it is again. It now also has mobX Flutter. MobX is a well-regarded library that incorporates functional responsive programming (TFRP) principles to simplify state management and make it easy to expand.
reference
Alibaba Fish-Redux address: github.com/alibaba/fis…
MobX Flutter: pub. Dev/packages/mo…