Flutter 基础体系 · 第 38/80 篇。示例基于当前稳定 Flutter 与 Dart 3 语言能力;Android、iOS、桌面和 Web 差异会明确说明。

Flutter InheritedWidget:依赖注册、更新通知和状态框架基础

InheritedWidget 是 Flutter Widget 树中用于“向下传递数据”的基础机制。它解决的不是状态如何产生,而是一个祖先 Widget 的数据发生变化后,哪些后代需要重新获得数据并重新构建。

许多状态管理框架都建立在类似的机制之上:

  1. 祖先提供状态或服务;
  2. 后代查找祖先;
  3. 查找时注册依赖;
  4. 祖先更新时判断是否通知;
  5. 被通知的后代重新执行依赖处理和构建。

理解这条链路,才能正确理解 ProviderInheritedNotifierInheritedModel 等工具,而不是把 InheritedWidget 简单当成“全局变量”。


一、先区分 Widget、Element、BuildContext 和 State

在分析 InheritedWidget 之前,需要区分 Flutter 框架中的几个对象。

Widget 是不可变配置

Widget 描述“界面应该是什么样子”,通常是不可变对象:

class MessageWidget extends StatelessWidget {
  final String message;

  const MessageWidget({
    super.key,
    required this.message,
  });

  @override
  Widget build(BuildContext context) {
    return Text(message);
  }
}

message 变化时,通常不是修改原来的 Widget,而是创建一个新的 MessageWidget

Element 是 Widget 在树中的运行时实例

Flutter 会把 Widget 配置挂载到 Element 上:

Widget:不可变配置
   │
   ▼
Element:树中的运行时节点,保存生命周期和父子关系
   │
   ▼
RenderObject:负责布局、绘制的对象(并非所有 Element 都直接对应)

InheritedWidget 的依赖关系实际记录在 InheritedElement 上,而不是记录在 Widget 对象本身。

BuildContext 实际上指向 Element

BuildContext 是供 Widget 查询树结构的接口。常见情况下,传入 build 方法的 context 实际对应某个 Element。

例如:

final theme = Theme.of(context);

表面上是在调用 BuildContext,实际上 Flutter 会沿着当前 Element 的祖先链查找对应的 InheritedElement

因此,BuildContext 的位置很重要:

  • 它只能查找自己所在位置的祖先;
  • 同一个 Widget 在不同位置获得的祖先数据可能不同;
  • 在错误的 BuildContext 上查找,可能找不到期望的 InheritedWidget

State 保存 StatefulWidget 的可变状态

StatefulWidget 本身仍然是不可变配置,可变数据保存在 State 对象中:

class CounterHost extends StatefulWidget {
  const CounterHost({super.key});

  @override
  State<CounterHost> createState() => _CounterHostState();
}

class _CounterHostState extends State<CounterHost> {
  int count = 0;

  void increment() {
    setState(() {
      count++;
    });
  }

  @override
  Widget build(BuildContext context) {
    return Text('$count');
  }
}

setState 的作用是让当前 State 对应的 Element 变脏,并在后续构建阶段重新执行 build。它本身并不负责把数据传给任意后代;数据向下传播可以交给 InheritedWidget


二、InheritedWidget 到底解决什么问题

假设一个页面有多层 Widget:

App
└── Page
    └── Panel
        └── CounterText

如果 App 中有一个计数值,最直接的传递方式是逐层增加参数:

Page(count: count)
  └── Panel(count: count)
      └── CounterText(count: count)

这称为“属性逐层传递”。当中间层 Panel 不关心 count,却必须接收并继续传递时,代码会产生明显的传参噪声。

InheritedWidget 允许祖先把数据放在树中:

CounterScope(count: 10)
└── Page
    └── Panel
        └── CounterText

CounterText 可以直接从自己的 BuildContext 查找最近的 CounterScope,无需中间层传递参数。

但“能够查找”与“能够在变化时自动更新”是两个不同能力:

  • getInheritedWidgetOfExactType<T>():只查找,不注册更新依赖;
  • dependOnInheritedWidgetOfExactType<T>():查找,并注册依赖。

后者正是 InheritedWidget 的核心。


三、依赖注册:后代如何告诉祖先“数据变化时通知我”

3.1 依赖注册的 API

最常用的 API 是:

context.dependOnInheritedWidgetOfExactType<MyScope>()

以一个封装方法为例:

class CounterScope extends InheritedWidget {
  final int count;
  final VoidCallback onIncrement;

  const CounterScope({
    super.key,
    required this.count,
    required this.onIncrement,
    required super.child,
  });

