Flutter 页面状态管理 & MvRx方案设计

1,479 阅读8分钟

状态管理

官方介绍地址:State management

主流状态管理,官方推荐地址:状态管理参考

Flutter的 状态管理 跟 反应式应用程序中的状态管理 很像。状态管理的作用是管理组件中需要重建的数据。

状态: 当任何时候需要重建用户界面时所需要的数据,该数据可以称为页面的状态。

image

Android : 命令式框架中修改 UI

ViewB b = new ViewB(context)
b.setColor(red)
b.clearChildren()
ViewC c3 = new ViewC(...)
b.add(c3)

Flutter:声明式修改UI

return ViewB(
  color: red,
  child: ViewC(...),
)

当用户界面发生变化时,Flutter 不会修改旧的实例 b,而是构造新的 widget 实例。框架使用 RenderObjects 管理。 RenderObjects 在帧之间保持不变, Flutter 的轻量级 widget 通知框架在状态之间修改 RenderObjects, Flutter 框架则处理其余部分。

Ephemeral & App State

需要自己 管理 的状态可以分为两种概念类型:短时 (ephemeral) 状态和应用 (app) 状态。

短时状态:

有时也称 用户界面 (UI) 状态 或者 **局部状态,**是可以完全包含在一个独立 widget 中的状态。

这种情况不需要使用状态管理架构(例如 ScopedModel, Redux)去管理这种状态,需要用的只是一个 StatefulWidget。

应用状态:

应用中的多个部分之间共享一个非短时的状态,并且在用户会话期间保留这个状态,称之为应用状态(有时也称共享状态)。

页面状态管理现状:

目前项目中主要使用两种方案解决页面状态管理:

  • Flutter 原生的 setState

  • 非常🔥的一个Flutter官方推荐的开源Provider

setState:

  1. 需要结合 StatefullWidget 使用

  2. 短时状态经常被用于一个单独 widget 的本地状态,通常使用 State 和 setState() 来实现。

setState 问题:

  1. 刷新是大范围的整体刷新,而不是精细的刷新

  2. 只有 StatefullWidget 即有状态的 widget,才支持 setState 方式进行刷新

setState 优点:

  1. 官方提供,使用极简单

  2. 调用无需context

provider:

工程结构:

image

Flutter 官方推荐,provider 非常好理解而且不需要写很多代码。

底层是使用了InheritedWidget, InheritedNotifier, InheritedModel等能力。InheritedWidget是Flutter中特别重要的一个Widget,它提供了一种数据在widget树中从上到下传递、共享的方式。

provider 核心类

  • ChangeNotifier

  • ChangeNotifierProvider

  • Consumer

ChangeNotifier:

它用于向监听器发送通知。换言之,如果被定义为 ChangeNotifier,你可以订阅它的状态变化。

image

ChangeNotifierProvider:

ChangeNotifierProvider widget 可以向其子孙节点暴露一个 ChangeNotifier 实例。

image

image

Consumer:

当继承于ChangeNotifier 子类 通过 ChangeNotifierProvider 在应用中与 widget 相关联,则可以使用 Consumer widget。

image

必须指定要访问的模型类型。在这个示例中,要访问 CartModel 那么就写上 Consumer<CartModel>。

Consumer widget 唯一必须的参数就是 builder。当 ChangeNotifier 发生变化的时候会调用 builder 这个函数。(换言之,当调用 notifyListeners() 时,所有相关的 Consumer widget 的 builder 方法都会被调用。)

Provider.of

使用 Provider.of,并且将 listen 设置为 false。用于访问数据,但又不希望改变ui的场景。

provider 问题:

  1. 需要context 强绑定

  2. 不能刷新屏幕外页面

  3. 不支持响应式变成

provider 优点:

  1. 使用简单,大约2天内掌握使用。

  2. 调试方便

其他:基于provider的页面多状态模板代码设计

一个网络页面,加载网络数据可能分为:加载中、请求成功、请求失败、暂无数据状态,而这些完全可以模板代码的方式进行提供。

对应 widget 模板代码:

class EkLoadWidget<P extends EkPageLoadProvider> extends StatelessWidget {
  final Widget Function(BuildContext context) success;
  final Widget Function(BuildContext context) failed;
  final Widget Function(BuildContext context) netFailed;
  final Widget Function(BuildContext context) noData;
  final Widget Function(BuildContext context) loading;

  EkLoadWidget({this.success, this.failed, this.netFailed, this.noData, this.loading});

