JHHK

欢迎来到我的个人网站
行者常至 为者常成

状态管理️⭐️⭐️

目录

State 表示的是整个 Widget 当前所拥有的运行时状态。

State
│
├── 数据
├── 控件状态
├── 动画状态
├── Controller
└── 生命周期

setState

一、介绍

Flutter 只刷新需要更新的 Widget,而不是整个页面。widget内的更新。

setState 的本质不是修改数据,而是通知 Framework我修改了状态,请重新计算 UI。

二、原理

用户点击按钮 → setState() → 执行回调 (修改 State)→

markNeedsBuild() → 当前element(_dirty = true) → 加入 Dirty List →

等待下一帧(Vsync)→ 当前Element.rebuild() → 当前State.build() →

生成新的 Widget Tree → Widget Diff[ 主要方法:当前Element.updateChild() ] →

RenderObject更新 → Layout → Paint → GPU 渲染

重要方法:当前Element.updateChild() 的思想

Element? updateChild(Element? child, Widget? newWidget, Object? newSlot) {
    
    Widget oldWidget = child.widget;

    // const 修饰的widget 编译器只会创建一个对象。
    // 所以有 const 修饰会直接返回。rebuild的时候不会调用childWidget的state的build方法。
    if (oldWidget == newWidget) {
        return oldElement;
    }
    
    if (Widget.canUpdate(oldWidget, newWidget)) {
        // 会触发childWidget 的 state 的 build 方法
        oldElement.update(newWidget);
        return oldElement;
    }
    
    deactivateChild(oldElement);
    
    // 重新创建新的element,从而重新创建新的state,所以此处会观察到调用了state的init方法。     
    final Element newChild = inflateWidget(newWidget);
    return newChild;
}
                    updateChild()
                         │
              oldWidget == newWidget ?
                 │               │
             Yes │               │ No
                 ▼               ▼
             直接 return     Widget.canUpdate()
                                 │
                                 ▼
                       oldElement.update(newWidget)
                                 │
                     ┌───────────┴───────────┐
                     ▼                       ▼
            StatelessElement         StatefulElement
                     │                       │
                  build()         didUpdateWidget()
                                             │
                                          build()

三、几个问题

1、为什么不立即build
先标记为dirty,加入列表,下一帧来的时候统一build,节省性能。

2、多次 setState() 为什么最终只会 build() 一次?
同一个 Element 在一帧内只会加入 Dirty List 一次(_dirty 标记避免重复)。

3、为什么 build() 不能调用 setState()?
setState 会标记当前 State 为脏状态(dirty)下一帧会执行 build() 刷新界面。
如果在build()中调用setState会进入标记dirty执行build,再标记dirty执行build的循环状态。
页面会直接卡死。

4、子Widget是如何递归更新的?
当前Element.updateChild()方法,会触发子Widget的build方法。
生成新的 Widget Tree → Widget Diff[ 主要方法:子Element.updateChild() ] →
子Element.updateChild()方法,会触发孙子Widget的build方法。
。。。

InheritedWidget

一、介绍

7.2 数据共享(InheritedWidget)

InheritedWidget提供了一种在 widget 树中 从上到下共享数据的方式。
Flutter SDK中正是通过 InheritedWidget 来共享应用主题(Theme)和 Locale (当前语言环境)信息的。

我们创建一个继承自InheritedWidget 的 ShareDataWidget

class ShareDataWidget extends InheritedWidget {
  const ShareDataWidget({
    Key? key,
    required this.data,
    required Widget child,
  }) : super(key: key, child: child);

  final int data; //需要在子树中共享的数据,保存点击次数

  //定义一个便捷方法,方便子树中的widget获取共享数据
  static ShareDataWidget? of(BuildContext context) {

    // context 实际是 element,通过当前element节点向上寻找最近的InheritedWidget
    // 同时还会进行注册依赖关系,所以当data数据变化时,调用context对应state的didChangeDependencies()
    return context.dependOnInheritedWidgetOfExactType<ShareDataWidget>();


    // 如果只想引用ShareDataWidget数据,不希望在ShareDataWidget的data发生变化时调用context对应的didChangeDependencies()方法使用下面的方法
    // 该方法与上边的方法相比,不会进行关系注册
    // return context.getElementForInheritedWidgetOfExactType<ShareDataWidget>()!.widget as ShareDataWidget;
  }


  //该回调决定当data发生变化时,是否通知子树中依赖data的Widget重新build
  @override
  bool updateShouldNotify(ShareDataWidget old) {
    return old.data != data;
  }
}

我们再创建一个使用了共享数据的 ShareDataChildWidget


class ShareDataChildWidget extends StatefulWidget {
  ShareDataChildWidget({Key? key}):super(key: key);
  
  @override
  State createState() => _ShareDataChildWidgetState();
}


class _ShareDataChildWidgetState extends State<ShareDataChildWidget> {
  