  static CounterScope of(BuildContext context) {
    final result =
        context.dependOnInheritedWidgetOfExactType<CounterScope>();

    if (result == null) {
      throw FlutterError(
        'CounterScope.of() called with a context that does not contain '
        'a CounterScope.',
      );
    }

    return result;
  }

  @override
  bool updateShouldNotify(CounterScope oldWidget) {
    return count != oldWidget.count;
  }
}

后代使用:

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

  @override
  Widget build(BuildContext context) {
    final scope = CounterScope.of(context);

    return Text('count = ${scope.count}');
  }
}

执行 CounterScope.of(context) 时,Flutter 会完成两个动作:

  1. 沿祖先链查找最近的 CounterScope
  2. 把当前 Element 注册为这个 CounterScope 对应 InheritedElement 的依赖者。

第二步非常关键。没有依赖注册,祖先即使知道数据变了,也不知道应该通知哪些后代。

3.2 形式化描述

设某个 InheritedElementII,某个后代 Element 为 DD

DD 在构建期间调用:

context.dependOnInheritedWidgetOfExactType<T>()

可以抽象为:

DDependents(I)D \in Dependents(I)

其中:

  • Dependents(I)Dependents(I) 表示依赖 II 的所有后代 Element 集合;
  • DD 是当前调用查找 API 的 Element;
  • II 是沿祖先方向找到的匹配 InheritedElement

II 的 Widget 从旧值 WoldW_{old} 更新为新值 WnewW_{new} 时,Flutter 会计算:

N=updateShouldNotify(Wold,Wnew)N = updateShouldNotify(W_{old}, W_{new})

如果:

N=trueN = true

则通知依赖集合中的 Element;如果:

N=falseN = false

则不会因为这次 InheritedWidget 更新而通知它们。

因此,更新传播的基本条件可以写成:

D 被通知    DDependents(I)updateShouldNotify(Wold,Wnew)=trueD \text{ 被通知} \iff D \in Dependents(I) \land updateShouldNotify(W_{old}, W_{new}) = true

这解释了三个常见现象:

  • 没有调用 dependOn... 的后代不会自动更新;
  • 调用了 dependOn...,但 updateShouldNotify 返回 false,也不会更新;
  • 只要条件成立,即使后代在树上距离很远,也可以收到通知。

四、InheritedWidget 的更新通知过程

下面是一次典型更新的时序:

sequenceDiagram
    participant S as State
    participant P as 父 Element
    participant I as InheritedElement
    participant D as 依赖者 Element

    D->>I: build 时调用 dependOnInheritedWidgetOfExactType
    I->>I: 注册 D 为依赖者

    S->>S: setState
    S->>P: 触发祖先重新 build
    P->>I: 使用新 CounterScope 更新旧 Element
    I->>I: 调用 updateShouldNotify(old, new)

    alt 返回 true
        I->>D: didChangeDependencies()
        I->>D: 标记需要重新构建
        D->>D: 后续重新执行 build
    else 返回 false
        I-->>D: 不发送依赖变化通知
    end

需要注意:通知通常不是“立刻递归执行所有后代的 build”。框架先让依赖者失效或标记为需要构建,后续由 Flutter 的构建流程重新执行相应 Element 的生命周期。

4.1 父 Widget 必须先产生新的 InheritedWidget

updateShouldNotify 只会在 InheritedWidget 对应的 Element 收到新 Widget 配置时被调用。

例如:

@override
Widget build(BuildContext context) {
  return CounterScope(
    count: count,
    onIncrement: increment,
    child: const CounterPage(),
  );
}

count 改变后,CounterHost.build 返回了一个新的 CounterScope,Flutter 会尝试更新原来的 CounterScope 对应 Element。

如果父级根本没有重新构建,或者最终仍然复用了完全相同的 Widget 配置,InheritedElement 就没有新的配置可比较。

4.2 updateShouldNotify 只表达“依赖者是否需要被通知”

实现:

@override
bool updateShouldNotify(CounterScope oldWidget) {
  return count != oldWidget.count;
}

它不是用来判断两个 Widget 是否完全相等,也不是用来判断整个子树是否必须重建,而是回答:

使用了这个 InheritedWidget 的依赖者,是否需要重新响应这次数据变化?

如果 CounterScope 有多个字段,应只比较影响依赖者的字段:

class AppScope extends InheritedWidget {
  final String locale;
  final ThemeMode themeMode;
  final int unreadCount;

