开发环境:
Mac OS 10.14.5
VSCode 1.36.1
Flutter 1.9.1+hotfix.2
前言
这是第二篇关于状态管理的文章。第一篇见 Flutter示例系列(二)之状态管理-壹(scoped_model)。
写过前端React或者RN的大概都接触过Redux,本文主要讲述阿里团队开发的 fish-redux 框架。还有一个使用较多的框架 flutter-redux,想要了解的可以参考此博文。
概念
1. fish-redux 的前世今生?
fish-redux 是一个基于 Redux 数据管理的组装式 flutter 应用框架, 它特别适用于构建中大型的复杂应用。
它的特点是配置式组装。 一方面我们将一个大的页面,对视图和数据层层拆解为互相独立的 Component|Adapter,上层负责组装,下层负责实现; 另一方面将 Component|Adapter 拆分为 View,Reducer,Effect 等相互独立的上下文无关函数。
所以它会非常干净,易维护,易协作。
fish-redux 的灵感主要来自于 Redux, Elm, Dva 这样的优秀框架。而 fish-redux 站在巨人的肩膀上,将集中,分治,复用,隔离做的更进一步。
官方文档声明,redux适用与中大型项目,所以如果觉得小项目不需要redux管理数据,完全可以不使用,之前没了解过的话,学习起来是有些难度的,用起来较为复杂。
2. 那么fish-redux 和 redux 有什么不同呢?
1)它们是解决不同层面问题的两个框架
Redux 是一个专注于状态管理的框架;fish-redux 是基于 Redux 做状态管理的应用框架。
应用框架不仅仅要解决状态管理的问题,还要解决分治,通信,数据驱动,解耦等等问题。
2)fish-redux 解决了集中和分治的矛盾。
Redux 通过使用者手动组织代码的形式来完成从小的 Reducer 到主 Reducer 的合并过程;
fish-redux 通过显式的表达组件之间的依赖关系,由框架自动完成从细力度的 Reducer 到主 Reducer 的合并过程;
3)fish-redux 提供了一个简单的组件抽象模型
它通过简单的 3 个函数组合而成
4)fish-redux 提供了一个 Adapter 的抽象组件模型
在基础的组件模型以外,fish-redux 提供了一个 Adapter 抽象模型,用来解决在 ListView 上大 Cell 的性能问题。
通过上层抽象,我们得到了逻辑上的 ScrollView,性能上的 ListView。
3. fish-redux提供哪些重要类?
1)ActionAction 包含两个字段
type
payload
推荐的写法是
为一个组件|适配器创建一个 action.dart 文件,包含两个类
为 type 字段起一个枚举类
为 Action 的创建起一个 ActionCreator 类,这样利于约束 payload 的类型。
Effect 接受处理的 Action,以 on{Verb} 命名
Reducer 接受处理的 Action,以{verb} 命名
示例代码
enum MessageAction {
onShare,
shared,
}
class MessageActionCreator {
static Action onShare(Map<String, Object> payload) {
return Action(MessageAction.onShare, payload: payload);
}
static Action shared() {
return const Action(MessageAction.shared);
}
}
2)ReducerReducer 是一个上下文无关的 pure function。它接收下面的参数
T state
Action action
它主要包含三方面的信息
接收一个“意图”, 做出数据修改。
如果要修改数据,需要创建一份新的拷贝,修改在拷贝上。
如果数据修改了,它会自动触发 State 的层层数据的拷贝,再以扁平化方式通知组件刷新。
示例代码
/// 第一种写法
String messageReducer(String msg, Action action) {
if (action.type == 'shared') {
return '$msg [shared]';
}
return msg;
}
class MessageComponent extends Component<String> {
MessageComponent(): super(
view: buildMessageView,
effect: buildEffect(),
reducer: messageReducer,
);
}
/// 第二种写法
Reducer<String> buildMessageReducer() {
return asReducer(<Object, Reducer<String>>{
'shared': _shared,
});
}
String _shared(String msg, Action action) {
return '$msg [shared]';
}
class MessageComponent extends Component<String> {
MessageComponent(): super(
view: buildMessageView,
effect: buildEffect(),
reducer: buildMessageReducer(),
);
}
//推荐的是第二种写法
3)Effect
Effect顾名思义,用于处理Action的副作用。
Effect用法跟Reducer差不太多,但是作用完全不同。
你可以通过控制effect的返回值来达到某些目的,默认情况下,effect会在reducer之前被执行。
当前effect返回 true 的时候,就会停止后续的effect和reducer的操作
当前effect返回 false 的时候,后续effect和reducer继续执行
Effect 是一个处理所有副作用的函数。它接收下面的参数
Action action
Context context
BuildContext context
T state
dispatch
isDisposed
Effect会接收来自 View 的“意图”,包括对应的生命周期的回调,然后做出具体的执行。 - 它的处理可能是一个异步函数,数据可能在过程中被修改,所以我们应该通过 context.state 获取最新数据。 - 如果它要修改数据,应该发一个 Action 到 Reducer 里去处理。它对数据是只读的,不能直接去修改数据。 - 如果它的返回值是一个非空值,则代表自己优先处理,不再做下一步的动作;否则广播给其他组件的 Effect 部分,同时发送给 Reducer。
4)Adapter
在基础 Component 的概念外,额外增加了一种组件化的抽象 Adapter。它的目标是解决 Component 模型在 ListView 的场景下的 3 个问题
1)将一个"Big-Cell"放在 ListView 里,无法享受 ListView 代码的性能优化。
2)Component 无法区分 appear|disappear 和 init|dispose 事件。
3)Effect 的生命周期和 View 的耦合,在 ListView 的有些场景下不符合直观的预期。
一个 Adapter 和 Component 几乎都是一致的,除了以下几点
Component 生成一个 Widget,Adapter 生成一个 ListAdapter,ListAdapter 有能力生成一组 Widget。
不具体生成 Widget,而是一个 ListAdapter,能非常大的提升页面帧率和流畅度。
Effect-Lifecycle-Promote
Component 的 Effect 是跟着 Widget 的生命周期走的,Adapter 的 Effect 是跟着上一级的 Widget 的生命周期走。
Effect 提升,极大的解除了业务逻辑和视图生命的耦合,即使它的展示还未出现,的其他模块依然能通过 dispatch-api,调用它的能力。
appear|disappear 的通知
由于 Effect 生命周期的提升,我们就能更加精细的区分 init|dispose 和 appear|disappear。而这在 Component 的模型中是无法区分的。
Reducer is long-lived, Effect is medium-lived, View is short-lived.
更多的概念参考官方说明
也可直接查看示例注释
探索
示例结构如下,是一个简单的待办事项app:
app.dart 和 main.dart 是项目入口文件,global_store下全局状态相关,todo_list_page下待办事项列表相关,todo_edit_page下编辑事项相关。
在VSCode中搜索插件 fish-redux-template 安装,方便生成模版文件。
看图分析
通过上面两张截图,可以发现示例分成两个页面(page),即todo_list_page 和 todo_edit_page,todo_list_page 包含公告栏(report_component)、列表(list_adapter+todo_component)和一个悬浮按钮。todo_edit_page 比较简单,只创建一个view展示,但是有一个’Change Theme’按钮,作用是改变整个主题,所以用global_store来管理。
接下来,就要利用插件 fish-redux-template 创建模版文件。
代码思路
结合代码一起理解,示例中有大量注释。
1)遵循redux原则,store必须单一,使用 createStore() 创建:
static Store<GlobalState> get store => _globalStore ??= createStore(GlobalState(), buildReducer());
示例中有改变主题色的需求(字体大小先忽略),因此定义state:
class GlobalState implements GlobalBaseState, Cloneable<GlobalState> {
//重写GlobalBaseState的属性
@override
Color themeColor;
int themeTextSize;
//重写Cloneable的方法,在reducer处理action之后,需要获取state对象,通过state对象获取其实例变量
@override
GlobalState clone() {
return GlobalState();
}
}
改变即是行为,定义action:
enum GlobalAction {
changeThemeColor,
changeTextSize,
}
//action生成器
class GlobalActionCreator {
//如果发出 改变主题色动作,则调用此方法
static Action changeThemeColor() {
return const Action(GlobalAction.changeThemeColor);
}
对应action定义处理函数reducer:
//Reducer 创建格式如下,只需要补充 相应的action以及对应的处理函数
Reducer<GlobalState> buildReducer() {
return asReducer(
<Object, Reducer<GlobalState>> {
GlobalAction.changeThemeColor: _changeThemeColor,
GlobalAction.changeTextSize: _changeTextSize,
}
);
}
2)通过 看图分析 得知,首页面分为三部分,公告栏(report)、 列表栏(todo_list 和 adapter)和悬浮按钮。
2.1)由此可以得出,page的action分为两种,初始化(initTodos ) 和 增加事项(onAdd)。
enum TodoListAction {
initTodos, //初始化列表
onAdd //增加待办事项
}
state默认继承Cloneable,还需要继承GlobalBaseState,用于全局修改主题色。定义List,用于保存数据:
class TodoListState implements GlobalBaseState, Cloneable<TodoListState> {
List<TodoState> todos;
为什么要用onAdd,意味着过程变得复杂,action需要先派发给effect,处理之后再决定是否继续派发给reducer(这种顺序是fish-redux特性)?
因为在effect处做了一个判断,从编辑页面传回的数据如果不完整,则不能添加进list,也不能显示在视图上。
Navigator.of(ctx.context).pushNamed('todo_edit', arguments: null).then((dynamic todo) {
//从编辑页面返回的 回调,判断下面条件,为true则向reducer派发add行为
if(todo != null && (todo.title?.isNotEmpty == true || todo.desc?.isNotEmpty == true)) {
ctx.dispatch(list_action.ListActionCreator.add(todo));
}
});
//注意:此刻是传递给adapter。
effect还做了一个操作,即初始化数据,然后,派发给reducer:
//从effect 派发任务到 reducer,effect只负责读取数据,处理数据只能reducer
ctx.dispatch(TodoListActionCreator.initTodosAction(initTodos));
reducer更新state:
TodoListState _initTodosReducer(TodoListState state, Action action) {
final List<TodoState> todos = action.payload ?? <TodoState>[];
final TodoListState newState = state.clone();
newState.todos = todos;
return newState;
}
view是纯界面布局,派发action。注意加载公告栏组件和列表组件的方法。
page整合其他文件。
2.2)公告栏组件
公告栏要展示事项总数及完成个数,因此只需state来保存这两个数据:
class ReportState implements Cloneable<ReportState> {
//总数,以及完成个数
int total;
int done;
//构造方法,默认都为0
ReportState({this.total = 0, this.done = 0});
view是纯界面布局。
component同page相似。
关键是怎么拿到数据?
fish-redux在page中提供了dependencies,配置了大组件和小组件的连接关系。使用connector可以从大数据中读取小数据,同时小数据的修改同步到大数据。
在todo_list_page下的state中,定义ReportConnector:
class ReportConnector extends Reselect2<TodoListState, ReportState, int, int> {
@override
ReportState computed(int sub0, int sub1) {
return ReportState()
..done = sub0
..total = sub1;
}
@override
int getSub0(TodoListState state) {
return state.todos.where((TodoState tds) => tds.isDone).toList().length;
}
@override
int getSub1(TodoListState state) {
return state.todos.length;
}
@override
void set(TodoListState state, ReportState subState) {
throw Exception('Unexpected to set PageState from ReportState');
}
然后再page中配置dependencies,ReportConnector绑定ReportComponent,并且还配置了adapter组件:
dependencies: Dependencies<TodoListState>(
adapter: NoneConn<TodoListState>() + ListA.ListAdapter(),
slots: <String, Dependent<TodoListState>>{
'report': ReportConnector() + ReportComponent()
}),
2.3)列表组件
使用fish-redux提供的adapter:
class ListAdapter extends DynamicFlowAdapter<TodoListState> {
ListAdapter()
: super(
pool: <String, Component<Object>>{
'todo': TodoComponent(),
},
connector: _ListConnector(),
reducer: buildReducer(),
);
}
列表有 增加 和 删除 的功能,但是 list_adapter下的action只有add?继续往下看:
enum ListAction { add }
class ListActionCreator {
static Action add(TodoState state) {
return Action(ListAction.add, payload: state);
}
}
别担心,请看reducer,嗯,多了remove?继续往下看:
Reducer<TodoListState> buildReducer() {
return asReducer(
<Object, Reducer<TodoListState>>{
ListAction.add: _add,
todo_action.TodoAction.remove: _remove
},
);
}
因为设计的是长按cell删除该行(而不是adapter或者说是列表),所以remove放在todo_component下的action中,但是实际处理应该是adapter的reducer。
2.4)cell组件
在state.dart中定义每条事项的属性:
class TodoState implements Cloneable<TodoState> {
String uniqueId;
String title;
String desc;
bool isDone;
在action.dart中定义行为:
enum TodoAction {
onEdit,
edit,
done,
onRemove,
remove
}
在effect.dart中处理 onEdit和 onRemove:
Effect<TodoState> buildEffect() {
return combineEffects(<Object, Effect<TodoState>>{
TodoAction.onEdit: _onEdit,
TodoAction.onRemove: _onRemove,
});
}
在reducer.dart中处理从effect派发的edit,以及从view中派发的done:
Reducer<TodoState> buildReducer() {
return asReducer(
<Object, Reducer<TodoState>>{
TodoAction.edit: _edit,
TodoAction.done: _done,
},
);
}
view是纯界面布局,派发action。
component同page相似。
2.5)编辑页面(分析大致如上所述)
3.)app.dart
3.1)配置路由,然后使用 connectExtraStore 方法,将page 的 store 连接到 app-store,页面的state同appState始终保持一致。
3.2)示例牵扯到AOP,做一些简单了解。
面向切面编程,主要作用是在不影响原有逻辑的基础上,增添一些常用的功能(比如打印日志、安全检查)。与业务逻辑解耦合,无侵入性。
为此阿里闲鱼团队开源AspectD,AOP for Flutter。
参考博文:
《重磅开源|AOP for Flutter开发利器——AspectD》
关于Middleware
思考?
1.为什么引入material.dart 时,要 hide Action?
因为要使用 fish_redux提供的Action,而非 material.dart 的,避免冲突。
总结
本文主要讲述 fish-redux的起源以及主要用法,并详细分析了示例代码。结合Demo注释会更加清晰,动手尝试效果更佳。
本文Demo地址:https://github.com/cxymq/FlutterDemo
fish-redux 地址:https://github.com/alibaba/fish-redux
————————————————
版权声明:本文为CSDN博主「夏目三三」的原创文章,遵循CC 4.0 BY-SA版权协议,转载请附上原文出处链接及本声明。
原文链接:https://blog.csdn.net/Crazy_SunShine/article/details/101430068/