  @override
  Widget build(BuildContext context) {
    // return Text("text");
    
    // 使用InheritedWidget中的共享数据
    // 同时text对应的element也会缓存InheritedWidget,不需要每次都去遍历element树,节省性能。      
    return Text(ShareDataWidget.of(context)!.data.toString());
  }

  @override
  void didChangeDependencies() {
    super.didChangeDependencies();
    //父或祖先widget中的InheritedWidget改变(updateShouldNotify返回true)时会被调用。
    //如果build中没有依赖InheritedWidget,则此回调不会被调用。
    print("Dependencies change ${widget.key}");
  }
}

所以真正重要的:
① 找 InheritedWidget
② 注册依赖
③ 建立缓存

二、原理

InheritedElement.update()

        │
        ▼
updateShouldNotify(oldWidget)

        │
   ┌────┴────┐
   │         │
false      true
   │         │
   ▼         ▼
return  notifyClients()
    
             │
             ▼
       遍历 _dependents
             │         
             ▼
        子 Element
             │
             ▼
     didChangeDependencies()
             │
             ▼
     markNeedsBuild()
             │
             ▼
     BuildOwner.dirtyElements
             │
             ▼
          build()

三、几个问题

1、为什么在测试代码中updateShouldNotify返回了false,还是调用了ShareDataChildWidget的build方法,并且数据还更新了?

实际上,Flutter 有两套机制:
第一套:Widget 自己更新(setState)
第二套:别人通知我更新(InheritedWidget)

它们最终都会走:
markNeedsBuild() –> build()

当返回false的时候InheritedWidget的更新逻辑中断了,从测试代码我们也可以看出因为didChangeDependencies方法没有调用。
那为什么build方法还是调用了,并且数据也更新了呢?
那是因为setState引起的子Widget的更新触发了build调用。
数据能更新是因为ShareDataWidget.of(context)!.data还是可以拿到最新的数据的。
为了消除setState的影响,我们可以使用const修饰。

const Padding(
  padding: const EdgeInsets.only(bottom: 20.0),
  // 此处使用const,是为了消除setState触发的更新逻辑对InheritedWidget更新逻辑的影响
  child: const ShareDataChildWidget(),
),

2、InheritedWidget为什么没有build方法
在更新原理图中,我们可以注意到InheritedElement的update方法是调用的notifyClients()方法更新依赖Widget。
而不是像setState原理图中element的update方法会触发build方法从而递归更新子Widget。
这也是InheritedWidget没有build方法的原因,因为他们是两套完全不同的更新机制。

四、应该在didChangeDependencies()中做什么?

一般来说子 widget 很少会重写此方法,因为在依赖改变后 Flutter 框架也都会调用build()方法重新构建组件树。
但是,如果你需要在依赖改变后执行一些昂贵的操作,比如网络请求,这时最好的方式就是在此方法中执行, 这样可以避免每次build()都执行这些昂贵操作。
也可以在data改变后不调用setState,避免build的调用,需要的逻辑在 didChangeDependencies 中进行,根据具体情况具体分析。

Provider

一、状态分类

在 Flutter 中,可以把状态分成两类:
① 局部状态(Local State)
只属于一个 Widget,生命周期跟 Widget 一致,不需要共享。
最佳方案:

setState()

② 共享状态(Shared State)
多个Widget使用,多个页面使用,生命周期通常长于单个Widget,需要统一维护。
最佳方案:

ChangeNotifier  // 添加监听,发布通知
        +
InheritedWidget // 处理监听事件,更新widget
        =
Provider

二、原理

注:为了理解主干,流程进行了简化省略了很多细节。

CounterNotifier
      │
      ▼
notifyListeners()
      │
      ▼
InheritedNotifierElement._handleUpdate()
      │
      ▼
InheritedElement.updated()
      │
      ▼
updateShouldNotify()
      │
      ▼
notifyClients()
      │
      ▼
dependents.markNeedsBuild()

三、context.watch 和 context.read

Provider 提供了两个最常用的扩展方法来获取状态:context.watchcontext.read
它们的本质都是通过 InheritedWidget 来获取共享数据,但注册依赖的方式不同,从而决定了它们的使用场景。

1、context.watch

  • 作用:监听 ChangeNotifier 的变化,当数据改变时会触发当前 Widget 重新 build
  • 特点:建立依赖关系(订阅),数据变了 UI 就刷新。
  • 使用场景:在 build 方法中获取需要响应变化的数据。
  • 注意:只能在 build 方法内部使用,不能在回调、生命周期方法(如 initState)中使用,否则会报错。
// 在 build 中使用,count 变化时会自动刷新当前 Widget
final count = context.watch<CounterNotifier>().count;
T watch<T>() {
  // 调用 dependOnInheritedWidgetOfExactType 建立依赖
  return Provider.of<T>(this, listen: true);
}
  static T of<T>(BuildContext context, {bool listen = true}) {
    final inheritedElement = _inheritedElementOf<T>(context);

    if (listen) {
      context.dependOnInheritedWidgetOfExactType<_InheritedProviderScope<T?>>();
    }

    final value = inheritedElement?.value;

    if (_isSoundMode) {
      if (value is! T) {
        throw ProviderNullException(T, context.widget.runtimeType);
      }
      return value;
    }
    return value as T;
  }

