Flutter是声明式UI框架,不像传统的安卓那样的命令式,我们无法拿到对应的组件然后将其修改。每当界面发生变化时,实际上就是重新创建了一组Widget,我们所说的状态管理就是对这样一组控制界面显示的变量做控制。

StatelessWidget

StatelessWidget无状态组件,它不包含任何状态属性,也无法响应状态的变化,仅仅就是一个普通组件,声明成什么样它就是什么样。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
class MyCircle extends StatelessWidget {
const MyCircle({super.key});

@override
Widget build(BuildContext context) {
return Container(
width: 100,
height: 100,
decoration: ShapeDecoration(color: Colors.red, shape: CircleBorder()),
child: Center(
child: TextButton(onPressed: () {
// 点击事件
}, child: Text('Click')),
),
);
}
}

例如上述例子,我们声明了一个100*100的红色的圆形,并且在圆形中间有一个文本按钮。它就是一个典型的无状态组件,当被声明在界面中后,他就不会变化,一直都是红色的圆形。当然,它本身也是有状态的,例如宽高和颜色都可以说是它的状态,因为它的显示需要依赖这些参数。那么我们将其提取出来,作为状态使用:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
class MyCircle extends StatelessWidget {

// 将尺寸和颜色提取出来,作为状态使用
double _size = 100;
Color _color = Colors.red;

MyCircle({super.key});

@override
Widget build(BuildContext context) {
return Container(
width: _size,// 使用提取出的状态
height: _size,
// 使用提取出的状态
decoration: ShapeDecoration(color: _color, shape: CircleBorder()),
child: Center(
child: TextButton(onPressed: () {
// 点击按钮时,修改状态,尺寸+20,颜色改成蓝色
_size += 20;
_color = Colors.blue;
}, child: Text('Click')),
),
);
}
}

我们将尺寸状态和颜色状态提取出去,然后在使用的地方直接使用这两个状态值,并且在点击按钮时修改这两个状态值。但是我们会发现,点击按钮时它并没有变化,还是一个红色的尺寸为100的圆形。

这是因为Flutter如果想要刷新界面,必须要重新调用它的build方法来创建新的组件。而我们点击按钮时,只是修改了状态值,并不能触发build,因此是无法响应变化的。而且作为无状态组件StatelessWidget,它也是不能被触发build的,只能由它的父组件刷新时,重新构建MyCicle,而重新构建又意味着重新创建了一个MyCicle,因此它还是一个红色的尺寸100的圆形。

因此,如果想要响应状态的变化,就不能使用StatelessWidget,而是要用StatefulWidget

StatefulWidget

StatefulWidget就是Flutter中的有状态组件,它会在声明Widget时创建一个管理状态的类,当状态发生变化时,通过setState触发组件的刷新,实际上就是触发它本身的build方法来重新创建子组件。将前面的例子进行修改:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
// 组件比较模板化,暂不需要关注
class MyCircle extends StatefulWidget {
const MyCircle({super.key});

@override
State<MyCircle> createState() => _MyCircleState();
}

// 状态管理类
class _MyCircleState extends State<MyCircle> {
// 提取出来的状态
double _size = 100;
Color _color = Colors.red;

@override
Widget build(BuildContext context) {
return Container(
width: _size,// 使用状态
height: _size,
decoration: ShapeDecoration(color: _color, shape: CircleBorder()),
child: Center(
child: TextButton(
onPressed: () {
// 点击时修改状态,必须使用setState触发更新
setState(() {
_size += 20;
_color = Colors.blue;
});
},
child: Text('Circle'),
),
),
);
}
}

将前面的MyCircle组件使用StatefulWidget修改如上,它会创建一个状态管理类,然后状态和UI都是在状态管理类中声明。其他都没有修改,唯一的改动就是在点击事件中,修改状态值时使用了setState方法来修改状态值的。这也是有状态组件的最重要的一个方法,因为这个方法会触发界面的刷新。

实际上,setState也并不是触发界面的刷新,而是触发了build方法来重新创建组件,注意这里只是重新调用了build方法,而不是重新创建了一个MyCircle,因此状态值的修改仍是有效的,此时_size是130,_color被改成了蓝色,因此新创建的组件就是一个尺寸为120的蓝色圆形,表现形式就是点击后尺寸变大20颜色修改为蓝色。

有状态组件会声明一个依赖的状态管理类,所依赖的状态都声明在这里面,当修改时通过setState触发重建从而刷新界面,这些状态都可以说是这个组件的内部状态。那么,此时我有两个MyCircle,并且我想在点击任意一个圆形时,两个圆形都同时变化,也就是说两个组件共用同一组状态,现有的管理方式就无法实现了。

