“组件依赖关系”到底是个啥?#
先说人话:在 Flutter 里,组件(Widget)要么“吃”来自祖先的东西(比如主题、屏幕尺寸、状态),要么被爸爸妈妈(父组件)管着怎么摆放、怎么长大(布局约束)。这些“吃东西”和“被管束”的过程,就是组件的依赖关系。你一旦明白谁依赖谁、怎么建立依赖、依赖变了会发生啥,写出来的界面就不容易“抽风”,性能还更稳。
三棵树,别搞混#
Flutter UI 运行时,背后有三棵树:
- Widget 树:配置树,描述“长啥样”和“想干嘛”。
- Element 树:实例树,Widget 的“落地分身”,负责生命周期与和上下文关系。
- RenderObject 树:渲染树,管理布局、绘制、命中测试。
依赖关系绝大多数时候,都是 Element 层在“登记备案”。比如你在 build
里通过 Theme.of(context)
拿主题,实际上是 Element 在对上游的 InheritedWidget 建立“监听关系”。
依赖的两条主线#
- 数据依赖:通过 InheritedWidget 系列(
Theme.of
、MediaQuery.of
、Provider
、InheritedModel
等)向上找数据,并登记“我在听”。当数据源变了,会触发相关子树重建。 - 布局依赖:父给子“约束”(constraints),子在约束里自我测量,父再决定你站哪儿。这个是“天然依赖”:不登记,也逃不掉。
InheritedWidget:监听是怎么“挂上去”的?#
当你在 build
里调用:
final theme = Theme.of(context);
本质是调用了 context.dependOnInheritedWidgetOfExactType<Theme>()
。这一步会:
- 找到最近的
Theme
(其实是InheritedWidget
的子类)的 Element; - 把“我(这个 Element)在听它”的关系登记起来。
之后只要这个 Theme
的 updateShouldNotify
返回了 true
,所有“在听”的孩子就会被标记重建。
注意还有个“只拿不听”的方式:
context.getElementForInheritedWidgetOfExactType<Theme>();
或第三方状态工具里的 read
,意思是:就取一次,不建立依赖,别给我重建。
生命周期:依赖关系在什么时候能用?#
initState
:还没建立依赖,适合读“只读不听”的东西(Provider.of(context, listen: false)
可以;Theme.of(context)
不建议)。didChangeDependencies
:专门为“依赖变了”设计的回调。第一次插入树里会调用一次;之后只要依赖的 InheritedWidget 更新,也会调用。build
:最常见的建立依赖的地方,of(context)
基本都在这儿干。dispose
:别再访问context
的依赖内容了,已经不安全了。
常见依赖来源和写法对比#
-
Theme.of(context)
、MediaQuery.of(context)
:典型 InheritedWidget。 -
Provider
家族:context.watch<T>()
/Provider.of<T>(context)
默认监听变化;context.read<T>()
只取不听;context.select<T, S>(selector)
精准监听某个字段,减少无谓重建。
-
Riverpod(顺嘴提一句):
ref.watch
监听,ref.read
不监听,原理类似,只是容器和依赖图在它自己那儿。
小例子:手写一个简单依赖#
import 'package:flutter/material.dart';
class CounterProvider extends InheritedWidget { const CounterProvider({ super.key, required this.count, required super.child, });
final int count;
static CounterProvider of(BuildContext context) { final provider = context.dependOnInheritedWidgetOfExactType<CounterProvider>(); assert(provider != null, 'no CounterProvider found in context'); return provider!; }
@override bool updateShouldNotify(CounterProvider oldWidget) => count != oldWidget.count;}
class CounterPage extends StatefulWidget { const CounterPage({super.key});
@override State<CounterPage> createState() => _CounterPageState();}
class _CounterPageState extends State<CounterPage> { int count = 0;
@override Widget build(BuildContext context) { return CounterProvider( count: count, child: Scaffold( appBar: AppBar(title: const Text('依赖关系示例')), body: const Center(child: CountText()), floatingActionButton: FloatingActionButton( onPressed: () => setState(() => count++), child: const Icon(Icons.add), ), ), ); }}
class CountText extends StatelessWidget { const CountText({super.key});
@override Widget build(BuildContext context) { final c = CounterProvider.of(context).count; // 建立依赖 return Text('当前 count: $c'); }}
这段里,CountText
对 CounterProvider
建立了依赖。只要 count
改变,updateShouldNotify
返回 true
,CountText
就会重建。
如果你只想读一次不监听,可以用 context.getElementForInheritedWidgetOfExactType
先拿 Element,再取 widget
,不过一般结合状态管理框架会更顺手。
布局依赖:父子之间的“规矩”#
Flutter 的布局协议是这样的:
- 父给子 constraints(比如“宽度最多 300,高度随意,自便”);
- 子在这个范围里测量自己(
performLayout
); - 父决定你放哪儿(
position
)。
这是一种“隐式依赖”:子必须在父给的规则里活动。你也能看到一些“依赖链”的现象,比如 Expanded/Flexible
依赖 Row/Column
的分配策略,Sliver
系列依赖滚动容器的约束节拍。
哪些地方容易踩坑?#
- 在
initState
用Theme.of(context)
做初始化逻辑:这会建立依赖,但此时 Element 关系可能还不稳定。把这类逻辑放到didChangeDependencies
,或用listen: false
/read
。 - 在父组件的
build
里频繁改动提供者导致全树重建:比如Provider(create: (_) => SomeClass())
放在会反复重建的地方,等于“每次都换老板”,下游全重建。把 Provider 提升到更稳的上层。 - 滥用
GlobalKey
:它是“跨树强行绑定”的依赖,代价大,能不用就不用。 context
越级找祖先失败:Builder
或把子树拆分出来,保证context
的位置在正确的祖先之下。
性能小妙招:把“听众”做小#
- 拆小组件:把真正需要数据的那一小块抽出来监听,其它部分用
const
、不监听。 - 精准监听:
context.select
/Selector
只订阅你关心的字段。 - 不要过度缓存:依赖会变的东西(比如
Theme
),不要死缓存,交给框架自动重建最安全。 - 合理利用
const
构造:与依赖无关的纯展示,const
能省重建成本。
生命周期再捋一遍#
- 有上下文依赖的初始化 → 放
didChangeDependencies
(会在依赖变更时再次调用)。 - 纯一次性不监听的读取 →
initState
里read
/listen: false
。 - 响应式 UI → 放
build
里,顺其自然地建立依赖。 - 清理资源 →
dispose
,别再用context
去找依赖。
心智模型:记住这三句话#
- 我在
build
里用of(context)
,就是在“报名当听众”。 - 数据源说“我变了”(
updateShouldNotify == true
),所有听众重建。 - 父子布局“天生就有依赖”,父管范围,子报尺寸,父来摆位。
一个更接地气的 Provider 对照#
// 监听:数据变动时重建final user = context.watch<User>();
// 只读一次:初始化拉接口用final repo = context.read<UserRepository>();
// 精准监听:只关心 name 字段变不变final name = context.select<User, String>((u) => u.name);
这三种,决定了“有没有建立依赖”、“依赖的粒度有多细”。听谁、听哪部分、什么时候听,完全由你拿捏。
收尾:写 Flutter,别跟依赖“怄气”#
组件依赖关系并不玄学:
- 数据依赖交给 InheritedWidget/Provider 家族;
- 布局依赖交给 constraints 协议;
- 生命周期里选对地儿建立/更新依赖;
- 优化时,让“听众”更小、更准。
把这些搞顺了,UI 就像上了油的拉面,筋道还不粘锅。等你下次看到一大片莫名其妙的重建,不妨先问一句:我到底在听谁?我是不是听多了?听对了没?
——写到这,我已经迫不及待把几个“爱瞎听”的组件拆小了。你也试试,准保见效。