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