因此,就引出了状态提升的概念,即将状态向上提升,提到它们共有的父组件中,这样它们就都能使用父组件中的状态值来实现同步变化了。此时,MyCircle中就不需要管理状态了,我们也可以将其简写成StatelessWidget了,需要的参数通过构造方法传入即可。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
class MyCircle extends StatelessWidget {
// 状态值通过构造函数传入
final double size;
final Color color;
// 点击事件通过回调传出
final VoidCallback callback;

const MyCircle({
super.key,
required this.size,
required this.color,
required this.callback,
});

@override
Widget build(BuildContext context) {
return Container(
// 使用构造方法传入的状态
width: size,
height: size,
decoration: ShapeDecoration(color: color, shape: CircleBorder()),
child: Center(
child: TextButton(onPressed: callback, child: Text('Circle')),
),
);
}
}

我们又将MyCircle修改为了无状态组件,并且状态值不是内部存储和修改的,而是通过构造方法从外部传入进来的,这样当外部的状态发生变化时,会调用外部的build来刷新界面,从而创建新的MyCircle,然后完成刷新界面的目的。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
// 状态提升到父组件中
class MyParent extends StatefulWidget {
const MyParent({super.key});

@override
State<MyParent> createState() => _MyParentState();
}

class _MyParentState extends State<MyParent> {
// 父组件持有状态
double _size = 100;
Color _color = Colors.red;

// 修改状态时仍通过setState修改
void _callback() {
setState(() {
_size += 20;
_color = Colors.blue;
});
}

@override
Widget build(BuildContext context) {
// 父组件的布局内容是一个Column,里面两个MyCircle
return Column(
children: [
// 通过构造方法传入状态
MyCircle(size: _size, color: _color, callback: _callback),
MyCircle(size: _size, color: _color, callback: _callback)
],
);
}
}

我们将MyCircle的两个状态提升到了共有父组件MyParent中,MyCircle通过构造方法接收状态值。当状态发生变化时,仍是通过setState触发重建,此时会调用MyParentbuild方法重新创建组件,然后就创建了新的两个MyCircle,并且构造方法传入的参数还是修改后的状态值,因此实现了同步变化的目的。

以上就是状态提升,也就是状态管理的手段,即将状态提升到足够高的位置,从而使得多个子组件之间可以共享状态。一般来说,只需要提到需要共享状态的子组件的最近共有父组件中就行了,就能实现它们的共享了。但在实际项目中,界面往往是非常复杂的,因此可能需要在多个组件中分别存放不同的状态,这对于管理来说是非常麻烦的 ,非常难进行维护,因此我们会将状态统一提到最顶层的父组件中,这样状态全部统一在一块了,就比较好管理维护了。

但这样带来一个问题,状态在最顶层的父组件中,使用的地方在多个层级以下的子组件中,因此需要通过构造方法一步步得将状态传递到目标子组件中,而中间的组件却根本不需要这些状态,这带来非常严重的耦合问题。当然这还不是最关键的,最关键的是会带来性能问题。

状态修改后是通过setState来触发重建的,当我们把状态提到最顶层后,任意一个状态发生变化,都会导致最顶层的父组件的build方法被调用,从而重建所有的组件,这是非常损耗性能的。

ChangeNotifier

最理想的情况是当状态发生变化时,只会重建使用到该状态的组件,而不会影响别的组件,因此就不能直接在父组件中setState。我们既想要将状态提升到最顶层父组件中,又想要状态变化时只影响到使用该状态的组件,这就需要用到观察者模式了。子组件观察最顶层父组件中的状态,当状态变化时就能通知到子组件中来,就不需要在父组件中setState来全部刷新了。

Listenable

Listenable就是Flutter中用于实现观察者模式的接口,它主要定义了两个方法,分别是添加监听和移除监听。主要原理就是其他组件通过addListener添加监听,然后当状态值发生变化时,就能通知到监听者了。

1
2
3
4
5
6
abstract class Listenable {
const Listenable();
factory Listenable.merge(Iterable<Listenable?> listenables) = _MergingListenable;
void addListener(VoidCallback listener);
void removeListener(VoidCallback listener);
}

ChangeNotifier

它的实现类就是ChangeNotifier,名字就能表现出它的功能,即发生变化时进行通知。逻辑基本上没什么可说的,就是维护一个集合,当addListener时,将监听方法添加到集合中进行存储;当不需要使用时可以通过removeListener移除监听;另外额外提供了一个notifyListeners方法,会触发所有的监听者的回调。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
mixin class ChangeNotifier implements Listenable {
// 监听者个数
int _count = 0;
// 存放监听者
List<VoidCallback?> _listeners = _emptyListeners;
// 添加监听者
@override
void addListener(VoidCallback listener) { ... }
// 移除监听者
@override
void removeListener(VoidCallback listener) { ... }
// 通知所有的监听者
void notifyListeners() { ... }
}

