Flutter是声明式UI框架,不像传统的安卓那样的命令式,我们无法拿到对应的组件然后将其修改。每当界面发生变化时,实际上就是重新创建了一组Widget,我们所说的状态管理就是对这样一组控制界面显示的变量做控制。
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: () { _size += 20 ; _color = Colors.blue; }, child: Text('Click' )), ), ); } }
我们将尺寸状态和颜色状态提取出去,然后在使用的地方直接使用这两个状态值,并且在点击按钮时修改这两个状态值。但是我们会发现,点击按钮时它并没有变化,还是一个红色的尺寸为100的圆形。
这是因为Flutter如果想要刷新界面,必须要重新调用它的build方法来创建新的组件。而我们点击按钮时,只是修改了状态值,并不能触发build,因此是无法响应变化的。而且作为无状态组件StatelessWidget,它也是不能被触发build的,只能由它的父组件刷新时,重新构建MyCicle,而重新构建又意味着重新创建了一个MyCicle,因此它还是一个红色的尺寸100的圆形。
因此,如果想要响应状态的变化,就不能使用StatelessWidget,而是要用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(() { _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; void _callback() { setState(() { _size += 20 ; _color = Colors.blue; }); } @override Widget build(BuildContext context) { return Column( children: [ MyCircle(size: _size, color: _color, callback: _callback), MyCircle(size: _size, color: _color, callback: _callback) ], ); } }
我们将MyCircle的两个状态提升到了共有父组件MyParent中,MyCircle通过构造方法接收状态值。当状态发生变化时,仍是通过setState触发重建,此时会调用MyParent的build方法重新创建组件,然后就创建了新的两个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; set size(value) { _size = value; 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(); } void onStateChanged() { setState(() {}); } @override Widget build(BuildContext context) { return Container( width: widget.controller.size, height: widget.controller.size, decoration: ShapeDecoration(color: widget.controller.color, shape: CircleBorder()), child: Center( child: TextButton(onPressed: () { 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) {...} @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 class CircleController { final size = ValueNotifier<double >(100 ); final color = ValueNotifier<Color>(Colors.red); }
实际上由于size和color它们是一组的,应该包装成一个整体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(); } void onStateChanged() { setState(() {}); } @override Widget build(BuildContext context) { return Container( 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; }class _MergingListenable extends Listenable { _MergingListenable(this ._children); final Iterable <Listenable?> _children; @override void addListener(VoidCallback listener) { for (final Listenable? child in _children) { child?.addListener(listener); } } @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}) { listenable = Listenable.merge([ controller.size, controller.color ]); } @override State<MyCircle> createState() => _MyCircleState(); }class _MyCircleState extends State <MyCircle > { @override void initState() { super .initState(); widget.listenable.addListener(onStateChanged); } @override void dispose() { 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(); } void _onStateChanged() { setState(() {}); } @override Widget build(BuildContext context) { 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) { return MyListenableBuilder( 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,以及它的子组件Center和TextButton以及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 { final Widget Function (BuildContext context, Widget? child) builder; final Listenable listenable; 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) { 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: 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就是我们实现的第一版的不带child的MyListenableBuilder。
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(); } void _handleChange() { if (!mounted) { return ; } setState(() { }); } @override Widget build(BuildContext context) => widget.build(context); }
整体下来没什么特殊的,和我们自定义的MyListenableBuidler是一样的逻辑。使用起来也是一样的,我们直接修改MyCircle,将MyListenableBuilder替换成内置的ListenableBuilder就行,其他一模一样,完全不需要改动。
基于Listenable的观察者,实际上还有好几个对应的组件可以使用:
1 2 3 4 5 6 7 8 9 const ListenableBuilder({ super .key, required super .listenable, required this .builder, this .child, });
AnimatedBuilder(和ListenableBuilder一模一样,就是名字不一样)1 2 3 4 5 6 7 8 9 const AnimatedBuilder({ super .key, required Listenable animation, required super .builder, super .child, })
1 2 3 4 5 6 7 8 9 const ValueListenableBuilder({ super .key, required this .valueListenable, 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 final _size = ValueNotifier<double >(100 ); ValueListenableBuilder( alueListenable: _size, builder: (context, value, child) { return Container( 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一把梭就行了。
为了管理和复用状态,我们使用状态提升的方式,将状态提取到最顶层父组件中,由父组件进行持有,从而可以共享到各个子组件中。但是状态提升带来了两个痛点:一是状态刷新会导致所有界面全部重建,一是状态需要通过构造方法一层一层传递到子组件中。第一个痛点我们使用ChangeNotifier和ListenableBuilder解决了,接下来就是第二个痛点了,如何将状态传递到子组件中。
我们之所以使用构造方法传递状态,是因为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 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) { 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( 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) { 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实现局部刷新。
接下来在继续看下InheritedWidget的updateShouldNotify方法,当然前面我们重写时是直接返回了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 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: [ 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: [ 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 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 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 ), child: MyChild() ) ),
如果在界面中,用到了多个InheritedWidget,并且它们的类型是一样的,这样在子组件中通过context获取时,就只会查找到离他最近的那个InheritedWidget。例如上面的例子中,MyChild在获取状态时,拿到的是200,而不是100。
总结 Flutter状态管理的实质就是状态提升,将状态提升到一定的高度后,方便对其进行管理以及方便子组件对状态的共享使用。
后面我们所做的一系列操作,都是为了解决状态提升后引入的问题,一是状态传递问题,一是全局刷新问题。针对这两个问题,我们可以引用InheritedWidget来解决状态的传递问题,然后在引入ChangeNotifier+ListenableBuilder解决全局刷新的问题。
整体逻辑如下图:顶层用InheritedWidget提供状态,需要使用状态的组件用ListenableBuilder包裹,不变的用child引用。然后注册监听,当监听到状态变化时,触发rebuild刷新界面。