状态提升(Lifting-state-up)
把子组件的状态,提升到上级组件中,从而实现在多个组件之间共享和同步数据的效果
以 flutter counter demo,那个按按钮+1 的来说,现在的 count 是几,不是存在页面显示几的地方,而是作为 HomePage 的一个 state,这样就提到了上级;子组件那个按钮的 press 事件,也不是说找到页面显示几的 Text 元素,然后改那个元素,而是改 state
子组件获取和控制父组件的状态
父组件传给子组件,只需要直接传参数进去即可
class Child extends StatelessWidget {final int stateFromParent;const Child({Key? key, required this.stateFromParent}): super(key: key);
}
子组件想要修改父组件的变量,需要使用 callback。在 dart 中,函数也可以作为参数传递,
class Child extends StatelessWidget {final void Function() changeStateInParent;const Child({Key? key, required this.changeStateInParent}): super(key: key);Widget build(BuildContext context) {return ElevatedButton(onPressed: changeStateInParent);}
}
关于 Funtion 类型
final void Function() changeStateInParent;
void 是代表这个函数的返回值
()是代表这个函数没有参数
为什么是这个顺序呢,因为我们平时写函数的时候也是这样
int fun(int x, int y){...
}
那么这个 fun 函数的函数类型就是 int Function(int x, int y)
void Function()可以缩写为 VoidCallback,因为 flutter 里面有如下定义
typedef VoidCallback = void Function();
关于 BuildContext
BuildContext 在 flutter 中是用于定位当前 Widget 在 Widget 树中位置的对象,用于访问父 widget 和其他相关信息,在构建 UI 时调用
比如,用 Nevigator 进行页面导航的时候,需要使用 BuildContext 来获取当前 Scaffold(页面基本元素布局,如 appBar 之类的)或 MaterialApp,以执行页面跳转操作
BuildContext 还可用于查找和访问在 widget 树中的其他 widget
onPressed: () {Scaffold scaffold = Scaffold.of(context);scaffold.showSnackBar(SnackBar(content: Text('Button Pressed')))
}
控制器(父组件控制子组件的状态)
第一个思路,状态提升,将子组件的状态提升到父组件上,可以,但是有一些问题
- 子组件不是我们自己写的,而是用的别人的库,这样就没办法要求它提升到我们的父组件了
- 而且我们封装子组件的目的就是为了提高性能,结果提升到父组件了,又要整体进行重绘,如拆
- 如果这个状态是基础数据类型,那么父组件给子组件传递的是值,是一个副本,子组件去修改这个值的时候,修改不到父组件的版本
解决状态提升的基础数据类型问题
基础数据类型下,父组件给子组件传递的是值不是引用,父组件控制子组件的功能是好的,但是如果子组件想正常的管理自己的 state,就通知不到父组件
解决方法是将 state 从基础数据类型转换为一个复杂的结构,这样传的就是引用,而不是值了
class IntHolder {int value;IntHolder(this.value);
}class _ParentState extends State<Parent> {IntHolder ih = IntHolder(1);Widget build(BuildContext context) {return Child(ih);}
}class Child extends StatefulWidget {final IntHolder ih;const Child({Key? key, required this.ih});
}class _ChildState extends State<Child> {Widget build(BuildContext context){return Column(children: [Text(widget.ih.value);ElevatedButton(onPressed: () {setState(() {widget.ih.value = 2; });});]);}
}
解决整体重绘的问题
子组件可以监听一个 stream,当 stream 发生变化的时候,这个子组件也可以发生变化,这个解决方案有点太“重”了,我们这个假如就是传递一个数,结果整了一个 stream,没必要
如果父组件有这么一个功能,当某个 state 变化的时候,能够“通知”相对应的子组件让它变化,这个过程中父组件自身不变,就可以完美解决这个问题了
这就要求 state 不是直接以 state 形式被提升在父组件里,而是被封装起来。即使里面的值发生变化,从父组件的角度看,这个被封装起来的块没有发生变化,就不会引发父组件重绘;同时,里面的值发生变化后,又要能够通知子组件,让子组件进行重绘
flutter 有一个 ChangeNotifier 类,可以有这样的效果,当 object 更新时,通知子组件更新
下面的例子是官方文档举的例子,其中 CounterModel 就混入了 ChangeNotifier,当_count 变化时,会通知这个 notifier 所在的 ListenableBuilder,然后 ListenableBuilder 重新使用 builder 进行 build
// 这里的 with 相当于 mixin,直接混入进去,如 class Dove extends Bird with Walker, Flyer {}
class CounterModel with ChangeNotifier {int _count = 0;int get count => _count;void increment() {_count += 1;notifyListeners(); // 在 _count 变化后通知}// 下面还能再加一些其他的更新 _count 的逻辑
}class ListenableBuilderExample extends StatefulWidget {const ListenableBuilderExample({super.key});State<ListenableBuilderExample> createState() =>_ListenableBuilderExampleState();
}class _ListenableBuilderExampleState extends State<ListenableBuilderExample> {final CounterModel _counter = CounterModel();Widget build(BuildContext context) {return MaterialApp(home: Scaffold(appBar: AppBar(title: const Text('ListenableBuilder Example')),body: CounterBody(counterNotifier: _counter),floatingActionButton: FloatingActionButton(onPressed: _counter.increment,child: const Icon(Icons.add),),),);}b
}class CounterBody extends StatelessWidget {const CounterBody({super.key, required this.counterNotifier});final CounterModel counterNotifier;Widget build(BuildContext context) {return Center(child: Column(mainAxisAlignment: MainAxisAlignment.center,children: <Widget>[const Text('Current counter value:'),// Thanks to the ListenableBuilder, only the widget displaying the// current count is rebuilt when counterValueNotifier notifies its// listeners. The Text widget above and CounterBody itself aren't// rebuilt.ListenableBuilder(listenable: counterNotifier, // 每当 counterNotifier 里 notifyListeners 后builder: (BuildContext context, Widget? child) { // 重新 buildreturn Text('${counterNotifier.count}');},),],),);}
}
这个 CounterModel 就是一个控制器,可以直接把它重新命名为 CounterController
如果是例子中这样,只为了一个变量实现一个类的 ChangeNotifier,有点繁琐。Flutter 为这种单变量值发生变化的 Notifier 提供了一个专门的类,ValueNotifier。CounterController 可以直接写成:
class CounterController {ValueNotifier count = ValueNotifier(0); // ValueNotifier 本身继承了 ChangeNotifier
}
注意,这样修改之后,count 变成了一个 Notifier,所以使用 count 值的时候,要写成 count.value
ListenableBuilder(listenable: widget.controller.count, // 每当 ValueNotifier count 的value 变化builder: (BuildContext context, Widget? child) { // 重新 buildreturn Text('${widget.controller.count.value}');},),
如果同时需要监听多个 Notifier 的变化,使用 Listenable.merge
ListenableBuilder(listenable: Listenable.merge([widget.controller.count,widget.controller.fontSize])builder: (BuildContext context, Widget? child) { // 重新 buildreturn Text('${widget.controller.count.value}',style: TextStyle(fontSize: widget.controller.fontSize.value),);},),
如果是这样多个 Notifier,可以统一放在一起,提升到父组件中,这就是控制器类(Controller)
组件在开发的时候,一般都遵循这个规范,使用 Controller。所以我们用别人写的组件的时候,只需要用他们写好的 Controller 就能实现父组件控制子组件的状态,且不影响子组件自己控制自己状态了
继承式组件 InheritedWidget
如果 state 被提升到顶部,就要一层一层传,这样,每一层组件的构造函数都有大量的参数
InheritedWidget 可以解决这一问题
class MyColor extends InteritedWidget {final Color color;MyColor({super.key, required super.child, required this.color});
}// 在组件树较高的位置用 MyColor 包裹
// 这样里面的所有组件都能访问到 color 这个属性
class MyApp extends StatelessWidget {const MyApp({Key? key}): super(key: key);Widget build(BuildContext context) {return MyColor(child: MaterialApp(....),color: Color.red);}
}
dependOnInheritedWidgetOfExactType:依赖于 继承式组件 of 特定的 Type
因为在某个组件所在的那一支上可能有其他的继承式组件,我们要找那个特定的继承式组件(这里 MyColor)
具体的寻找方法,就是从这个组件向上,向组件树的根部去找,直到找到 ExactType
如果有多个同样的组件(MyColor),选最近的那一个
组件如何访问到这个继承式组件的属性呢
Widget build(BuildContext context) {final myColor = context.dependOnInheritedWidgetOfExactType<MyColor>();return Container(color: myColor.color,...);
}
关于 updateShouldNotify
InteritedWidget 有一个函数 updateShouldNotify,是指当 InheritedWidget 发生变化的时候,需不需要通知相关的子组件进行重绘
按理说,变了肯定要通知,要不然不白变了吗
但是有的时候,InteritedWidget 的属性是被 setState 变掉的,setState 本身就会让子组件刷新,所以不用通知,子组件本来就是新的
class MyColor extends InteritedWidget {final Color color;// color 是父组件传进来的,是父组件的 state。父组件 setState 后子组件也重绘了MyColor({super.key, required super.child, required this.color}); bool updateShouldNotify(covariant Inherited oldWidget) {// return true;return false; // 颜色也会变化,不过不是 updateShouldNotify 导致的}
}
虽然能够成功更新颜色,但这种一 setState,全局组件就要重绘,不是我们想看到的,所以要多加 const
return const Child();
这样的话父组件里的 state 发生变化,子组件就不会自动重绘了
这样 updateShouldNotify 的重要性就凸显出来了
updateShouldNotify(covariant Inherited oldWidget) {return color != oldWidget.color;
}
bool
当新的 color 不等于旧的 color 时,告诉子组件刷新
如果是希望获取一次 color 后就不管了,不再监听之后 color 的更新,可以使用 getInheritedWidgetOfExactType
final myColor = context.getInheritedWidgetOfExactType<MyColor>();
在 color 更新后,如果 updateShouldNotify 为 true,且子组件是 dependOnInheritedWidgetOfExactType,也会调用 didChangeDependencies 这个生命周期函数
关于 of
如果能将 dependOnInheritedWidgetOfExactType 封装起来,不用每次子组件使用的时候都写这么长一串,那就更好了。直接将这个封装到 MyColor 这个继承式组件中
有两种,of 和 maybeOf
of 指一定能从这个 context (组件树往上找)中找到 MyColor
maybeOf 指有可能能找到,可能为空
class MyColor extends InteritedWidget {final Color color;MyColor({super.key, required super.child, required this.color});static MyColor of(BuildContext context) {return context.dependOnInheritedWidgetOfExactType<MyColor>()!;// !代表确信不为空}// 或static MyColor maybeOf(BuildContext context) {return context.dependOnInheritedWidgetOfExactType<MyColor>();} bool updateShouldNotify(covariant Inherited oldWidget) {return color != oldWidget.color;}
}
通过这种写法,子组件找到继承式组件就能更加简洁了
color: MyColor.of(context).color;