ChangeNotifier本身是一个混入类,不仅可以正常继承,也可以进行混入(已经有了继承关系的类使用混入的方式),提高了灵活性。对于前面的例子,我们就可以将MyCircle所涉及的状态通过ChangeNotifier来进行管理:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
class CircleController extends ChangeNotifier {
double _size = 100;
get size => _size;
// 修改size时,触发notifyListeners
set size(value) {
_size = value;
notifyListeners();
}

// 修改color时,触发notifyListeners
Color _color = Colors.red;
get color => _color;
set color(value) {
_color = value;
notifyListeners();
}
}

当我们的父组件只需要持有状态,而不涉及到状态的修改,即不需要在父组件中setState

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
class MyParent extends StatefulWidget {
const MyParent({super.key});

@override
State<MyParent> createState() => _MyParentState();
}

class _MyParentState extends State<MyParent> {

// 仍是将状态提升到最顶层父组件中
final controller = CircleController();

@override
Widget build(BuildContext context) {
return Column(
children: [
// 子组件仍通过构造方法传入
MyCircle(controller: controller),
// 不涉及到状态的组件
Text('Other Widget'),
// 需要共用状态的组件
MyCircle(controller: controller)
],
);
}
}

状态仍然是提升到最顶层,方便各个子组件共用这些状态,当然目前这些状态被封装到了CircleController中了,其他没什么变化,对于状态仍是通过构造方法逐层向下传递到目标子组件中。

然后就是子组件了,子组件需要向CircleController中添加监听,并且响应变化。而Flutter中,想要响应变化,就必须通过build构建新的组件,而触发build方法又必须用setState,因此MyCircle又得变回成有状态组件:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
class MyCircle extends StatefulWidget {

// 状态通过构造方法传入
final CircleController controller;
const MyCircle({super.key, required this.controller});

@override
State<MyCircle> createState() => _MyCircleState();
}

// 对应的状态管理类
class _MyCircleState extends State<MyCircle> {
@override
void initState() {
super.initState();
// 初始化时添加监听
widget.controller.addListener(onStateChanged);
}

@override
void dispose() {
// 结束时移除监听
widget.controller.removeListener(onStateChanged);\
super.dispose();
}

// 状态变化时触发setState
void onStateChanged() {
setState(() {});
}


@override
Widget build(BuildContext context) {
return Container(
// 使用controller中的参数
width: widget.controller.size,
height: widget.controller.size,
decoration: ShapeDecoration(color: widget.controller.color, shape: CircleBorder()),
child: Center(
child: TextButton(onPressed: () {
// 点击圆圈时,直接修改controller中的内容即可
widget.controller.size += 20;
widget.controller.color = Colors.blue;
}, child: Text('Circle')),
),
);
}
}

这样,当我们点击圆圈时,修改了controller中的状态值,此时会将通知发送给所有的监听者,也就是我们多个MyCircle组件,而在MyCircle组件中,接收到状态变化时直接调用了setState触发了重建,因此能够实现状态响应。

ValueNotifier

上述我们的CircleController中,涉及到了两个状态,一个是_size,一个是_color。如果只涉及到一个状态的变化,则可以使用ValueNotifier,它是ChangeNotifier的子类,内部存储了一个泛型状态值。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
class ValueNotifier<T> extends ChangeNotifier implements ValueListenable<T> {
// 构造方法中传入默认值
ValueNotifier(this._value) {...}

// 通过value获取到对应的值
@override
T get value => _value;
T _value;
set value(T newValue) {
if (_value == newValue) {
return;
}
_value = newValue;
notifyListeners();
}

@override
String toString() => '${describeIdentity(this)}($value)';
}

可以看到它本身是比较简单的,只在内部包装了一个value值并提供对应的get/set方法,并且在set时触发通知监听。跟我们自己写的CircleController逻辑是一样的,只是它用泛型代替了状态,因此用起来更方便一些。

另外就是,ChangeNotifier无法针对性的回调相关属性的监听,例如前面我们写的CircleController中,不论是_size变化还是_color变化,都会触发它所有的监听者。那么问题来了,当我们界面中状态很多的时候,组件A使用状态a,组件B使用状态b,组件C使用状态c,然后将他们全部进行状态提升,放在了最顶层父组件中。然后通过ChangeNotifier将他们放在同一个类中,此时不论任何一个状态发生变化,都会导致组件A、B、C同时刷新,这显然也是与我们的目标是不一致的。

此时就需要用到了ValueNotifier,我们将相关联的状态仍放在同一个ChangeNotifier中,如上面的例子中的CircleController,不关联的状态封装成单独的ValueNotifier,这样就能够实现它们之间互不关联了。这里,我们可以将CircleController修改一下:

