目录
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
一、介绍
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.watch 和 context.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 即可。
行者常至,为者常成!