没有完美的结构设计,只有阶段性的最优解
架构焦虑
在搭建 Flutter 项目整体基础组件结构的时候,想着引入一套完美框架用于满足整个项目数据流转、状态保存、跨界面状态传递,觉得选型很重要会导致整体未来两三年内的走向;但真正实际落地发现,框架只是能力提供并不是万能牌,盲目引入只会带来额外问题;
框架只是能力提供者,并不是万能解法。真正需要解决的,是随着项目复杂度增长而出现的“状态传播问题”。
一、状态驱动机制
通过可观察的数据变化触发 rebuild 进而通过 Widget diff 驱动界面更新的过程;
框架调用 build 生成新的 Widget 配置,Element 树对比并复用已有节点,
最终由 RenderObject 完成布局与绘制。Widget 自身不可变,持久性与复用发生在 Element
与 RenderObject 层,这使得`频繁 rebuild`成为可能,让“状态如何传播”成为真正的关键。
flowchart LR
S[State变化\nsetState / notify / emit]
BO[BuildOwner\n调度rebuild]
E[Element\n持有Widget\ndirty + rebuild]
W[Widget\n不可变配置\nbuild]
K[Diff规则\nruntimeType + key]
RO[RenderObject\nlayout + paint]
PO[PipelineOwner\nflushLayout/flushPaint]
ENG[Engine\nSkia / Impeller]
S --> BO
BO -->|markNeedsBuild| E
E -->|rebuild| W
W -->|build生成子Widget| E
E -->|diff依据| K
E -->|create/update| RO
RO -->|markNeedsLayout/Paint| PO
PO -->|flush| RO
RO -->|draw commands| ENG
为什么做状态管理
其实刚开始接触 Flutter 技术栈时候就有过类似疑问,如果不做任何三方状态管理框架引入,而是通过借助 Flutter 本身框架特性使用父子传递结合 InheritedWidget 是否可满足使用,又有可能遇到哪些问题;
结合实际场景看对于刚刚起步的项目或者单独模块、基础公共组件来看这样做完全没问题;只是如果对于一些复杂跨页面场景将会因为传参链路、颗粒度维护等相关问题带来灾难性问题;
-
传参链路 ——对于层级嵌套的组件,只能通过层次传递参数进行刷新控制,以至于面临多个参数或者层级过深时后续的新增与修改等维护动作面临困难;
-
rebuild 范围大——InheritedWidget 通过字段/模块进行刷新状态传递,不断根据对象/字段进行拆分会带来结构复杂问题,融合字段监听又容易导致每次重绘范围过大,两者陷入两难;
-
耦合,性能——为解决刷新颗粒度问题将会出现间接的刷新传递或者整个视图的高频刷新,因为整体没什么严格意义的分层、拆分概念,所有的界面都会被迫耦合(代码和刷新状态),导致整个项目功能耦合严重以及面临性能问题;
二、局部状态管理
通常来讲,状态分为局部状态(ephemeral state) 与应用状态 (app state),顾名思义局部状态代指的是影响范围有限生命周期相对较短不需要持久化的 widget —— 比如一次按钮点击响应,路由页签选中态切换;而应用状态则体现在业务,被多页面共享状态甚至跨视图跨界面刷新的场景,比如购物车,登录状态等;
边界不是绝对,他将随着业务及团队协作变化
2.1 setState 状态管理
setState 适合局部状态和单组件内的交互逻辑。它让状态更新与 UI 重建绑定在一个明确的位置,读写路径清晰,成本低。
示例:计数器
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 Column(
children: [
Text('count: $count'),
ElevatedButton(
onPressed: () => setState(() => count++),
child: const Text('加一'),
),
],
);
}
}
setState 本身非常适用于页面局部交互、短生命周期的场景,或者公共基础小组件的搭建可以使用它快速完成并且易于后续维护;但是一旦遇到跨界面传递,或者全局状态数据传递那么就捉襟见肘;
flowchart LR
subgraph setState
A[用户点击] --> B[setState]
B --> C[修改本组件状态]
C --> D[当前 Widget rebuild]
end
2.2 ValueNotify+InheritedNotifier 轻量共享
属于 InheritedWidget的引申使用,专门用于处理定义的简易生产消费模型,通过 Listenable进行 notifier 消费响应,并且合并通知减少 rebuild调用;
示例:用 ValueNotifier 做局部共享
final counter = ValueNotifier<int>(0);
class CounterText extends StatelessWidget {
const CounterText({super.key});
@override
Widget build(BuildContext context) {
return ValueListenableBuilder<int>(
valueListenable: counter,
builder: (_, value, __) => Text('count: $value'),
);
}
}
ValueNotify 适用于小范围刷新频繁逻辑精简的组件使用,也可以通过包装自定义InheritedNotifier实现在子树范围共享传递使用,避免了层层传递痛点;
ValueNotifier 可以看作 Flutter 中最简单的响应式状态模型:
State → Notifier → Listener → rebuild
flowchart LR
subgraph ValueNotifier
E[修改 notifier.value] --> F[ValueNotifier 通知]
F --> G[ValueListenableBuilder 监听]
G --> H[监听组件 rebuild]
end
当状态只在局部组件使用时,setState 或 ValueNotifier 已经足够。但当状态开始跨组件甚至跨页面共享时, 就需要更系统化的状态管理方式;
三、轻量管理与框架管理的结构对比
在项目初期(系统规模较小时),简单的状态管理方式往往已经足够;
但随着状态开始跨页面传播, 依赖关系逐渐复杂, 开发者就需要一种更稳定的结构来组织状态,引入 InheritedWidget或 InhertedNotifier,引入状态管理框架;
有哪些状态管理框架可选
目前社区中常见的方案包括:Provider、Riverpod、Bloc、Redux、GetX 等。
本文不会逐一介绍所有框架,
而是选择两个具有代表性的方案进行说明:
- Provider —— 最接近 Flutter 原生机制的状态管理封装
- Riverpod —— 新一代 Provider 体系,更强调依赖关系管理
通过这两个框架,可以大致理解 Flutter 状态管理的两种思路。
3.1 Provider
最接近原生的结构化封装,本事是基于 InheritedWidget 机制提供 context.watch/select、Consumer/Selector 用于控制 rebuild 颗粒度以及减少样板代码;
class CounterModel extends ChangeNotifier {
int count = 0;
void inc() {
count++;
notifyListeners();
}
}
class CounterView extends StatelessWidget {
const CounterView({super.key});
@override
Widget build(BuildContext context) {
final count = context.select((CounterModel m) => m.count);
return Column(
children: [
Text('count: $count'),
ElevatedButton(
onPressed: () => context.read<CounterModel>().inc(),
child: const Text('加一'),
),
],
);
}
}
本质上是对 InheritedWidget 的结构化封装,通过 ChangeNotifier 提供状态变更能力,
并通过 context.watch/select 控制 rebuild 颗粒度。
Provider:ChangeNotifier + select/read
flowchart LR
subgraph Provider
A[用户点击按钮] --> B[context.read.inc]
B --> C[ChangeNotifier 修改 count]
C --> D[notifyListeners]
D --> E[Provider 触发依赖更新]
E --> F[context.select 订阅者重建\n只重建依赖 count 的 Widget]
end
3.2 Riverpod
@riverpod
Future<String> configs(Ref ref) async {
// 模拟异步
await Future.delayed(const Duration(milliseconds: 300));
return 'ok';
}
class ConfigView extends ConsumerWidget {
const ConfigView({super.key});
@override
Widget build(BuildContext context, WidgetRef ref) {
final result = ref.watch(configsProvider);
return switch (result) {
AsyncData(:final value) => Text('data: $value'),
AsyncError() => const Text('error'),
_ => const CircularProgressIndicator(),
};
}
}
Riverpod:Provider/Ref + watch + AsyncValue
flowchart LR
subgraph Riverpod
G[build 时 ref.watch configsProvider ] --> H[Provider 执行/缓存\n根据依赖自动失效]
H --> I[返回 AsyncValue\nloading/data/error]
I --> J[ConsumerWidget 根据状态渲染\nloading/data/error 分支]
K[依赖变化/手动 refresh] --> H
end
Riverpod 的 Provider 并不依赖 Widget Tree, 而是通过 Provider Graph 来管理依赖关系。
3.3 章节小结
| 对比维度 | Provider | Riverpod |
|---|---|---|
| 依赖结构 | 基于 Widget Tree | 基于 Provider Graph |
| 状态声明 | ChangeNotifier / Provider | Provider / Ref |
| 依赖管理 | 依赖 BuildContext | 不依赖 BuildContext |
| 使用复杂度 | 简单易上手 | 更灵活但概念更多 |
| 适用场景 | 中小型项目 | 状态依赖复杂的大型项目 |
状态管理的核心问题并不是“如何更新 UI”,而是“如何组织状态的传播路径”。
自定义管理可以轻装简行,但同时带来的问题就是高度依赖开发者设计能力,需要考虑的事包括但不限于
- 状态归属 —— 字段属性生产者消费者状态维护
- 变更通道 —— 通过什么方式更新
- 刷新边界 —— 改动的影响范围
框架管理更倾向于边界制定与模式化工具使用,因此在团队协作是不可获取的存在;但对于一些不需要协同开发/单模块使用功能,盲目引入框架只会增加维护成本以及带来性能风险;
总结
整体状态管理策略
| 方案 | 适用场景 |
|---|---|
| setState | 单组件 |
| ValueNotifier | 小范围共享 |
| InheritedWidget | 子树共享 |
| Provider | 中型项目 |
| Riverpod | 复杂依赖 |
不同方案并不存在绝对优劣,它们只是适用于不同复杂度阶段。
当系统规模较小时,简单方案反而更高效。 当系统复杂度增加,状态管理框架可以帮助建立更清晰的边界。
没有完美的结构设计,只有阶段性的最优解。