博客地址:https://www.jianshu.com/p/10a1a16dfff7
背景
原生提供了StatefulWidget这个有状态组件来管理状态,对于多组件的状态交互可以选择由父组件进行统一管理分发,但是当业务一旦复杂,组件树的分支足够多,会出现状态下沉过深入,状态传递复杂的问题。
简单情况是这样的:
随着功能的增加,你的应用程序将会有几十个甚至上百个状态。这个时候你的应用应该会是这样:
上述实际就是多个页面需要共享状态和传递信息场景下出现的,直接的做法是:
通过父widget来分发通知,有嵌套层级深的问题,父层级的setState导致不必要build问题
通过回调传递,同样存在传递深的问题 ,回调也会出现漏调用的问题
面临的问题
如何获取数据源
如何更新数据源
如何通知组件数据源更新
跨组件数据源如何共享
数据流概念
状态管理里会出现基于单向数据流的情况,这里先介绍下数据流的概念
单向数据流
state:驱动应用的数据源。
view:以声明方式将 state 映射到视图 。
actions:响应在 view 上的用户输入导致的状态变化
单向数据流的状态管理:通过定义和隔离状态管理中的各种概念并强制遵守一定的规则,我们的代码将会变得更结构化且易维护
特点:
(1) 所有状态的改变可记录、可跟踪,源头易追溯;
(2) 所有数据只有一份,组件数据只有唯一的入口和出口,使得程序更直观更容易理解,有利于应用的可维护性;
(3) 一旦数据变化,就去更新页面(data->页面),但是没有(页面->data);
(4) 如果用户在页面上做了变动,那么就手动收集起来(双向是自动),合并到原有的数据中
双向数据流
双向数据绑定,带来双向数据流,数据(state)和视图(View)之间的双向绑定。
ng 里的 ng-model 和 vue 里的 v-model,以及Android的DataBinding
说到底就是 (value 的单向绑定 + onChange 事件侦听)的一个语法糖
特点:
(1)无论数据改变,或是用户操作,都能带来互相的变动,自动更新。适用于项目细节,如:UI控件中(通常是类表单操作)。
(2)状态的改变不可控
解决方案
StatefulWidget
官方自带有状态组件,其组件里的各种状态可以由自身管理,也可由父组件管理,哪个管理合适,一般遵循以下原则:
- 如果状态是用户数据,如复选框的选中状态、滑块的位置,则该状态最好由父Widget管理。
- 如果状态是有关界面外观效果的,例如颜色、动画,那么状态最好由Widget本身来管理。
- 如果某一个状态是不同Widget共享的则最好由它们共同的父Widget管理。
一般来说,在Widget内部管理状态封装性会好一些,而在父Widget中管理会比较灵活,如果不知道是不是该由widget自身管理,则优先设计为由父widget管理并将其设计为StatelessWidget
问题:
功能单一,复杂多页面情况下,状态下沉过于深入,状态传递复杂,rebuild的范围过大等
InherityWidget
功能型组件,提供了一种数据在widget树中从上到下传递、共享的机制
比如在根Widget通过一个InherityWidget共享了一个状态,那么便可以在任意子widget中获取它,Theme,Navigator等都是通过这种机制来共享给整个应用的,它省去了逐级传递的麻烦
使用
用于存储共享数据的父Widget,该widget继承InheritedWidget
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16class FatherWidget extends InheritedWidget {
final int data;
FatherWidget({@required this.data, Widget child}) : super(child: child);
//子树通过该方法获取共享数据
static FatherWidget of(BuildContext context) {
return context.inheritFromWidgetOfExactType(FatherWidget);
}
//该回调决定当data发生变化时,是否通知子树中依赖data的widget
@override
bool updateShouldNotify(FatherWidget oldWidget) {
return oldWidget.data != data;
}
}子widget,获取状态和处理依赖发生变化时的响应
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20class ChildWidget extends StatefulWidget {
@override
_ChildWidgetState createState() => _ChildWidgetState();
}
class _ChildWidgetState extends State<ChildWidget> {
@override
Widget build(BuildContext context) {
return new Text(FatherWidget.of(context).data.toString());
}
@override
void didChangeDependencies() {
super.didChangeDependencies();
//父或祖先widget中的InheritedWidget改变(updateShouldNotify返回true)时会被调用
//如果build中没有依赖InheritedWidget,则此回调不会被调用
print("didChangeDependencies = " +
FatherWidget.of(context).data.toString());
}
}整合
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23class ContainerWidget extends StatefulWidget {
@override
_ContainerWidgetState createState() => _ContainerWidgetState();
}
class _ContainerWidgetState extends State<ContainerWidget> {
int _data = 0;
void _incrementCounter() {
setState(() {
_data++; /// 改变状态
});
}
@override
Widget build(BuildContext context) {
return FatherWidget(
data: widget.data,
child: ChildWidget(),
);
}
}
图示
- 如何实现子树获取InheritedWidget
- InheritedWidget和用of获取过它的子Widget如何建立联系的
关键点
“didChangeDependencies”:
State里的生命周期函数之一,表示依赖发生变化时由Framework调用通知,这里的依赖指的是子widget是否使用了InherityWidget的数据
另外,此回调紧跟initState执行,这里可以直接.context获取来使用
源码
如何实现子树直接获取InheritedWidget
新建build InheritedWidget时,对应的Element会调用如下_updateInheritance方法,从父Element复制 _inheritedWidgets并将此InheritedElement注册进 _inheritedWidgets里
1 | class InheritedElement extends ProxyElement { |
获取过程,如下
1 | /// 调用of方法 |
注意:还有个方法是只取InheritedElement而不注册依赖关系的
1 | /// Element class |
总结
优点:
自动订阅
可跨组件获取状态
缺点:
每次促使inheritedWidget build重建 事实上都会触发所有子树的build,所以需要封装一个StatefulWidget来配合实现缓存加载
没有有效分离视图逻辑和业务逻辑。
无法定向通知/指向性通知。 事实上依赖InheriteWidget的子Widget,在调用State的didChangeDependencies前,在Element这一级会调用markNeedsBuild,所以都会rebuild一下