Flutter 的状态管理

状态提升(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')))
}

控制器(父组件控制子组件的状态)

第一个思路,状态提升,将子组件的状态提升到父组件上,可以,但是有一些问题

  1. 子组件不是我们自己写的,而是用的别人的库,这样就没办法要求它提升到我们的父组件了
  2. 而且我们封装子组件的目的就是为了提高性能,结果提升到父组件了,又要整体进行重绘,如拆
  3. 如果这个状态是基础数据类型,那么父组件给子组件传递的是值,是一个副本,子组件去修改这个值的时候,修改不到父组件的版本

解决状态提升的基础数据类型问题

基础数据类型下,父组件给子组件传递的是值不是引用,父组件控制子组件的功能是好的,但是如果子组件想正常的管理自己的 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;
相关推荐
鑫~阳11 分钟前
html + css 淘宝网实战
前端·css·html
Catherinemin15 分钟前
CSS|14 z-index
前端·css
2401_882727572 小时前
低代码配置式组态软件-BY组态
前端·后端·物联网·低代码·前端框架
NoneCoder2 小时前
CSS系列(36)-- Containment详解
前端·css
anyup_前端梦工厂2 小时前
初始 ShellJS:一个 Node.js 命令行工具集合
前端·javascript·node.js
5hand2 小时前
Element-ui的使用教程 基于HBuilder X
前端·javascript·vue.js·elementui
GDAL2 小时前
vue3入门教程:ref能否完全替代reactive?
前端·javascript·vue.js
六卿2 小时前
react防止页面崩溃
前端·react.js·前端框架
z千鑫3 小时前
【前端】详解前端三大主流框架:React、Vue与Angular的比较与选择
前端·vue.js·react.js
m0_748256143 小时前
前端 MYTED单篇TED词汇学习功能优化
前端·学习