  @override
  Widget build(BuildContext context) {
    return SafeArea(
        child: Selector<P, EkPageState>(
      selector: (context, provider) => provider.pageStatus,
      builder: (_, pageStatus, __) {
        switch (pageStatus) {
          case EkPageState.SUCCESS:
            return _buildLoadSuccess(context);
          case EkPageState.FAILED:
            return _buildLoadFailed(context);
          case EkPageState.NET_FAILED:
            return _buildLoadNetFailed(context);
          case EkPageState.NO_DATA:
            return _buildLoadNoData(context);
          default:
            return _buildLoading(context);
        }
      },
    ));
  }

  Widget _buildLoadSuccess(BuildContext context) {
    if (success == null) return Container();
    return success(context);
  }

  Widget _buildLoadFailed(BuildContext context) {
    if (failed == null) return Container();
    return failed(context);
  }

  Widget _buildLoadNetFailed(BuildContext context) {
    if (netFailed == null) return Container();
    return netFailed(context);
  }

  Widget _buildLoadNoData(BuildContext context) {
    if (noData == null) return Container();
    return noData(context);
  }

  Widget _buildLoading(BuildContext context) {
    if (loading == null)
      return Center(
        child: CircularProgressIndicator(),
      );
    return loading(context);
  }
}

使用方只需要传参 success、failed、netFailed、noData、loading 即可。

对应 provider 模板代码:

class EkPageLoadProvider extends EkChangeNotifier {
  EkPageState _pageStatus = EkPageState.LOADING;

  EkPageState get pageStatus => _pageStatus;

  set pageStatus(EkPageState state) {
    if (state != null && state != _pageStatus) _pageStatus = state;
    notifyListeners();
  }

  void setPageStatusOnly(EkPageState state){
    if (state != null && state != _pageStatus) _pageStatus = state;
  }

  void notifyLoading() {
    if (_pageStatus != EkPageState.LOADING) _pageStatus = EkPageState.LOADING;
    notifyListeners();
  }

  void notifySuccess() {
    if (_pageStatus != EkPageState.SUCCESS) _pageStatus = EkPageState.SUCCESS;
    notifyListeners();
  }

  void notifyFailed() {
    if (_pageStatus != EkPageState.FAILED) _pageStatus = EkPageState.FAILED;
    notifyListeners();
  }

  void notifyNetFailed() {
    if (_pageStatus != EkPageState.NET_FAILED) _pageStatus = EkPageState.NET_FAILED;
    notifyListeners();
  }

  void notifyNoData() {
    if (_pageStatus != EkPageState.NO_DATA) _pageStatus = EkPageState.NO_DATA;
    notifyListeners();
  }
}

提供了各种状态的 notify,使用方按需调用即可。

GetX:

GetX , 现在是热度最高🔥的一个页面状态管理框架,Getx提供了三大部分的能力:路由管理,一系列的工具方法,以及状态管理。

image

getX问题:

  1. GetX 涉及 路由管理、一些工具方法,实际上是一个大杂烩的组件,缺失单一性设计原则

  2. 路由管理使用复杂度很高,项目使用、改造成本很大,收益反而很小。

getX优点:

  1. 一个简单的响应式状态管理解决方案。

  2. 使用相对简单

  3. 调用无需context,如果需要,可以全局调用

getX 看法

  1. getX 是一个大杂烩的组件,好在整体代码不多,而且单独个功能之间耦合度低,可以按需分开使用。

  2. 虽然 GetX 有缺点,但是亮点(响应式状态解决) 也是很有吸引力的,完全可以去其糙泊取其精华,即使用 GetX 状态管理的响应式能力。

  3. 单纯状态管理场景,代码迁移成本不高。

GetX 响应式使用示例:

  1. 声明一个响应式变量
//1. .obs 扩展
var countObs = 0.obs;  === RxInt(0)  // ''.obs [].obs  {}.obs  user.obs

//2. Rx类  
var count = RxInt(0); // RxSting, RxBool,RxNum, RxMap, RxList

//3. Rx泛型
var count = Rx<Int>(0);
var userObs = Rx(user)
  1. 绑定widget
Obx(() => Text('${countObs.value}'))
  1. 更新变量的值,自动更新对应的widget
countObs.value += 1;

字节内状态管理场景实践:

(from Flutter中台,2021年数据)

项目项目类型flutter规模(按Dart代码行粗略归类)选择备注(2021.1.21)
番茄畅听混合setState单一页面,业务探索
特效君纯flutter中小provider
火山混合appprovider + flutter_redux
西瓜混合provider +bloc +mobx
小荷纯flutterbloc现在往getX 转
幸福里纯fluttergetX

主流状态框架比较:

官方框架比较:setState、Provider、Stream 比较