1
2
3
4
5
6
// 普通类,没有继承ChangeNotifier
class CircleController {
// 将两个状态都封装成ValueNotifier
final size = ValueNotifier<double>(100);
final color = ValueNotifier<Color>(Colors.red);
}

实际上由于sizecolor它们是一组的,应该包装成一个整体ChangeNotifier,而不是两个ValueNotifier,这里只是为了示例,才将他们拆开的。

然后MyParent不需要修改,还是持有着CircleController,然后通过构造方法传递状态,主要就是MyCircle中需要修改下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
class MyCircle extends StatefulWidget {

final CircleController controller;

const MyCircle({super.key, required this.controller});

@override
State<MyCircle> createState() => _MyCircleState();
}

class _MyCircleState extends State<MyCircle> {
@override
void initState() {
super.initState();
// 初始化时添加监听,注意需要将多个属性都添加一次
widget.controller.size.addListener(onStateChanged);
widget.controller.color.addListener(onStateChanged);
}

@override
void dispose() {
// 结束时移除监听,注意需要将多个属性都移除一次
widget.controller.size.removeListener(onStateChanged);
widget.controller.color.removeListener(onStateChanged);
super.dispose();
}

// 状态变化时触发setState
void onStateChanged() {
setState(() {});
}


@override
Widget build(BuildContext context) {
return Container(
// 后缀需要通过value获取到实际的值
width: widget.controller.size.value,
height: widget.controller.size.value,
decoration: ShapeDecoration(color: widget.controller.color.value, shape: CircleBorder()),
child: Center(
child: TextButton(onPressed: () {
print('onPresssed');
widget.controller.size.value += 20;
widget.controller.color.value = Colors.blue;
}, child: Text('Circle')),
),
);
}
}

和原先的逻辑基本上没啥区别,就是在添加监听的时候将每个关联的属性都添加了一次,移除的时候也是一样,然后就是使用状态值的时候,通过value属性获取实际的值。另外如果涉及的状态值很多的话,需要添加和移除好几次listener,是否有办法简化呢?

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
abstract class Listenable {
// 工厂构造方法
factory Listenable.merge(Iterable<Listenable?> listenables) = _MergingListenable;
}

// 合并的Listenable
class _MergingListenable extends Listenable {
_MergingListenable(this._children);

// 记录需要合并的Listenable
final Iterable<Listenable?> _children;

// 添加时,给每个child添加
@override
void addListener(VoidCallback listener) {
for (final Listenable? child in _children) {
child?.addListener(listener);
}
}

// 移除时,给每个child移除
@override
void removeListener(VoidCallback listener) {
for (final Listenable? child in _children) {
child?.removeListener(listener);
}
}
}

我们可以通过Listenable的工厂构造方法merge来合并多个Listenable,其内部逻辑就是一个包装器,将添加进来的监听给同时添加到多个child上就行了。下面我们使用这个来简化下我们的MyCircle

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
class MyCircle extends StatefulWidget {

final CircleController controller;
late Listenable listenable;

MyCircle({super.key, required this.controller}) {
// 通过merge构建一个新的Listenable
listenable = Listenable.merge([
controller.size,
controller.color
]);
}

@override
State<MyCircle> createState() => _MyCircleState();
}

class _MyCircleState extends State<MyCircle> {

@override
void initState() {
super.initState();
// 初始化时添加监听,直接使用listenable
widget.listenable.addListener(onStateChanged);
}

@override
void dispose() {
// 结束时移除监听,直接使用listenable
widget.listenable.removeListener(onStateChanged);
super.dispose();
}

...
// 其他部分不需要改变
}

到这里,基本上就已经能实现我们的目标了:状态变化只影响到使用该状态的组件。我们的方案就是使用观察者模式,将状态提升到最顶层父组件后,并不直接触发setState,而是在需要使用该状态的地方,包装出一个StatefulWidget,然后注册监听,当监听到状态变化时,开始触发重建,从而刷新UI。每次都新建一个StatefulWidiget未免太过于麻烦,于是我们为了省事,将这部分封装一下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
class MyListenableBuilder extends StatefulWidget {
final Widget Function(BuildContext context) builder;
final Listenable listenable;

const MyListenableBuilder({
super.key,
// 需要监听的状态
required this.listenable,
// 构建界面的方法
required this.builder,
});

@override
State<MyListenableBuilder> createState() => _MyListenableBuilderState();
}

class _MyListenableBuilderState extends State<MyListenableBuilder> {
@override
void initState() {
super.initState();
// 添加监听
widget.listenable.addListener(_onStateChanged);
}

@override
void dispose() {
// 移除监听
widget.listenable.removeListener(_onStateChanged);
super.dispose();
}

// 触发监听回调时,刷新UI
void _onStateChanged() {
setState(() {});
}

@override
Widget build(BuildContext context) {
// 调用builder方法构建新的组件
return widget.builder(context);
}
}

