StatefulWidget.setState
flutter中,最简单的App Widget无需任何状态管理模式,直接利用StatefulWidget的setState即可。
这样的App Widget只有一个顶级StatefulWidget中存放App状态,当setState被调用时,整个App Widget被重建以反映最新状态。简单可靠,就是可能当页面非常复杂时,存在逻辑堆积的问题,并且相比更精细化的状态管理方案,这种粗犷简单的方式不太高效。
不过,对于没那么复杂的页面而言,setState,重建Widget,就完事儿了!
InheritedWidget模式
flutter framework提供了InheritedWidget类,用于进行简单的状态管理。InheritedWidget中存放着App状态,位于InheritedWidget之下的widget可以通过以下方法获得InheritedWidget的引用,同时将自己注册到BuildContext,这样当InheritedWidget状态改变时,widget就能够及时被flutter重建以反映最新状态:
BuildContext.dependOnInheritedWidgetOfExactType
上面的方法名字有点可怕,实际业务中通常会在InheritedWidget中定义一个of方法来获取InheritedWidget状态。事实上flutter中的Theme.of就是这么回事,不过Theme本身并不是一个InheritedWidget,而是一个StatelessWidget,它build出来的子widget是InheritedWidget,通过这种方式,Theme类可以保证只有通过静态的of方法,外部才可能拿到Theme对应的InheritedWidget上的状态,禁止外部直接通过BuildContext.dependOnInheritedWidgetOfExactType获取InheritedWIdget对象,达成了封装的目的(不过看起来也让Theme变得不那么容易理解了)。
Provider模式
在flutter学习那一篇笔记中已经记录:https://www.yuque.com/gaolf/ipvf6y/ancb85#jWVCK
虽然flutter社区里,Provider还挺流行的,但对于复杂一些的App而言,个人还是倾向于Redux一步到位。
Redux模式
之前在学习web时记录过一篇笔记:https://www.yuque.com/gaolf/dmp1cx/oqyf2s;
flutter也提供了方便应用redux模式的包,flutter-redux。
redux对App的开发模式进行了更加严格的限制,但好处是App的状态变化变得清晰可预测。
概念
参考:https://blog.novoda.com/introduction-to-redux-in-flutter/
下面介绍一下flutter-redux究竟都提供了些什么。
flutter-redux包包含了redux,redux提供了所有的必需的Dart组件,包括Store、Reducer和Middleware。
- Store - 状态的容器,构造store时必须提供reducer,以及可选的初始状态、middleware;
- Store的实现使用了Stream,因此在构造时还能够传入一个syncStream参数,默认情况下事件都是异步到达的,不同的;
- Store还允许传入distinct参数,表示如果reducer返回的State和之前完全一样,是否直接忽略;
- Store中存储的状态,按照Redux惯例,必须是不可变对象,声明时需要声明为const的,reducer返回的必须是参考Action和前一个状态而新创建的State对象,Dart由于没有提供方便的克隆对象的途径,目前只能借助于plugin来完成对象clone:dart data class plugin,这个plugin可以为我们自动生成copyWith方法;
- Dispatcher - 是Reducer和Middleware的抽象,Dispatcher是一个函数,接受一个Action,进行处理后将结果交给下一级Dispatcher处理(Middleware)或是直接将结果通知给所有监听Store的Listener(Reducer);
- Reducer - 用于将被Middleware处理过的Action进行最后的处理,返回最终的State;
- redux库提供了combineReducer方法,用于将多个Reducer组合,有助于逻辑拆分;
- redux库提供了TypedReducer,可以针对特定类型的Action进行处理,明确reducer意图;
- Middleware - 用于在Reducer处理Action前,先对Action进行某些处理的函数,Middleware是链式的,当前一个Middleware完成处理后,必须将处理结果交给下一个Middleware;
- 异步逻辑必须在Middleware中处理,
- redux库提供了TypedMiddleware,可以针对特定类型的Action进行处理,明确reducer意图;
- Reducer - 用于将被Middleware处理过的Action进行最后的处理,返回最终的State;
疑问
- 在Redux架构下,对话框和页面跳转应该如何处理?这些都不是常规的页面状态,因为它们都不能被作为普通的页面元素,而是有专门的API,无法通过一个通常的render方法表现出对话框或者页面跳转。
- 可以从另一个角度看,flutter中对话框和页面跳转都是通过Navigation来操作的,并且它们都会返回一个Future,在对话框关闭或页面返回时,这个Future会resolve,这样,我们就可以将flutter的对话框或者页面跳转看作是一个effect,通过middleware等待并接收其结果,最终dispatch同步事件给reducer,对话框和页面跳转本身不作为state的一部分,而是被作为一种effect;
- 如果页面不会再次被显示,直接被从隐藏状态被pop调了,这个future会直接resolve,这时候不需要dispatch任何东西 - 页面马上就被干掉了,什么都不用做,不必担心内存泄漏;
- 等待future的过程中,页面还可以响应任何其他的event,只要做好状态管理就没问题;
- 可以从另一个角度看,flutter中对话框和页面跳转都是通过Navigation来操作的,并且它们都会返回一个Future,在对话框关闭或页面返回时,这个Future会resolve,这样,我们就可以将flutter的对话框或者页面跳转看作是一个effect,通过middleware等待并接收其结果,最终dispatch同步事件给reducer,对话框和页面跳转本身不作为state的一部分,而是被作为一种effect;
例子
下面是一个什么都没做,但包含所有redux组成部分的例子(但这里直接用了State作为ViewModel):
import 'package:flutter/foundation.dart';
import 'package:redux/redux.dart';
enum Actions {
INPUT_CHANGE, // 记录输入变化
CLICK_LOGIN, // 点击登录,显示loading并发起请求
}
enum Inputs {
USERNAME,
PASSWORD,
}
enum PageState {
IDLE,
LOADING,
}
class LoginState {
String username;
String password;
PageState pageState;
LoginState.empty()
: this(username: '', password: '', pageState: PageState.IDLE);
//<editor-fold desc="Data Methods" defaultstate="collapsed">
LoginState({
@required this.username,
@required this.password,
@required this.pageState,
});
@override
bool operator ==(Object other) =>
identical(this, other) ||
(other is LoginState &&
runtimeType == other.runtimeType &&
username == other.username &&
password == other.password &&
pageState == other.pageState);
@override
int get hashCode =>
username.hashCode ^ password.hashCode ^ pageState.hashCode;
@override
String toString() {
return 'LoginState{' +
' username: $username,' +
' password: $password,' +
' pageState: $pageState,' +
'}';
}
LoginState copyWith({
String username,
String password,
PageState pageState,
}) {
return new LoginState(
username: username ?? this.username,
password: password ?? this.password,
pageState: pageState ?? this.pageState,
);
}
Map<String, dynamic> toMap() {
return {
'username': this.username,
'password': this.password,
'pageState': this.pageState,
};
}
factory LoginState.fromMap(Map<String, dynamic> map) {
return new LoginState(
username: map['username'] as String,
password: map['password'] as String,
pageState: map['pageState'] as PageState,
);
}
//</editor-fold>
}
class InputChangeAction {
String input;
Inputs which;
static LoginState updateState(LoginState state, InputChangeAction action) {
LoginState result = state;
switch (action.which) {
case Inputs.USERNAME:
result = state.copyWith(username: action.input);
break;
case Inputs.PASSWORD:
result = state.copyWith(password: action.input);
break;
}
return result;
}
}
class ClickLoginAction {}
LoginState inputChangeReducer(LoginState state, InputChangeAction action) {
return InputChangeAction.updateState(state, action);
}
LoginState clickLoginReducer(LoginState state, ClickLoginAction action) {
return null;
}
var reducer = combineReducers<LoginState>([
TypedReducer<LoginState, InputChangeAction>(inputChangeReducer),
TypedReducer<LoginState, ClickLoginAction>(clickLoginReducer),
]);
void loadItemsMiddleware(
Store<LoginState> store,
ClickLoginAction action,
NextDispatcher next,
) {
next(action);
}
List<Middleware<LoginState>> middleware = [
TypedMiddleware<LoginState, ClickLoginAction>(loadItemsMiddleware),
];