从底层逻辑看看 flutter 的状态管理

0 阅读7分钟

状态管理

框架层已实现 diff 算法,为什么框架一直在解决局部刷新的问题?

  1. Diff 发生的时间节点太晚了:当 Flutter 进行 canUpdate 比对时,新的 Widget 节点已经被 new 出来了,且父组件的 build() 函数已经执行完毕。
  2. Diff 的目的只是复用 Element:Flutter 仅仅是复用旧的 Element 和 RenderObject 节点,把新的 Widget 配置塞给它们。

小结:原生框架存在 Widget() 和 build()重建,及子节点的属性变化其子树重建时

  1. 虽然单个 Widget 对象很轻量,但如果一个大页面包含几百上千个 Widget

    1. 每次根部刷新,所有子 Widget 的构造函数全都会被重新调用一次。
    2. 内存中会瞬时产生成千上万个废弃的旧 Widget 垃圾对象,直接给 Dart 的垃圾回收(GC)带来巨大压力,引发 GC 停顿,导致界面丢帧、卡顿。
  2. CPU 开销:无意义的 Dart 代码逻辑重复执行。每个 Widget 的 build() 函数里往往写着不少逻辑(如:数据格式化、字符串拼接、Theme.of(context) 哈希检索等)。

  3. Layout / Paint 阶段的连锁打脏,当新的 Widget 配置塞给 RenderObject 时:

    1. 如果某些属性改变(例如文本变长、边距改变),RenderObject 会被标脏 markNeedsLayout()。
    2. 这会导致 Flutter 重新计算排版(Measure & Layout)。由于布局存在树状依赖,一个子节点的改变可能会引发整棵树重新计算几何尺寸。

状态管理解决了那些问题?

  1. 跨组件传递数据问题

  2. 局部刷新问题

  3. 业务逻辑与 UI 视图解耦(架构清晰度)

  4. 状态的生命周期与资源自动管理(防止内存泄漏)

  5. “依赖注入”的编译期检查(Riverpod)

默认状态管理

  1. 响应式、声明式区别:

    1. 响应式需要开发者控制刷新状态,例如更新了状态,需要去手动更新view;
    2. 声明式直接调用setState(),交给sdk去处理刷新。
  2. 声明式(flutter、SwiftUI):

    1. 优势: 让开发者摆脱组件的繁琐控制,聚焦于状态处理

    2. 缺点:

      1. 控制刷新范围;
      2. 逻辑与业务耦合;
      3. 夸组件访问困难;

InheritedWidget:

为什么需要InheritedWidget?

比如中间隔了 9 层,手动怎么实现:

  1. 传参解决:代码臃肿,中间件被污染且代码难以被维护。

  2. 全局单例:全局变量无法感知UI 树的局部生命周期,数据变了,UI 也不会自动刷新

Flutter 必须提供一种能够自顶向下隐式共享数据,同时又能精确驱动 UI 依赖更新的机制,这就是 InheritedWidget 诞生的初衷。

原理:

必须要解决的问题:

  1. 自定义的 InheritedWidget 怎么存储? _inheritedElements

  2. 订阅的数据怎么刷新? _dependents()

  3. 怎么驱动订阅的数据?updateShouldNotify()

  4. 数据结构: InheritedWidget 在 Element 层采用了 哈希表 + 指针向下传递 的映射结构。普通 Element 节点:在 mount 阶段直接继承并指向父节点的 _inheritedElements 指针(零内存开销)

    1. //1. 每一个 Element 节点内部都维护着一个字段
      Map<Type, InheritedElement>? _inheritedElements;
      
      
      //2. Element.mount 源码示意
      void mount(Element? parent, Object? newSlot) {
        _parent = parent;
        // 直接继承父节点的 Map 引用,内存共享,效率极高
        _inheritedElements = parent?._inheritedElements; 
        ...
      }
      
      //例:
      RootElement (根节点)
      └── Theme (InheritedWidget A)
      └── Container (普通节点 1 )
      ├── Column (普通节点 2 )
      │    └── Text (普通节点 3 )
      └── Provider (InheritedWidget B)
      └── Button (普通节点 4 )
      
      //1. 当遇到 Theme 时,浅拷贝产生  Map 2 ➔ { Type(Theme): ThemeElement }
      //2. 当遇到 Provider 时,从 Map 2 浅拷贝产生 Map 3 ➔ { Type(Theme): ThemeElement, Type(Provider): ProviderElement } 
      
  5. 查找机制:Theme.of(context) 当子组件通过 context.dependOnInheritedWidgetOfExactType<T>() 获取数据时,直接通过 _inheritedElements[T] 获取对应的 InheritedElement

    1. 建立依赖:InheritedElement 内部维护了一个订阅者集合 _dependents:
  6. 怎么刷新: updateShouldNotify()

    1.   触发时机:当父级组件重新构建(rebuild),向树中传入了新的 InheritedWidget 实例时触发。
    2.   触发 setState(),调用到widget.update()调到这个方法,触发updateShouldNotify()
  7. context.getInheritedWidgetOfExactType()与context.dependOnInheritedWidgetOfExactType<T>()的区别

    1.   是否注册依赖在 _dependents, dependOnInheritedWidgetOfExactType 当前组件会重新 build, getInheritedWidgetOfExactType 不会重新 build在点击事件时调用。
使用
 //定义
class ShareDataWidget extends InheritedWidget {
  ShareDataWidget({required this.data, required Widget child}) : super(child: child);

  final int data; //需要在子树中共享的数据

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

  //该回调决定当data发生变化时,是否通知子树中依赖data的Widget
  //是 widget.update()调到这个方法
@override
  bool updateShouldNotify(ShareDataWidget old) {
    //如果返回true,则子树中依赖(build函数中有调用)本widget
 //的子widget的`state.didChangeDependencies`会被调用
return old.data != data;
  }
}

//包裹
ShareDataWidget(
  //使用ShareDataWidget
data: count,
  child: child,
)

//使用
ShareDataWidget.of(context)!.data
缺点
  1. 样板代码极多,手写封装繁琐

  2. 缺少灵活的“生命周期管理与资源释放”

    1. 原生 InheritedWidget 无法自己管理持久状态和监听挂载/销毁
    2. InheritedWidget 只是一个纯粹的数据透传容器,它自己不具备异步加载、依赖注入和资源销毁的能力。
  3. 缺乏“条件刷新/局部过滤”机制(Selector / Consumer)

    1. 局部刷新:整个 model的任一对象改变,都会刷新
    2. 条件过滤
  4. 无法优雅地处理“数据组合与链式依赖”(ProxyProvider),原生 InheritedWidget:处理这种嵌套依赖非常痛苦,需要层层嵌套,并在 didUpdateWidget 中手动传递和更新旧数据。

Provider

原理

它的底层本质是对 Flutter 原生组件 InheritedWidget 进行的高级封装与增强,并结合了 ChangeNotifier (观察者模式) 来解决数据传递与局部刷新的问题。

有InheritedWidget为什么还需要Provider?
  1. 代码简化

  2. 提供生命周期管理及资源自动释放

  3. 提供“条件刷新/局部过滤”

  4. ProxyProvider解决层层嵌套问题

如何封装一个Provider:

提供了 child缓存机制,避免了无效的刷新

内部封装了StatefulWidget,实现生命周期管理

  1. CommonInheritedProvider extend InheritedWidget:正常Provider的使用,需要自定义

  2. CustomModel对象,继承自ChangeNotifier

  3. ChangeNotifierProvider StatefulWidget

    1. 使用CommonInheritedProvider包裹data和widget

    2. 充当观察者,监听Model对象属性变化,当对象变化时,进行setState()刷新UI

      1. setState() 是局部刷新。

        • 仅使用Provider引用了data的widget刷新,而不会整个外部Widget刷新,并且引用的widget用Builder改变了层级。
        • 外部传入的 widget 不会刷新,原因是外部的 widget 缓存;。
  4. ChangeNotifierProvider.of(context) 即Consumer 跨页面获取数据、消费数据,并自动刷