此时,我们只需要在需要使用状态的地方直接使用MyListenableBuilder包裹即可,然后我们修改下MyCircle,因为我们已经使用MyListenableBuilder了,所以MyCircle又可以回到最初的无状态组件了:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
class MyCircle extends StatelessWidget {
// 通过构造方法传入状态
final CircleController controller;

const MyCircle({super.key, required this.controller});

@override
Widget build(BuildContext context) {
// 使用我们定义的MyListenableBuilder包裹
return MyListenableBuilder(
// 因为controller中我们将状态包装成了多个ValueNotifier
// 这里需要通过merge进行合并,方便我们添加监听
listenable: Listenable.merge([controller.color, controller.size]),
builder: (_) => Container(
// 直接使用状态值即可
width: controller.size.value,
height: controller.size.value,
decoration: ShapeDecoration(
color: controller.color.value,
shape: CircleBorder(),
),
child: Center(
child: TextButton(
onPressed: () {
controller.size.value += 20;
controller.color.value = Colors.blue;
},
child: Text('Circle'),
),
),
),
);
}
}

当我们点击按钮时,会修改controller中的尺寸和颜色,此时MyListenableBuilder就会触发重建,而它的build方法就是简单的调用参数builder,因此会通过builder重新构建组件。

1
MyCircle--MyListenableBuilder--Container--Center--TextButton--Text

现在,我们的MyCircle组件的层级是如上所示的,当状态变化时,会触发重建,重建的部分是Container,以及它的子组件CenterTextButton以及Text。但我们发现只有Container用到了状态,而另外三个组件并没有用到状态,也就是说重建实际上只需要Container重建就行了。于是我们修改下MyListenableBuilder,引入一个child属性来记录不可变的部分:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
class MyListenableBuilder extends StatefulWidget {
// builder中加入一个参数child
final Widget Function(BuildContext context, Widget? child) builder;
final Listenable listenable;
// 增加一个属性child,通过构造方法传入
final Widget? child;

const MyListenableBuilder({
super.key,
required this.listenable,
required this.builder,
this.child // 构造方法传入
});

@override
State<MyListenableBuilder> createState() => _MyListenableBuilderState();
}

class _MyListenableBuilderState extends State<MyListenableBuilder> {
@override
void initState() {
super.initState();
widget.listenable.addListener(_onStateChanged);
}

@override
void dispose() {
widget.listenable.removeListener(_onStateChanged);
super.dispose();
}

void _onStateChanged() {
setState(() {});
}

@override
Widget build(BuildContext context) {
// 将child参数传入
return widget.builder(context, widget.child);
}
}

这样我们加入了一个可空的child,这是因为可能有的组件不包含不可变部分,因此不需要通过该参数进行记录。然后修改我们的MyCircle:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
class MyCircle extends StatelessWidget {
final CircleController controller;

const MyCircle({super.key, required this.controller});

@override
Widget build(BuildContext context) {
return MyListenableBuilder(
listenable: Listenable.merge([controller.color, controller.size]),
builder: (_, child) => Container(
width: controller.size.value,
height: controller.size.value,
decoration: ShapeDecoration(
color: controller.color.value,
shape: CircleBorder(),
),
// 原来的位置直接引用参数列表中的child即可
child: child,
),
// 将不可变部分提取到child参数中
child: Center(
child: TextButton(
onPressed: () {
controller.size.value += 20;
controller.color.value = Colors.blue;
},
child: Text('Circle'),
),
),
);
}
}

经过上面的一番改造,我们既保持了MyCircle的无状态属性,又能使其可以跟随状态发生变化,同时还控制了刷新的范围,只有用到状态的部分才重建,其他部分保持不变,提升了性能,简化了逻辑。

ListenableBuilder

简化整个代码的关键部分就在于我们自定义的MyListenableBuilder,它实际上是一个StatefulWidget,内部帮我们主动注册和移除监听,以及状态变化时主动帮我们setState。使用它,我们甚至可以保持整个编码过程中只使用StatelessWidget

当然,这么有用的组件Flutter怎么会想不到呢,所以它其实是被内置到Flutter中的一个组件,名字叫做ListenableBuilder

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
class ListenableBuilder extends AnimatedWidget {
const ListenableBuilder({
super.key,
required super.listenable,// 监听状态
required this.builder,// 构建组件
this.child,// 不变的组件
});

@override
Listenable get listenable => super.listenable;
final TransitionBuilder builder;
final Widget? child;

@override
Widget build(BuildContext context) => builder(context, child);
}

整个逻辑非常简单,主要就是继承自AnimatedWidget,而AnimatedWidget就是我们实现的第一版的不带childMyListenableBuilder

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
abstract class AnimatedWidget extends StatefulWidget {

const AnimatedWidget({super.key, required this.listenable});

final Listenable listenable;
@protected
Widget build(BuildContext context);

@override
State<AnimatedWidget> createState() => _AnimatedState();

}