setStateProviderStream
优点1.官方提供
2.需要继承StateFullWidget
3.使用简单
1.官方提供
2.依赖InheritedWidget
3.使用简单
1.官方提供
2.结合StreamBuilder 使用
不足1.不能精确刷新1.需要context
2.存在作用域
3.不能响应式编程
1.功能单一,并不能适合更多复杂场景

其他响应式框架:

StreamRxDartflutter-reduxmobxblocGetX
优点1.官方提供
2.结合StreamBuilder 使用
1.官方提供
2.实际上是使用了Stream能力(继承)
3.提供了丰富的能力
1.流程清晰,套路通用
2.作为状态管理框架,功能完备性好。
1.概念少,易于理解。
2.编码几乎是最简便的,通过注解和codegen减少了样板代码
1.使用了RxDart能力
2.业务逻辑和UI分离较好
1.将依赖管理结合,无context
2.响应编程上手容易,效率较高,提供了很多高级的封装,如futurize(一行代码实现Loading, Success, Error状态处理)
不足1.功能单一,并不能适合更多复杂场景1.使用方式复杂,大范围使用成本高
2.UI页面开发方式单一
1.redux的编码繁琐
2.样板代码较多
3.使用成本高
1.使用成本高
2.注解编译生成dart代码,调试成本高
1.使用方式复杂,大范围使用成本高
2. UI页面开发方式单一
1.不够纯净, 属于混合插件,提供状态管理、路由管理、依赖管理
2. 路由管理过于复杂、改动过大、项目契合度不符

初步结论:

  1. 简单页面刷新 setState,如splash 页面

  2. 较页面使用 Provider,管理数据量少

  3. 如果更复杂页面,如页面多处刷新,页面联动刷新 使用 GetX,而 GetX 建议使用 Rx 能力 响应式刷新。

基于 GetX 响应式刷新能力,页面可以可以采用MVRX 设计。

基于GetX 页面状态管理的 MvRx:

MvRx: ModelView ReactiveX

下面是 MVVM 流程图:

image

View: 在Flutter 中属于 各种 widget,继承 GetX 的 GetView 。

ViewModel: 在Flutter 中 基于 GetX, 使用 GetxController 子类代替,需要响应ui的数据使用 Rx 对应类型,以支持响应编程,刷新ui。

Model: 在Flutter中承担非页面状态的数据管理,负责逻辑处理。

而 MvRx:ViewModel 将被 GetX 响应式的 Rx 模块代替。

Flutter MvRx 元素:

image

四个核心概念对应Flutter 的方案:

  • State:

  • ViewModel:

  • View:

  • Async:

优点:

  1. 响应式更新编程

  2. 简化页面开发逻辑,页面展示、控制逻辑解耦

  3. 使用成本低

缺点:

  1. GetX 组件功能复杂,需要抽丝剥茧,选择有用的模块

  2. 需要一定的理解成本

一个简单的代码示例

下面是一个设置页面是否展示实验室入口的MVVM代码示例:

View 层示例:

class SettingsView extends GetView<SettingsController> {
  @override
  Widget build(BuildContext context) {
    return Scaffold(
      body: Column(
        children: [
          SizedBox(
            height: 20,
          ),
          _buildLogin("登录配置"),
          SizedBox(
            height: 20,
          ),
          _buildDevice("设备配置"),
          SizedBox(
            height: 20,
          ),
          Obx(() => controller.isShowLab.value ? _buildLogin("实验室配置") : SizedBox())
        ],
      ),
    );
  }
}

使用 GetX Obx 意味着,对应的(){} 支持响应式变化。

ViewModel 层示例:

class SettingsController extends GetxController {

  final SettingsModel _settingsModel = SettingsModel();

  final RxBool isShowLab = false.obs;

  @override
  void onInit() {
    super.onInit();
    _settingsModel.requestUrl("xxxxxx").then(config){
      if(config == null) return;
      SettingsConfig settingsConfigRes = config;
      isShowLab.value = settingsConfigRes.labConfig;
    };
  }
}

这里的 bool 类型的 isShowLab,表示是否展示实验室入口,使用 RxBool 意味着 isShowLab 数据变化,对应的 View层对应需要该数据的 widget 会进行对应的刷新。

Model 层示例:

class SettingsModel extends StateModel {

  SettingsConfig _settingsConfig;
  
  SettingsConfig requestUrl(String url,{Map params}) async{
    _settingsConfig =  await SettingsApi.requestSettings(SettingsConfigRes()..url=url..params=params);
    return _settingsConfig;
  }

}

SettingsModel 是用于网络等数据获取。