  const AppScope({
    super.key,
    required this.locale,
    required this.themeMode,
    required this.unreadCount,
    required super.child,
  });

  @override
  bool updateShouldNotify(AppScope oldWidget) {
    return locale != oldWidget.locale ||
        themeMode != oldWidget.themeMode ||
        unreadCount != oldWidget.unreadCount;
  }
}

如果某个字段发生变化,但所有依赖者都不需要感知,就可以不纳入判断。不过,这要求设计者明确知道该字段的语义边界。


五、didChangeDependencies 与 build 的关系

依赖者收到通知后,通常会经历:

didChangeDependencies()
        │
        ▼
build()

State.didChangeDependencies 的用途是处理“祖先依赖变化”:

class CounterLabel extends StatefulWidget {
  const CounterLabel({super.key});

  @override
  State<CounterLabel> createState() => _CounterLabelState();
}

class _CounterLabelState extends State<CounterLabel> {
  int? lastCount;

  @override
  void didChangeDependencies() {
    super.didChangeDependencies();

    final count = CounterScope.of(context).count;

    if (count != lastCount) {
      lastCount = count;
      debugPrint('CounterLabel observed count = $count');
    }
  }

  @override
  Widget build(BuildContext context) {
    return Text('count = ${CounterScope.of(context).count}');
  }
}

didChangeDependencies 会在以下情形调用:

  1. State 初始化后,首次 build 前;
  2. 已注册的 InheritedWidget 判断需要通知时;
  3. State 在树中的依赖环境发生变化时,例如被移动到另一个祖先环境下。

它和 build 的职责不同:

  • didChangeDependencies 适合重新计算依赖外部环境的对象;
  • build 负责根据当前状态生成 Widget。

例如,某个 State 根据本地化配置创建格式化器:

class PriceText extends StatefulWidget {
  final double value;

  const PriceText({
    super.key,
    required this.value,
  });

  @override
  State<PriceText> createState() => _PriceTextState();
}

class _PriceTextState extends State<PriceText> {
  NumberFormat? formatter;

  @override
  void didChangeDependencies() {
    super.didChangeDependencies();

    final locale = Localizations.localeOf(context).toLanguageTag();
    formatter = NumberFormat.currency(locale: locale);
  }

  @override
  Widget build(BuildContext context) {
    return Text(formatter!.format(widget.value));
  }
}

这里的关键是:本地化环境变化时,State 不仅要重建,还可能需要重新创建与 locale 相关的对象。


六、完整可运行示例:用 InheritedWidget 传递计数状态

下面的示例包含:

  • StatefulWidget 保存可变状态;
  • InheritedWidget 向下提供状态;
  • 后代通过 of 注册依赖;
  • updateShouldNotify 控制通知;
  • 按钮通过祖先提供的回调修改状态。

将代码放入新建 Flutter 工程的 lib/main.dart,即可运行:

import 'package:flutter/material.dart';

void main() {
  runApp(const MyApp());
}

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

  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      title: 'InheritedWidget Demo',
      theme: ThemeData(
        colorScheme: ColorScheme.fromSeed(seedColor: Colors.blue),
        useMaterial3: true,
      ),
      home: const CounterHost(),
    );
  }
}

/// 保存真正可变状态,并把状态包装成 CounterScope 向下提供。
class CounterHost extends StatefulWidget {
  const CounterHost({super.key});

  @override
  State<CounterHost> createState() => _CounterHostState();
}

class _CounterHostState extends State<CounterHost> {
  int _count = 0;

  void _increment() {
    setState(() {
      _count++;
    });
  }

  @override
  Widget build(BuildContext context) {
    return CounterScope(
      count: _count,
      onIncrement: _increment,
      child: const CounterPage(),
    );
  }
}

/// 不保存可变状态,只保存当前一次构建的不可变数据。
class CounterScope extends InheritedWidget {
  final int count;
  final VoidCallback onIncrement;

  const CounterScope({
    super.key,
    required this.count,
    required this.onIncrement,
    required super.child,
  });

  static CounterScope of(BuildContext context) {
    final result =
        context.dependOnInheritedWidgetOfExactType<CounterScope>();

    if (result == null) {
      throw FlutterError(
        'CounterScope.of() called with a context that does not contain '
        'a CounterScope.',
      );
    }

    return result;
  }