class _AnimatedState extends State<AnimatedWidget> {
// 注册监听
@override
void initState() {
super.initState();
widget.listenable.addListener(_handleChange);
}

// 更新时重新注册
@override
void didUpdateWidget(AnimatedWidget oldWidget) {
super.didUpdateWidget(oldWidget);
if (widget.listenable != oldWidget.listenable) {
oldWidget.listenable.removeListener(_handleChange);
widget.listenable.addListener(_handleChange);
}
}

// 移除监听
@override
void dispose() {
widget.listenable.removeListener(_handleChange);
super.dispose();
}

// 状态变化时setState
void _handleChange() {
if (!mounted) {
return;
}
setState(() {
});
}

@override
Widget build(BuildContext context) => widget.build(context);
}

整体下来没什么特殊的,和我们自定义的MyListenableBuidler是一样的逻辑。使用起来也是一样的,我们直接修改MyCircle,将MyListenableBuilder替换成内置的ListenableBuilder就行,其他一模一样,完全不需要改动。

基于Listenable的观察者,实际上还有好几个对应的组件可以使用:

  • ListenableBuilder
1
2
3
4
5
6
7
8
9
const ListenableBuilder({
super.key,
// 观测的状态,
required super.listenable,
// 构建组件的builder
required this.builder,
// 不变的部分
this.child,
});
  • AnimatedBuilder(和ListenableBuilder一模一样,就是名字不一样)
1
2
3
4
5
6
7
8
9
const AnimatedBuilder({
super.key,
// 观测的状态
required Listenable animation,
// 构建组件的builder
required super.builder,
// 不变的部分
super.child,
})
  • ValueListenableBuilder
1
2
3
4
5
6
7
8
9
const ValueListenableBuilder({
super.key,
// 注意类型是ValueListenable
required this.valueListenable,
// 构建组件的builder,多了一个参数是value
required this.builder,
// 不变的部分
this.child,
});

其中ValueListenableBuilder在用法上和ListenableBuilder稍微有些不同,它接收的状态类型不是普通的Listenable,而是ValueListenable,也就是对应的类型为ValueNotifier。它的局限性就在于只能观测到一个状态的变化,使用方式大概如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
// 状态是一个ValueNotifier
final _size = ValueNotifier<double>(100);

ValueListenableBuilder(
// 观察的状态
alueListenable: _size,
// 构建组件,可以直接拿到value
builder: (context, value, child) {
return Container(
// 直接使用value,不需要通过_size.value获取
// 当然通过_size.value获取也是可以的
width: value,
height: value,
decoration: ShapeDecoration(
color: Colors.red,
shape: CircleBorder(),
),
child: child,
);
},
// 不可变部分
child: TextButton(
onPressed: () {
_size.value += 20;
},
child: Text('Circle'),
),
)

实际上我们用这个比较少,毕竟局限性太大,只能观测到一个状态的变化,而方便之处也仅仅是在builder中给我们提供了value值,使得我们不需要通过_size.value获取值了。因此,这个组件只能说是比较鸡肋吧,我直接ListenableBuilder一把梭就行了。

InheritedWidget

为了管理和复用状态,我们使用状态提升的方式,将状态提取到最顶层父组件中,由父组件进行持有,从而可以共享到各个子组件中。但是状态提升带来了两个痛点:一是状态刷新会导致所有界面全部重建,一是状态需要通过构造方法一层一层传递到子组件中。第一个痛点我们使用ChangeNotifierListenableBuilder解决了,接下来就是第二个痛点了,如何将状态传递到子组件中。

我们之所以使用构造方法传递状态,是因为Flutter是声明式UI,我们不能获取到对应的组件,从而无法从组件中获取到状态。但有一个组件比较例外,就是InheritedWidget,它允许我们在子组件中通过context获取到它本身,从而获取到它所持有的状态。InheritedWidget是一个抽象类,必须要继承它实现相关的方法才可以。

1
2
3
4
5
6
7
8
9
class CircleControllerProvider extends InheritedWidget {

final controller = CircleController();

CircleControllerProvider({super.key, required super.child});

@override
bool updateShouldNotify(covariant CircleControllerProvider oldWidget) => false;
}

我们定义了一个CircleControllerProvider,用于存储我们的状态,它内部也基本上没干啥,就是定义了一个controller,然后重写了updateShouldNotify方法来判断是否需要通知依赖的子组件。

当它被声明在组件树中的时候,我们就可以通过context来获取到它:

1
2
3
4
// 使用依赖方式获取InheritedWidget
context.dependOnInheritedWidgetOfExactType<T>();
// 直接获取
context.getInheritedWidgetOfExactType<T>();

