状态管理
官方介绍地址:State management
主流状态管理,官方推荐地址:状态管理参考
Flutter的 状态管理 跟 反应式应用程序中的状态管理 很像。状态管理的作用是管理组件中需要重建的数据。
状态: 当任何时候需要重建用户界面时所需要的数据,该数据可以称为页面的状态。
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:
-
需要结合 StatefullWidget 使用
-
短时状态经常被用于一个单独 widget 的本地状态,通常使用 State 和 setState() 来实现。
setState 问题:
-
刷新是大范围的整体刷新,而不是精细的刷新
-
只有 StatefullWidget 即有状态的 widget,才支持 setState 方式进行刷新
setState 优点:
-
官方提供,使用极简单
-
调用无需context
provider:
工程结构:
Flutter 官方推荐,provider 非常好理解而且不需要写很多代码。
底层是使用了InheritedWidget, InheritedNotifier, InheritedModel等能力。InheritedWidget是Flutter中特别重要的一个Widget,它提供了一种数据在widget树中从上到下传递、共享的方式。
provider 核心类
-
ChangeNotifier
-
ChangeNotifierProvider
-
Consumer
ChangeNotifier:
它用于向监听器发送通知。换言之,如果被定义为 ChangeNotifier,你可以订阅它的状态变化。
ChangeNotifierProvider:
ChangeNotifierProvider widget 可以向其子孙节点暴露一个 ChangeNotifier 实例。
Consumer:
当继承于ChangeNotifier 子类 通过 ChangeNotifierProvider 在应用中与 widget 相关联,则可以使用 Consumer widget。
必须指定要访问的模型类型。在这个示例中,要访问 CartModel 那么就写上 Consumer<CartModel>。
Consumer widget 唯一必须的参数就是 builder。当 ChangeNotifier 发生变化的时候会调用 builder 这个函数。(换言之,当调用 notifyListeners() 时,所有相关的 Consumer widget 的 builder 方法都会被调用。)
Provider.of
使用 Provider.of,并且将 listen 设置为 false。用于访问数据,但又不希望改变ui的场景。
provider 问题:
-
需要context 强绑定
-
不能刷新屏幕外页面
-
不支持响应式变成
provider 优点:
-
使用简单,大约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提供了三大部分的能力:路由管理,一系列的工具方法,以及状态管理。
getX问题:
-
GetX 涉及 路由管理、一些工具方法,实际上是一个大杂烩的组件,缺失单一性设计原则
-
路由管理使用复杂度很高,项目使用、改造成本很大,收益反而很小。
getX优点:
-
一个简单的响应式状态管理解决方案。
-
使用相对简单
-
调用无需context,如果需要,可以全局调用
getX 看法
-
getX 是一个大杂烩的组件,好在整体代码不多,而且单独个功能之间耦合度低,可以按需分开使用。
-
虽然 GetX 有缺点,但是亮点(响应式状态解决) 也是很有吸引力的,完全可以去其糙泊取其精华,即使用 GetX 状态管理的响应式能力。
-
单纯状态管理场景,代码迁移成本不高。
GetX 响应式使用示例:
- 声明一个响应式变量
//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)
- 绑定widget
Obx(() => Text('${countObs.value}'))
- 更新变量的值,自动更新对应的widget
countObs.value += 1;
字节内状态管理场景实践:
(from Flutter中台,2021年数据)
| 项目 | 项目类型 | flutter规模(按Dart代码行粗略归类) | 选择 | 备注(2021.1.21) | ||
|---|---|---|---|---|---|---|
| 番茄畅听 | 混合 | 小 | setState | 单一页面,业务探索 | ||
| 特效君 | 纯flutter | 中小 | provider | |||
| 火山 | 混合app | 中 | provider + flutter_redux | |||
| 西瓜 | 混合 | 大 | provider +bloc +mobx | |||
| 小荷 | 纯flutter | 大 | bloc | 现在往getX 转 | ||
| 幸福里 | 纯flutter | 大 | getX |
主流状态框架比较:
官方框架比较:setState、Provider、Stream 比较
| setState | Provider | Stream | |
|---|---|---|---|
| 优点 | 1.官方提供 2.需要继承StateFullWidget 3.使用简单 | 1.官方提供 2.依赖InheritedWidget 3.使用简单 | 1.官方提供 2.结合StreamBuilder 使用 |
| 不足 | 1.不能精确刷新 | 1.需要context 2.存在作用域 3.不能响应式编程 | 1.功能单一,并不能适合更多复杂场景 |
其他响应式框架:
| Stream | RxDart | flutter-redux | mobx | bloc | GetX | |
|---|---|---|---|---|---|---|
| 优点 | 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. 路由管理过于复杂、改动过大、项目契合度不符 |
初步结论:
-
简单页面刷新 setState,如splash 页面
-
较页面使用 Provider,管理数据量少
-
如果更复杂页面,如页面多处刷新,页面联动刷新 使用 GetX,而 GetX 建议使用 Rx 能力 响应式刷新。
基于 GetX 响应式刷新能力,页面可以可以采用MVRX 设计。
基于GetX 页面状态管理的 MvRx:
MvRx: ModelView ReactiveX
下面是 MVVM 流程图:
View: 在Flutter 中属于 各种 widget,继承 GetX 的 GetView 。
ViewModel: 在Flutter 中 基于 GetX, 使用 GetxController 子类代替,需要响应ui的数据使用 Rx 对应类型,以支持响应编程,刷新ui。
Model: 在Flutter中承担非页面状态的数据管理,负责逻辑处理。
而 MvRx:ViewModel 将被 GetX 响应式的 Rx 模块代替。
Flutter MvRx 元素:
四个核心概念对应Flutter 的方案:
-
State:
-
ViewModel:
-
View:
-
Async:
优点:
-
响应式更新编程
-
简化页面开发逻辑,页面展示、控制逻辑解耦
-
使用成本低
缺点:
-
GetX 组件功能复杂,需要抽丝剥茧,选择有用的模块
-
需要一定的理解成本
一个简单的代码示例
下面是一个设置页面是否展示实验室入口的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 是用于网络等数据获取。