  @override
  bool updateShouldNotify(CounterScope oldWidget) {
    // count 是消费者关心的状态。count 改变时通知依赖者。
    return count != oldWidget.count;
  }
}

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

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: const Text('InheritedWidget'),
      ),
      body: const Center(
        child: CounterText(),
      ),
      floatingActionButton: FloatingActionButton(
        onPressed: () {
          CounterScope.of(context).onIncrement();
        },
        child: const Icon(Icons.add),
      ),
    );
  }
}

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

  @override
  Widget build(BuildContext context) {
    final scope = CounterScope.of(context);

    debugPrint('CounterText.build: ${scope.count}');

    return Text(
      'count = ${scope.count}',
      style: Theme.of(context).textTheme.headlineMedium,
    );
  }
}

一次点击的完整过程

初始构建时:

_CounterHostState._count = 0
        │
        ▼
CounterHost.build
        │
        ▼
CounterScope(count: 0)
        │
        ▼
CounterText.build
        │
        ▼
CounterScope.of(context)
        │
        ▼
CounterText 对 CounterScope 注册依赖

点击按钮后:

onPressed
   │
   ▼
_CounterHostState._increment()
   │
   ▼
setState
   │
   ▼
_count 从 0 变为 1
   │
   ▼
CounterHost.build 返回 CounterScope(count: 1)
   │
   ▼
updateShouldNotify(old: 0, new: 1) == true
   │
   ▼
CounterText 收到依赖变化通知
   │
   ▼
CounterText.build 重新执行

预期现象:

页面文字从 count = 0 变为 count = 1
调试输出包含:
CounterText.build: 1

为什么按钮能调用回调

按钮的 onPressed 闭包在 CounterPage.build 中创建:

onPressed: () {
  CounterScope.of(context).onIncrement();
}

这段代码中的 CounterScope.of(context) 实际发生在用户点击时,而不是 build 时。这个示例中,按钮所在的 CounterPage 本身在初次构建时并没有通过 of 读取 count,所以它不会因为 count 变化而仅凭这次读取自动成为依赖者。

更稳妥、也更容易理解的写法是,在 build 中读取所需的回调:

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

  @override
  Widget build(BuildContext context) {
    final onIncrement = CounterScope.of(context).onIncrement;

    return FloatingActionButton(
      onPressed: onIncrement,
      child: const Icon(Icons.add),
    );
  }
}

但这样 IncrementButton 会注册对整个 CounterScope 的依赖,count 变化时也会重建。若只想读取一次而不订阅更新,可以使用非依赖式查找,但必须明确承担数据可能过期的风险。


七、依赖式查找和非依赖式查找

7.1 dependOnInheritedWidgetOfExactType

final scope =
    context.dependOnInheritedWidgetOfExactType<CounterScope>();

它的语义是:

  1. 查找最近的、类型匹配的祖先;
  2. 注册当前 Element 对该祖先的依赖;
  3. 祖先后续通知时,当前 Element 重新处理依赖变化。

这是消费者需要响应变化时的正确 API。

7.2 getInheritedWidgetOfExactType

final scope =
    context.getInheritedWidgetOfExactType<CounterScope>();

它只进行查找,不注册依赖。

适用于明确不需要监听后续更新的场景,例如一次性读取某项不会变化的配置。但它不应该被误用为性能优化手段:

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

  @override
  Widget build(BuildContext context) {
    final scope =
        context.getInheritedWidgetOfExactType<CounterScope>();

    return Text('${scope?.count}');
  }
}

如果 count 后续变化,WrongCounterText 不会因为 CounterScope 的通知而自动重建。页面可能继续显示旧值,除非它由于其他原因恰好重新构建。

7.3 “of” 方法通常意味着订阅

很多 Flutter API 采用这种约定:

Theme.of(context)
MediaQuery.of(context)
Localizations.of(context, ...)

这类方法通常会通过依赖式查找建立依赖关系。但不能仅凭方法名推断所有第三方 API 的行为,具体仍应查看其实现和 API 文档。


八、InheritedWidget 不保存状态,也不会自动修改数据

InheritedWidget 的字段应当视为当前配置的快照:

class CounterScope extends InheritedWidget {
  final int count;

  const CounterScope({
    super.key,
    required this.count,
    required super.child,
  });

  @override
  bool updateShouldNotify(CounterScope oldWidget) {
    return count != oldWidget.count;
  }
}

下面这种写法不符合常见设计:

class BadScope extends InheritedWidget {
  int count; // 可变字段

  BadScope({
    super.key,
    required this.count,
    required super.child,
  });

  @override
  bool updateShouldNotify(BadScope oldWidget) {
    return count != oldWidget.count;
  }
}

即使语法上可以设计出类似结构,也会破坏 Widget 不可变配置的基本模型。正确的职责划分是:

