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意图;

疑问

  • 在Redux架构下,对话框和页面跳转应该如何处理?这些都不是常规的页面状态,因为它们都不能被作为普通的页面元素,而是有专门的API,无法通过一个通常的render方法表现出对话框或者页面跳转。
    • 可以从另一个角度看,flutter中对话框和页面跳转都是通过Navigation来操作的,并且它们都会返回一个Future,在对话框关闭或页面返回时,这个Future会resolve,这样,我们就可以将flutter的对话框或者页面跳转看作是一个effect,通过middleware等待并接收其结果,最终dispatch同步事件给reducer,对话框和页面跳转本身不作为state的一部分,而是被作为一种effect;
      • 如果页面不会再次被显示,直接被从隐藏状态被pop调了,这个future会直接resolve,这时候不需要dispatch任何东西 - 页面马上就被干掉了,什么都不用做,不必担心内存泄漏;
      • 等待future的过程中,页面还可以响应任何其他的event,只要做好状态管理就没问题;

例子

下面是一个什么都没做,但包含所有redux组成部分的例子(但这里直接用了State作为ViewModel):

  1. import 'package:flutter/foundation.dart';
  2. import 'package:redux/redux.dart';
  3. enum Actions {
  4. INPUT_CHANGE, // 记录输入变化
  5. CLICK_LOGIN, // 点击登录,显示loading并发起请求
  6. }
  7. enum Inputs {
  8. USERNAME,
  9. PASSWORD,
  10. }
  11. enum PageState {
  12. IDLE,
  13. LOADING,
  14. }
  15. class LoginState {
  16. String username;
  17. String password;
  18. PageState pageState;
  19. LoginState.empty()
  20. : this(username: '', password: '', pageState: PageState.IDLE);
  21. //<editor-fold desc="Data Methods" defaultstate="collapsed">
  22. LoginState({
  23. @required this.username,
  24. @required this.password,
  25. @required this.pageState,
  26. });
  27. @override
  28. bool operator ==(Object other) =>
  29. identical(this, other) ||
  30. (other is LoginState &&
  31. runtimeType == other.runtimeType &&
  32. username == other.username &&
  33. password == other.password &&
  34. pageState == other.pageState);
  35. @override
  36. int get hashCode =>
  37. username.hashCode ^ password.hashCode ^ pageState.hashCode;
  38. @override
  39. String toString() {
  40. return 'LoginState{' +
  41. ' username: $username,' +
  42. ' password: $password,' +
  43. ' pageState: $pageState,' +
  44. '}';
  45. }
  46. LoginState copyWith({
  47. String username,
  48. String password,
  49. PageState pageState,
  50. }) {
  51. return new LoginState(
  52. username: username ?? this.username,
  53. password: password ?? this.password,
  54. pageState: pageState ?? this.pageState,
  55. );
  56. }
  57. Map<String, dynamic> toMap() {
  58. return {
  59. 'username': this.username,
  60. 'password': this.password,
  61. 'pageState': this.pageState,
  62. };
  63. }
  64. factory LoginState.fromMap(Map<String, dynamic> map) {
  65. return new LoginState(
  66. username: map['username'] as String,
  67. password: map['password'] as String,
  68. pageState: map['pageState'] as PageState,
  69. );
  70. }
  71. //</editor-fold>
  72. }
  73. class InputChangeAction {
  74. String input;
  75. Inputs which;
  76. static LoginState updateState(LoginState state, InputChangeAction action) {
  77. LoginState result = state;
  78. switch (action.which) {
  79. case Inputs.USERNAME:
  80. result = state.copyWith(username: action.input);
  81. break;
  82. case Inputs.PASSWORD:
  83. result = state.copyWith(password: action.input);
  84. break;
  85. }
  86. return result;
  87. }
  88. }
  89. class ClickLoginAction {}
  90. LoginState inputChangeReducer(LoginState state, InputChangeAction action) {
  91. return InputChangeAction.updateState(state, action);
  92. }
  93. LoginState clickLoginReducer(LoginState state, ClickLoginAction action) {
  94. return null;
  95. }
  96. var reducer = combineReducers<LoginState>([
  97. TypedReducer<LoginState, InputChangeAction>(inputChangeReducer),
  98. TypedReducer<LoginState, ClickLoginAction>(clickLoginReducer),
  99. ]);
  100. void loadItemsMiddleware(
  101. Store<LoginState> store,
  102. ClickLoginAction action,
  103. NextDispatcher next,
  104. ) {
  105. next(action);
  106. }
  107. List<Middleware<LoginState>> middleware = [
  108. TypedMiddleware<LoginState, ClickLoginAction>(loadItemsMiddleware),
  109. ];