开发环境:
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)Action
enum MessageAction {
onShare,
shared,
}
class MessageActionCreator {
static Action onShare(Map payload) {
return Action(MessageAction.onShare, payload: payload);
}
static Action shared() {
return const Action(MessageAction.shared);
}
}
2)Reducer
/// 第一种写法
String messageReducer(String msg, Action action) {
if (action.type == 'shared') {
return '$msg [shared]';
}
return msg;
}
class MessageComponent extends Component {
MessageComponent(): super(
view: buildMessageView,
effect: buildEffect(),
reducer: messageReducer,
);
}
/// 第二种写法
Reducer buildMessageReducer() {
return asReducer(
3)Effect
Effect顾名思义,用于处理Action的副作用。
Effect用法跟Reducer差不太多,但是作用完全不同。
你可以通过控制effect的返回值来达到某些目的,默认情况下,effect会在reducer之前被执行。
当前effect返回 true 的时候,就会停止后续的effect和reducer的操作
当前effect返回 false 的时候,后续effect和reducer继续执行
4)Adapter
更多的概念参考官方说明
也可直接查看示例注释
app.dart 和 main.dart 是项目入口文件,global_store下全局状态相关,todo_list_page下待办事项列表相关,todo_edit_page下编辑事项相关。
在VSCode中搜索插件 fish-redux-template 安装,方便生成模版文件。
接下来,就要利用插件 fish-redux-template 创建模版文件。
1)遵循redux原则,store必须单一,使用 createStore() 创建:
static Store get store => _globalStore ??= createStore(GlobalState(), buildReducer());
示例中有改变主题色的需求(字体大小先忽略),因此定义state:
class GlobalState implements GlobalBaseState, Cloneable {
//重写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 buildReducer() {
return asReducer(
2)通过 看图分析 得知,首页面分为三部分,公告栏(report)、 列表栏(todo_list 和 adapter)和悬浮按钮。
2.1)由此可以得出,page的action分为两种,初始化(initTodos ) 和 增加事项(onAdd)。
enum TodoListAction {
initTodos, //初始化列表
onAdd //增加待办事项
}
state默认继承Cloneable,还需要继承GlobalBaseState,用于全局修改主题色。定义List,用于保存数据:
class TodoListState implements GlobalBaseState, Cloneable {
List 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 todos = action.payload ?? [];
final TodoListState newState = state.clone();
newState.todos = todos;
return newState;
}
view是纯界面布局,派发action。注意加载公告栏组件和列表组件的方法。
page整合其他文件。
2.2)公告栏组件
公告栏要展示事项总数及完成个数,因此只需state来保存这两个数据:
class ReportState implements Cloneable {
//总数,以及完成个数
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 {
@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(
adapter: NoneConn() + ListA.ListAdapter(),
slots: >{
'report': ReportConnector() + ReportComponent()
}),
2.3)列表组件
使用fish-redux提供的adapter:
class ListAdapter extends DynamicFlowAdapter {
ListAdapter()
: super(
pool: >{
'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 buildReducer() {
return asReducer(
因为设计的是长按cell删除该行(而不是adapter或者说是列表),所以remove放在todo_component下的action中,但是实际处理应该是adapter的reducer。
2.4)cell组件
在state.dart中定义每条事项的属性:
class TodoState implements Cloneable {
String uniqueId;
String title;
String desc;
bool isDone;
在action.dart中定义行为:
enum TodoAction {
onEdit,
edit,
done,
onRemove,
remove
}
在effect.dart中处理 onEdit和 onRemove:
Effect buildEffect() {
return combineEffects(
在reducer.dart中处理从effect派发的edit,以及从view中派发的done:
Reducer buildReducer() {
return asReducer(
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地址
fish-redux 地址
=================================================================
个人博客
Github
个人公众号:Flutter小同学
个人网站