State / ChangeNotifier / 其他模型
        │
        │ 产生新状态
        ▼
InheritedWidget
        │
        │ 向下暴露当前快照和操作入口
        ▼
后代消费者

InheritedWidget 是传播机制,不是状态存储器。它本身没有 setState,也不会因为某个字段被修改就自动通知后代。


九、updateShouldNotify 的正确性与常见反例

9.1 返回 false 导致界面过期

@override
bool updateShouldNotify(CounterScope oldWidget) {
  return false;
}

即使 count 从 0 变成 1,依赖者也不会收到通知。典型表现是:

  • State 中的值已经变化;
  • 祖先的 build 可能已经执行;
  • 依赖者仍显示旧数据;
  • 依赖者没有执行预期的 didChangeDependencies

这不是 Flutter 丢失更新,而是 updateShouldNotify 明确告诉框架“不需要通知”。

9.2 返回 true 导致无意义重建

@override
bool updateShouldNotify(CounterScope oldWidget) {
  return true;
}

这保证依赖者不会错过通知,但每次 CounterScope 收到新配置都会让所有依赖者重新处理,即使相关数据没有变化。

如果祖先频繁重建,依赖者数量又很多,可能增加构建成本。这个返回值应体现数据语义,而不是无条件返回 true

9.3 可变对象原地修改导致比较失效

下面的模型有一个常见问题:

class UserProfile {
  String name;

  UserProfile(this.name);
}

class UserScope extends InheritedWidget {
  final UserProfile profile;

  const UserScope({
    super.key,
    required this.profile,
    required super.child,
  });

  @override
  bool updateShouldNotify(UserScope oldWidget) {
    return profile != oldWidget.profile;
  }
}

如果外部代码原地修改:

profile.name = 'Alice';

然后继续使用同一个 profile 实例构造新 UserScope,比较结果可能是:

oldWidget.profile == newWidget.profile

因为两者指向同一个对象。此时 updateShouldNotify 返回 false,消费者不会被通知,即使对象内部字段已经改变。

更安全的方式是使用不可变对象并替换实例:

class UserProfile {
  final String name;

  const UserProfile(this.name);

  UserProfile copyWith({String? name}) {
    return UserProfile(name ?? this.name);
  }
}

更新时:

setState(() {
  profile = profile.copyWith(name: 'Alice');
});

这使新旧状态具有清晰的快照边界,也使引用或字段比较具有可预测性。

9.4 回调字段的比较问题

假设 Scope 暴露回调:

class ActionScope extends InheritedWidget {
  final VoidCallback onSubmit;

  const ActionScope({
    super.key,
    required this.onSubmit,
    required super.child,
  });

  @override
  bool updateShouldNotify(ActionScope oldWidget) {
    return onSubmit != oldWidget.onSubmit;
  }
}

如果祖先每次 build 都创建新的闭包:

ActionScope(
  onSubmit: () {
    submit();
  },
  child: child,
)

即使业务逻辑没有变化,函数对象也可能是新的,从而导致比较结果为 true

处理方式取决于语义:

  • 如果回调变化本身需要被消费者感知,应比较它;
  • 如果回调只是内部实现细节,可以让回调保持稳定,或不把它作为需要通知的字段;
  • 不应为了压制重建而随意忽略一个实际上会变化的回调。

十、生命周期中的依赖查找限制

10.1 不要在 initState 中建立 InheritedWidget 依赖

不推荐:

@override
void initState() {
  super.initState();

  final scope = CounterScope.of(context);
}

原因不是此时一定无法找到祖先,而是 initState 只调用一次。如果依赖的祖先后来变化,框架无法把这次初始化代码重新执行一遍。

应该使用:

@override
void didChangeDependencies() {
  super.didChangeDependencies();

  final scope = CounterScope.of(context);
}

didChangeDependencies 会在初始化后调用,也会在已注册的依赖发生变化时再次调用。

10.2 dispose 中不要依赖祖先查找

不推荐:

@override
void dispose() {
  final scope = CounterScope.of(context);
  // ...
  super.dispose();
}

dispose 阶段的 State 正在从树中移除,不应再尝试建立或依赖树中的祖先关系。需要释放的资源应当保存为 State 字段,并在生命周期适当阶段清理:

class ExampleState extends State<Example> {
  StreamSubscription<int>? subscription;

  @override
  void didChangeDependencies() {
    super.didChangeDependencies();

    // 根据当前依赖建立 subscription。
  }

  @override
  void dispose() {
    subscription?.cancel();
    super.dispose();
  }
}