当状态由CircleControllerProvider提供时,我们的MyParent也不需要持有状态了,也就是可以改成无状态组件了:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
class MyParent extends StatelessWidget {
const MyParent({super.key});

@override
Widget build(BuildContext context) {
// 获取到对应的InheritedWidget组件
final provider = context.dependOnInheritedWidgetOfExactType<CircleControllerProvider>();
return Column(
children: [
// 拿到组件后可以获取到状态值
MyCircle(controller: provider!.controller),
Text('Other Widget'),
MyCircle(controller: provider!.controller),
],
);
}
}

我们是在MyParent中获取到的CircleControllerProvider,因此我们在声明组件树的时候,必须要将它包在MyParent外面。

1
2
3
4
5
6
Scaffold(
body: SafeArea(
// MyParent外面包一层Provider
child: CircleControllerProvider(child: MyParent())
),
);

我们在编写代码时,常用的颜色属性会通过Theme.of(context)来获取到ThemeData,这里其实用的也是InheritedWidget实现的,实际上使用of的方式获取实例也算是Flutter中一个约定俗成的方式。因此我们将其也改一下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
class CircleControllerProvider extends InheritedWidget {
final controller = CircleController();

CircleControllerProvider({super.key, required super.child});

@override
bool updateShouldNotify(covariant CircleControllerProvider oldWidget) => false;

// 静态方法,返回的强制转成非空类型
static CircleController of(BuildContext context) {
final provider = context.dependOnInheritedWidgetOfExactType<CircleControllerProvider>()!;
return provider.controller;
}

// 静态方法,返回的是可空类型
static CircleController? maybeOf(BuildContext context) {
final provider = context.dependOnInheritedWidgetOfExactType<CircleControllerProvider>();
return provider?.controller;
}

}

注意我们通过context获取到的是可空类型,这是因为当你没有在界面中使用当前InheritedWidget时,肯定是无法获取到的,所以返回值是可空类型。

当使用这种方式时,MyParent也不需要通过构造方法来传递controller了,让使用状态的组件自己去获取就行了:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
class MyParent extends StatelessWidget {
const MyParent({super.key});

@override
Widget build(BuildContext context) {
return Column(
children: [
// 不需要构造方法传入,直接就一个空参数就行
MyCircle(),
Text('Other Widget'),
MyCircle(),
],
);
}
}

然后修改下MyCircle,让他自己去获取状态:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
class MyCircle extends StatelessWidget {

// 不需要构造方法传入
const MyCircle({super.key});

@override
Widget build(BuildContext context) {
// 直接通过context获取
final controller = CircleControllerProvider.of(context);
// 下面的部分不需要改动
return ListenableBuilder(
listenable: Listenable.merge([controller.color, controller.size]),
builder: (_, child) => Container(
width: controller.size.value,
height: controller.size.value,
decoration: ShapeDecoration(
color: controller.color.value,
shape: CircleBorder(),
),
child: child,
),
child: Center(
child: TextButton(
onPressed: () {
controller.size.value += 20;
controller.color.value = Colors.blue;
},
child: Text('Circle'),
),
),
);
}
}

到这里基本上就实现了状态管理,我们通过InheritedWidget来保存状态,在子组件中通过context来获取到状态,然后状态使用ChangeNotifier+ListenableBuilder实现局部刷新。

接下来在继续看下InheritedWidgetupdateShouldNotify方法,当然前面我们重写时是直接返回了false,而它实际的作用是用来通知依赖的组件进行重建的。我们在获取InheritedWidget时,是通过context获取的,有两种方式,一个是getInheritedWidgetOfExactType,一个是dependOnInheritedWidgetOfExactType,其中使用dependOn开头的方法获取时,会将自身注册到InheritedWidget中,而get开头的仅仅是获取而不会注册。

InheritedWidget实际上本来应该是要配合有状态组件来完成数据传递的,当状态发生变化时,会通知到InheritedWidget,然后它再去通知依赖的子组件进行重建,举个例子:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
// 持有状态的InheritedWidget
class StateInheritedWidget extends InheritedWidget {
final double size;
final Color color;

const StateInheritedWidget({
super.key,
required super.child,
required this.size,
required this.color,
});

// 不通知更新
@override
bool updateShouldNotify(covariant StateInheritedWidget oldWidget) {
return false;
}
}

// 普通的子组件
class ChildWidget extends StatelessWidget {
const ChildWidget({super.key});

@override
Widget build(BuildContext context) {
// 获取到状态,然后使用
final state = context
.dependOnInheritedWidgetOfExactType<StateInheritedWidget>()!;
return Container(
width: state.size,
height: state.size,
decoration: ShapeDecoration(color: state.color, shape: CircleBorder()),
);
}
}