为什么不能在 initState 和 回调 中使用?
① Flutter Framework 要求:
Dependency Graph 必须在 Widget 的构建阶段(build 或 didChangeDependencies)建立。
这是Framework做的一层保护,统一放在build中去建立和销毁依赖关系更便于管理。所以在initState和回调中调用context.watch会触发Framework内的断言。
② 另外Flutter 本质上是声明式 UI 框架。UI 应该由 build 描述,依赖关系也应该由 build 推导,而不是由事件驱动动态建立。watch() 被限制在 build 中,正是为了保证这一设计原则。
③有些博客解释initState执行时:Widget 刚初始化,Element 还未挂载,所以不能建立依赖关系。这是错误的,createElement -> mount -> initState 这时其实已经挂载。

2、context.read

  • 作用:直接获取 ChangeNotifier 实例,不建立依赖关系,不监听变化
  • 特点:只读取一次,数据变化不会触发重建。
  • 使用场景:在事件回调(onPressed)、initState、dispose 等地方获取实例调用方法。
  • 注意:不要在 build 方法中使用(会失去响应式能力,且无法触发重建)。
T read<T>() {
  return Provider.of<T>(this, listen: false);
}
// 在按钮回调中调用方法,无需监听变化
onPressed: () {
  context.read<CounterNotifier>().increment();
},

3、核心区别对比

方法 是否监听变化 是否触发重建 使用位置
context.watch build 方法内
context.read 回调 / 生命周期方法

4、最简 Demo

import 'package:flutter/material.dart';
import 'package:provider/provider.dart';

// ① 定义状态:继承 ChangeNotifier
class CounterNotifier extends ChangeNotifier {
  int count = 0;

  void increment() {
    count++;
    notifyListeners(); // 通知监听者刷新
  }
}

void main() {
  runApp(
    // ② 顶层注入 Provider
    ChangeNotifierProvider(
      create: (_) => CounterNotifier(),
      child: const MyApp(),
    ),
  );
}

class MyApp extends StatelessWidget {
  const MyApp({super.key});

  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      home: Scaffold(
        appBar: AppBar(title: const Text('Provider Demo')),
        body: const Center(child: CounterText()),
        // ③ context.read:在回调中调用方法
        floatingActionButton: FloatingActionButton(
          onPressed: () => context.read<CounterNotifier>().increment(),
          child: const Icon(Icons.add),
        ),
      ),
    );
  }
}

class CounterText extends StatelessWidget {
  const CounterText({super.key});

  @override
  Widget build(BuildContext context) {
    // ④ context.watch:在 build 中监听数据
    final count = context.watch<CounterNotifier>().count;
    return Text(
      '$count',
      style: const TextStyle(fontSize: 48),
    );
  }
}

Consumer 和 Selector

一、Consumer

Consumer 不是减少 rebuild,而是改变 rebuild 的边界。
Consumer的本质就是一个封装好的:StatelessWidget。
被注册的是Consumer对应的Element,所以被调用的build也是Consumer的build。

1、原理如下

class Consumer<T> extends SingleChildStatelessWidget {
  
  @override
  Widget build(context) {

      final value =
          Provider.of<T>(
              context,
              listen:true,
          );


      return builder(
          context,
          value,
          child,
      );
  }
}

2、Consumer 的 child 参数

Consumer 第一次 builder 时会保存child,builder重新执行时会直接服用 child。
如果不需要刷新,放child可以避免重复构建。

二、Selector

Selector 的使用

  Selector<UserNotifier, int>(
    selector: (BuildContext context, UserNotifier notifier) => notifier.age,
    builder: (BuildContext context, int age, Widget? child) {
      print("年龄 build");
      return Text(
        "年龄:$age",
        style: const TextStyle(fontSize: 22),
      );
    },
  ),

Selector 可以只监听对象中的某一部分数据。
Selector 的本质与 Consumer相同,只是多了一步比较 另外不同的是,Selector是一个封装好的:StatefulWidget。因为它内部需要保存old数据。

1、原理如下

notifyListeners()
        │
        ▼
Selector 收到通知
        │
重新执行 selector()
        │
拿到新的结果
        │
比较:
old == new ?
      │
 ┌────┴────┐
 │         │
相等      不相等
 │         │
不刷新    builder 重建

三、总结

Consumer:缩小 Widget 的刷新范围,任何 notifyListeners() 都会重建。
Selector:在 Consumer 的基础上增加了数据比较,只有选中的数据发生变化才会重建。
child 参数可以进一步避免静态子树重复创建。

在实际开发中,如果页面只依赖某个字段(如 name、count、themeMode),优先使用 Selector;
如果整个对象都会参与 UI 构建,使用 Consumer 即可。


行者常至,为者常成!





R
Valine - A simple comment system based on Leancloud.