如果依赖对象本身可能变化,还需要在重新订阅前取消旧订阅,避免重复监听。

10.3 异步回调不是依赖注册机制

下面的代码不能因为在回调里读取了 Scope,就自动建立合理的响应关系:

onPressed: () async {
  await loadData();
  final value = CounterScope.of(context).count;
}

依赖关系的建立应放在 Widget 构建或 didChangeDependencies 阶段。异步回调中使用 context 时,还必须考虑 Widget 是否已经卸载:

onPressed: () async {
  await loadData();

  if (!context.mounted) {
    return;
  }

  final value = CounterScope.of(context).count;
  debugPrint('$value');
}

这里的 mounted 只解决异步完成后的生命周期安全问题,不会替代 dependOnInheritedWidgetOfExactType 的依赖注册。


十一、InheritedElement 如何管理依赖者

从框架行为上看,InheritedElement 至少需要维护以下信息:

InheritedElement
├── 当前 InheritedWidget
├── 依赖它的 Element 集合
└── 可选的依赖条件,例如 aspect

依赖者调用依赖式查找时,当前 Element 被加入依赖集合。InheritedWidget 更新时:

  1. InheritedElement 收到新的 Widget;
  2. 调用新 Widget 的 updateShouldNotify(oldWidget)
  3. 如果返回 true,遍历相关依赖者;
  4. 通知依赖者依赖发生变化;
  5. 依赖者被标记为需要重新构建;
  6. 后续构建阶段重新执行 didChangeDependenciesbuild

这也解释了为什么普通后代不会全部重建。

例如:

CounterScope
├── CounterText      // 调用了 CounterScope.of,注册依赖
└── StaticLogo       // 没有调用 CounterScope.of

count 变化时:

CounterText  :收到通知并重建
StaticLogo   :不会因为 CounterScope 通知而重建

StaticLogo 如果同时因为父级传入的新 Widget、布局变化或其他状态变化而需要重建,仍然可能执行自己的 build。因此,“没有依赖”只表示不会因该 InheritedWidget 的通知而重建,不表示它永远不会重建。


十二、InheritedWidget、InheritedNotifier 和 InheritedModel

12.1 InheritedWidget:配置变化时通知

InheritedWidget 通常由祖先在 build 中创建新实例:

CounterScope(
  count: count,
  child: child,
)

当实例更新时,通过 updateShouldNotify 决定是否通知。

它适合表达:

  • 当前配置;
  • 当前状态快照;
  • 祖先提供的服务入口;
  • 与 Widget 树生命周期绑定的数据。

12.2 InheritedNotifier:把 Listenable 的通知接入 Widget 树

当状态对象实现 Listenable,可以使用 InheritedNotifier

class CounterModel extends ChangeNotifier {
  int _count = 0;

  int get count => _count;

  void increment() {
    _count++;
    notifyListeners();
  }
}

class CounterModelScope extends InheritedNotifier<CounterModel> {
  const CounterModelScope({
    super.key,
    required CounterModel model,
    required super.child,
  }) : super(notifier: model);

  static CounterModel of(BuildContext context) {
    final result =
        context.dependOnInheritedWidgetOfExactType<CounterModelScope>();

    if (result == null || result.notifier == null) {
      throw FlutterError('CounterModelScope not found.');
    }

    return result.notifier!;
  }
}

它的职责划分是:

CounterModel.notifyListeners()
        │
        ▼
InheritedNotifier 监听 Listenable
        │
        ▼
通知依赖它的后代

InheritedNotifier 的构造参数 notifier 是一个 Listenable?。当 notifier 发出通知时,依赖者会被标记更新。框架可以在一次构建周期中合并多次通知,因此消费者不应把每一次通知都理解为对应一次独立的 build 调用。

ChangeNotifier 的通知通常发生在 Dart UI isolate 中,监听回调的调用与线程模型有关;Flutter Widget 构建不会因为 InheritedNotifier 而自动变成多线程并发。耗时的异步或计算任务仍需单独设计。

12.3 InheritedModel:按 aspect 选择性通知

当一个 Scope 包含多个彼此独立的字段时,普通 InheritedWidget 的依赖粒度是整个 Scope。InheritedModel 可以让消费者按 aspect 注册:

final value = InheritedModel.inheritFrom<AppModel>(
  context,
  aspect: AppAspect.theme,
);

祖先更新时,根据 aspect 判断哪些消费者需要通知。

这适用于:

一个共享模型
├── theme
├── locale
└── unreadCount

如果某个消费者只依赖 themeunreadCount 变化时可以避免通知它。

