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,});@overridebool operator ==(Object other) =>identical(this, other) ||(other is LoginState &&runtimeType == other.runtimeType &&username == other.username &&password == other.password &&pageState == other.pageState);@overrideint get hashCode =>username.hashCode ^ password.hashCode ^ pageState.hashCode;@overrideString 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),];