// 父组件,实际的状态持有者
class TopWidget extends StatefulWidget {
const TopWidget({super.key});

@override
State<TopWidget> createState() => _TopWidgetState();
}

class _TopWidgetState extends State<TopWidget> {
double _size = 100;
Color _color = Colors.red;

@override
Widget build(BuildContext context) {
return Column(
children: [
// 使用InheritedWidget传递状态,实际布局是ChildWidget
StateInheritedWidget(size: _size, color: _color, child: ChildWidget()),
ElevatedButton(onPressed: (){
setState(() {
_size += 40;
_color = Colors.blue;
});
}, child: Text('Button'))
],
);
}
}

上述代码实际是能正常运行的,并且点击按钮也会响应颜色和尺寸的修改。这是因为当点击按钮时通过setState触发了TopWidget#build,导致所有的组件都重建了一次,所以是能够正常显示的。如果我们不想让它重建,那么可以将不重建的部分通过const修饰。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
class _TopWidgetState extends State<TopWidget> {
double _size = 100;
Color _color = Colors.red;

@override
Widget build(BuildContext context) {
return Column(
children: [
// 在ChildWidget前面加了const
StateInheritedWidget(size: _size, color: _color, child: const ChildWidget()),
ElevatedButton(onPressed: (){
setState(() {
_size += 40;
_color = Colors.blue;
});
}, child: Text('Button'))
],
);
}
}

此时点击按钮时,颜色和尺寸虽然改变了,但是由于ChildWidget没有重建,因此会导致点击按钮时,界面不会发生变化。如果想要它变化,就需要在InheritedWidget中重写updateShouldNotify的逻辑。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
// 持有状态的InheritedWidget
class StateInheritedWidget extends InheritedWidget {
final double size;
final Color color;

const StateInheritedWidget({
super.key,
required super.child,
required this.size,
required this.color,
});

@override
bool updateShouldNotify(covariant StateInheritedWidget oldWidget) {
// 当新旧InheritedWidget数据不一致时通知依赖的子组件重新build
return oldWidget.size != size || oldWidget.color != color;
}
}

改成以上的逻辑就可以了,这个方法的作用就是在它发生重建时,决定是否通知依赖的子组件进行重建。而依赖的子组件是在context.dependOnInheritedWidgetOfExactType获取时注册进来的,因此即使子组件被声明为const,当InheritedWidget通知你刷新时,子组件就必须要重新build了。

再回到我们最初的例子中,我们的状态使用的是ChangeNotifier,观察者使用的是ListenableBuilder,也就是我们只需要它传递状态的能力,而不需要它的这一套刷新逻辑,所以我们只需要简单的return false即可。并且我们在获取状态时,也直接使用get开头的方法就行,而不需要使用dependOn开头的方法。

由于InheritedWidget是抽象类,所以我们使用时必须要继承它,就像前面我们声明的CircleControllerProvider一样。当然这样写不是太合适,毕竟不可能每个controller都写一个对应的类吧,因此我们再对他抽象一下,使用泛型声明一个通用的类:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
class MyProvider<T> extends InheritedWidget {
final T controller;

const MyProvider({super.key, required this.controller, required super.child});

static T of<T>(BuildContext context) {
return context
.dependOnInheritedWidgetOfExactType<MyProvider<T>>()!
.controller;
}

static T? maybeOf<T>(BuildContext context) {
return context
.dependOnInheritedWidgetOfExactType<MyProvider<T>>()
?.controller;
}

@override
bool updateShouldNotify(covariant InheritedWidget oldWidget) => false;
}

这样,我们就可以直接使用MyProvider来传递状态了,而不需要再重新写一遍。

1
2
3
4
5
6
7
8
MyProvider(
controller: ValueNotifier<double>(100),
child: MyProvider(
controller: ValueNotifier<double>(200),
// 包了两层MyProvider
child: MyChild()
)
),

如果在界面中,用到了多个InheritedWidget,并且它们的类型是一样的,这样在子组件中通过context获取时,就只会查找到离他最近的那个InheritedWidget。例如上面的例子中,MyChild在获取状态时,拿到的是200,而不是100。

总结

Flutter状态管理的实质就是状态提升,将状态提升到一定的高度后,方便对其进行管理以及方便子组件对状态的共享使用。

后面我们所做的一系列操作,都是为了解决状态提升后引入的问题,一是状态传递问题,一是全局刷新问题。针对这两个问题,我们可以引用InheritedWidget来解决状态的传递问题,然后在引入ChangeNotifier+ListenableBuilder解决全局刷新的问题。

整体逻辑如下图:顶层用InheritedWidget提供状态,需要使用状态的组件用ListenableBuilder包裹,不变的用child引用。然后注册监听,当监听到状态变化时,触发rebuild刷新界面。

状态管理