InheritedModel 增加了依赖声明和通知条件的复杂度。只有当一个 Scope 确实包含多个可独立更新的维度时,这种粒度控制才有明显价值。


十三、InheritedWidget 作为状态框架基础

一个最小状态框架通常可以拆成四层:

状态持有者
    │
    │ setState / notifyListeners / 异步结果
    ▼
状态快照或模型
    │
    ▼
InheritedWidget / InheritedNotifier
    │
    │ 依赖注册与更新通知
    ▼
Widget 消费者

13.1 状态产生

状态可能由以下对象产生:

  • State
  • ChangeNotifier
  • ValueNotifier
  • 自定义模型;
  • Repository 或异步数据层。

InheritedWidget 不规定状态必须放在哪里。

13.2 状态暴露

Scope 通常暴露两类内容:

class SessionScope extends InheritedWidget {
  final User? user;
  final VoidCallback signOut;

  const SessionScope({
    super.key,
    required this.user,
    required this.signOut,
    required super.child,
  });

  @override
  bool updateShouldNotify(SessionScope oldWidget) {
    return user != oldWidget.user;
  }
}
  • 数据:例如 user
  • 操作:例如 signOut

数据通常参与 updateShouldNotify。操作是否参与通知,则取决于回调是否会替换,以及消费者是否需要获得新的回调实现。

13.3 状态消费

消费者声明依赖:

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

  @override
  Widget build(BuildContext context) {
    final session =
        context.dependOnInheritedWidgetOfExactType<SessionScope>();

    return Text(session?.user?.name ?? '未登录');
  }
}

这种读取不是一次普通属性访问,而是建立了:

UserName Element → SessionScope InheritedElement

这条依赖边是自动更新的基础。

13.4 状态框架提供的核心能力

基于 InheritedWidget 的框架通常需要额外处理:

  • 状态对象的创建和销毁;
  • 依赖对象变化时的重新订阅;
  • 选择性读取;
  • 缺少 Provider 时的错误信息;
  • 异步任务和卸载状态;
  • 测试替换和作用域隔离;
  • 状态持久化或恢复。

因此,InheritedWidget 是机制基础,不等于完整的状态管理方案。实际框架往往在它之上提供更好的 API、生命周期管理和调试能力。


十四、作用域、最近祖先和多个实例

查找是沿当前 Element 的祖先方向进行的,因此最近的匹配 Scope 会覆盖更远的 Scope:

ThemeScope(light)
└── Page
    └── ThemeScope(dark)
        └── Button

Button 调用:

ThemeScope.of(context)

得到的是内部的 ThemeScope(dark),而不是外部的 ThemeScope(light)

这使得 InheritedWidget 天然适合作用域设计:

  • 应用级配置放在根部;
  • 页面级配置放在页面子树;
  • 对话框或局部区域可以覆盖某项配置;
  • 测试中可以在局部树注入替代实现。

但这也带来一个诊断要点:如果查到的数据不符合预期,应检查 BuildContext 所在位置以及中间是否存在同类型的覆盖 Scope。


十五、BuildContext 使用错误与诊断

15.1 在错误位置查找祖先

典型场景是 MaterialApp 尚未成为当前 Context 的祖先:

class MyApp extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return Scaffold(
      body: Text(Theme.of(context).textTheme.bodyLarge.toString()),
    );
  }
}

如果这个 context 属于 MaterialApp 外部的 Element,就不能期望通过它找到 MaterialApp 内部提供的主题。

可以插入一个新的 Builder,让查询发生在正确的子树中:

return MaterialApp(
  home: Builder(
    builder: (context) {
      final theme = Theme.of(context);

      return Scaffold(
        body: Text(
          '${theme.textTheme.bodyLarge}',
        ),
      );
    },
  ),
);

原则不是“多使用 Builder”,而是确认查找 Context 的位置确实位于目标 InheritedWidget 之下。

15.2 缺少祖先时的失败表现

如果调用:

CounterScope.of(context)

但当前树中没有 CounterScope,封装方法可以抛出清晰的 FlutterError

CounterScope.of() called with a context that does not contain a CounterScope.

相比直接使用空断言:

context.dependOnInheritedWidgetOfExactType<CounterScope>()!

自定义错误更容易定位组件树和使用位置。

15.3 在事件回调里使用已失效的 Context

异步回调可能在页面退出后才完成:

onPressed: () async {
  await Future<void>.delayed(const Duration(seconds: 1));

  if (!context.mounted) {
    return;
  }

  Navigator.of(context).pop();
}