Riverpod

优点:

  1. 彻底摆脱 BuildContext 依赖

  2. 将错误扼杀在“编译期”(编译期类型安全)

    1. 泛型强制推导
    2. 基于指针引用的类型隔离
    3. 代码生成器
  3. 支持同类型多实例

缺点:

自由度比较高,新手容易乱写

Bloc / Cubit

Bloc(Business Logic Component)/ Cubit 的出现,就是为了提供强约束的规范化架构。

  1. 理解Bloc、Cubit

    1. Bloc的核心理念是通过事件驱动和响应式编程将 UI 与业务逻辑解耦。

    2. 核心组件:Event、Bloc、State、Stream(事件和状态传递的载体)

      1. // 本质也是InheritedWidget
        BlocProvider(
          create: (BuildContext context) => MyHomePage(
            title: '',
          ),
        );
        
        BlocProvider.of<>(context);调用了context.dependOnInheritedWidgetOfExactType()
        
        // 本质通过listener调用setState()
        BlocBuilder(
          builder: (BuildContext context, state) {},
        );
        
        @override
        Widget build(BuildContext context) {
          if (widget.bloc == null) {
            // Trigger a rebuild if the bloc reference has changed.
         // See https://github.com/felangel/bloc/issues/2127.
        context.select<B, bool>((bloc) => identical(_bloc, bloc));
          }
          return BlocListener<B, S>(
            bloc: _bloc,
            listenWhen: widget.buildWhen,
            listener: (context, state) => setState(() => _state = state),
            child: widget.build(context, _state),
          );
        }
        
        通过_stateController.stream绑定了Bloc与BlocBuilder的关系。emit最后是进行标脏
        
        context.read<DemoBloc>().add(DemoEvent()); 
        emit(state);
        
        final subscription = value.stream.listen(
          (dynamic _) => e.markNeedsNotifyDependents(),
        );
        
    3. Bloc局部刷新,使用buildWhen:

      1. BlocBuilder<CounterBloc, int>(
          buildWhen: ( previous,  current) => previous != current,
          builder: (context, count) {
            return Text(
              '$count',
              style: Theme.of(context).textTheme.displayLarge,
            );
          },
        )
        

GetX

  1. GetBuilder:底层原理: GetBuilder 是通过 InheritedWidget 实现的。当 Model 层的数据发生变化时,GetBuilder 会通过 InheritedWidget 机制通知相关的 Widget 进行重建。

  2. Obx:底层原理: Obx 使用了 Stream 来实现响应式更新。它订阅了被观察对象(通常是 Rx 类型的变量),当这些对象的值发生变化时,Obx 会收到通知并触发 Widget 的重建。

    1. class Controller extends GetxController {
        var count = 0;
        void increment() {
          count++;
          update();
        }
      }
      
      class HomePage extends StatelessWidget {
        @override
        Widget build(BuildContext context) {
          final Controller controller = Get.put(Controller()); // 将 Controller 注入到依赖中
      
          return Scaffold(
            appBar: AppBar(
              title: Text('GetX Example'),
            ),
            body: Center(
              child: Obx(() {
                return Text(
                  'Counter: ${controller.count}', // 使用 Obx 来自动监听 count 变化
                  style: TextStyle(fontSize: 30),
                );
              }),
            ),
            floatingActionButton: FloatingActionButton(
              onPressed: controller.increment, // 点击按钮时调用 increment 方法
              child: Icon(Icons.add),
            ),
          );
        }
      }
      
  3. 全局生命周期使用:GetxService

  4. 优缺点: 包含了局部刷新,跨组件问题,生命周期管理,业务逻辑与 UI 解耦等;

    1.   和 Provider 一样,没有编译检查,Get.find在没有注册时,出现直接报错