context.mounted 用于检查 Context 对应的 Element 是否仍在树中。它不能解决所有异步状态问题,但能避免在卸载后继续访问导航、主题或 Scope。


十六、重建边界与性能取舍

16.1 依赖关系决定通知范围

假设:

AppScope
├── Header       // 依赖 user
├── Content      // 依赖 items
└── Footer       // 不依赖 AppScope

如果使用一个普通 AppScope,它的 updateShouldNotify 返回 true 时,所有注册依赖的后代都会收到通知,即使某个消费者只使用其中一个字段。

因此,常见的拆分方式是:

UserScope
└── ItemsScope
    └── 页面内容

拆分后,用户状态变化不必通知只依赖列表数据的消费者。

这不是绝对规则。Scope 过度拆分会增加组件层级、依赖管理和理解成本。应根据状态的更新频率、消费者数量和字段独立性决定边界。

16.2 const 不能绕过已注册依赖

const 可以减少 Widget 配置对象的创建和无意义更新,但如果某个消费者已经注册了对 Scope 的依赖,Scope 返回 true 时,消费者仍然需要处理通知。

const 优化的是 Widget 配置复用,不是取消依赖关系。

16.3 build 被调用不等于一定发生了昂贵绘制

依赖者重新执行 build 只表示 Widget 配置重新计算。Flutter 后续还会根据 Widget、Element 和 RenderObject 的变化决定布局、绘制等工作。

但这不意味着可以无视重建成本。大量依赖者、复杂构建逻辑或频繁通知仍可能增加帧构建压力。


十七、平台差异

InheritedWidget 属于 Flutter Widget 树机制,Android、iOS、Windows、macOS、Linux 和 Web 的依赖注册、updateShouldNotify 判断及生命周期语义基本一致。

平台差异主要出现在状态来源和外部边界:

  • Android、iOS 可能涉及原生生命周期、权限和平台通道;
  • 桌面端可能涉及窗口、键盘和多窗口能力;
  • Web 可能涉及浏览器路由、刷新和页面生命周期;
  • 平台视图、原生页面或独立 Flutter Engine 不会自动共享同一个 Widget 树中的 InheritedWidget

因此,一个 InheritedWidget 只能向同一个 Flutter Widget 树中的后代传播数据。跨 Flutter 根节点、跨 Engine 或跨原生页面时,需要通过显式模型、平台通道、进程通信或其他共享机制传递状态。


十八、如何验证依赖是否正确建立

可以使用日志验证依赖链路:

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

  @override
  Widget build(BuildContext context) {
    debugPrint('LoggingCounterText.build');

    final scope = CounterScope.of(context);

    return Text('${scope.count}');
  }
}

然后观察不同情形:

操作 CounterScopeupdateShouldNotify 依赖者是否因 Scope 通知而重建
count 从 0 改为 1 true
count 未变化 false
消费者使用 getInheritedWidgetOfExactType 不适用
消费者未查找该 Scope 不适用
祖先因其他原因重建消费者 不适用 可能重建

诊断时应按顺序检查:

  1. 消费者是否真的调用了 dependOnInheritedWidgetOfExactType
  2. 调用时的 BuildContext 是否在目标 Scope 的子树中;
  3. 祖先是否确实返回了新的 Scope 配置;
  4. updateShouldNotify 是否正确比较了相关字段;
  5. 被比较的模型是否被原地修改;
  6. 消费者是否可能因为其他原因重建,从而造成“看起来像自动更新”的误判。

十九、核心边界

InheritedWidget 的机制可以概括为:

查找祖先
  + 注册依赖
  + 比较新旧配置
  + 通知已注册依赖者
  = Widget 树中的向下状态传播

其中每一步都不可替代:

  • 只有查找,没有依赖注册:后续不会自动更新;
  • 只有依赖注册,没有正确比较:可能漏更新或过度更新;
  • 只有通知,没有状态持有者:没有可靠的新数据来源;
  • 只有状态持有者,没有 Scope:后代需要通过参数或其他方式获取状态。

所以,InheritedWidget 既不是全局变量,也不是单独完整的状态管理框架。它提供的是 Flutter Widget 树中一套明确的依赖图和失效通知机制:消费者在构建时声明依赖,祖先在配置更新时决定通知,框架再让相关消费者重新处理依赖并构建。这套机制正是许多 Flutter 状态框架能够工作的基础。


系列导航与关联阅读

官方资料

本文依据 Flutter 与 Dart 官方文档重新梳理;正文与示例由 WR BLOG 编写。