Flutter 状态管理选型: Provider / Riverpod / Bloc / GetX 怎么选
Flutter 状态管理这个话题, 社区吵了好几年也没定论。多数讨论有两个毛病: 只聊 API 哪个优雅, 不聊项目跑半年后好不好维护; 只聊自己喜欢什么, 不聊团队、业务复杂度、生态稳定性这些现实问题。这篇不比”哪个写起来爽”, 比的是”哪个能在真实项目里活下来”。
一、先把问题掰直: 状态管理到底在管什么
很多人一上来就对比 setState / notifyListeners / ref.watch / emit / Obx——这是表象。状态管理真正要解决的, 是这五件事:
- 状态放在哪里
- 状态变化后, 谁来通知 UI
- 异步请求、空状态、错误状态怎么建模
- 页面越来越复杂时, 代码会不会失控
- 团队里第二个人接手, 还看不看得懂
大家都能写出一个能跑的页面。真正拉开差距的, 是当业务变成”列表 + 刷新 + 请求失败 + 搜索 + 筛选 + 排序 + 弹 Toast/Dialog/跳详情”这种组合时, 这套结构还能不能顶住。
状态管理的核心, 不是”能不能更新 UI”, 而是”当 UI 和业务越来越复杂时, 这套结构还能不能稳”。这是选型的真正维度。
二、Provider: Flutter 状态管理里的”老实人”
基于 InheritedWidget 封装, Google 官方推荐, 概念最直白: 状态在 ViewModel 里, 用 notifyListeners 通知, Consumer 监听。
class CounterViewModel extends ChangeNotifier {
int _count = 0;
int get count => _count;
void increment() {
_count++;
notifyListeners();
}
}
// 注入
ChangeNotifierProvider(
create: (_) => CounterViewModel(),
child: Consumer<CounterViewModel>(
builder: (_, vm, __) => Text('${vm.count}'),
),
);
优点: 官方背书, 生态成熟, 文档丰富, 新人上手快, 面试常问。
代价: 依赖 BuildContext, 异步方法里拿不到 context 是经典痛点; 样板代码多, ChangeNotifier + Consumer 写到手抽; 多个相互依赖的状态会缠在一起。
适合: 中小项目、入门、单人快速起步。
三、Riverpod: Provider 的究极进化
Provider 作者 Remi 自己重写的方案, 解决了 Provider 的几个硬伤: 无 context 限制、编译期类型安全、不依赖 Flutter 树。
final counterProvider = NotifierProvider<CounterNotifier, int>(CounterNotifier.new);
class CounterNotifier extends Notifier<int> {
@override
int build() => 0;
void increment() => state++;
}
// 任意位置读写, 不需要 context
class CounterView extends ConsumerWidget {
@override
Widget build(BuildContext context, WidgetRef ref) {
final count = ref.watch(counterProvider);
return Text('$count');
}
}
优点: 无 context, 能在任意位置读写; 编译期类型安全, 错误提前暴露; 支持代码生成 (riverpod_generator), 减少样板; 可测试性极强。
代价: 概念多 (Provider/Family/Notifier/AsyncNotifier…), 初学会被一堆命名搞晕; 心智成本比 Provider 高一档。
适合: 中大型新项目、需要高可测试性、强调类型安全。这是我目前最愿意推荐的平衡方案。
四、Bloc / Cubit: 企业级钢铁堡垒
强制单向数据流, Event → Bloc → State, UI 和逻辑完全解耦。闲鱼、宝马等大厂长期使用。
// 事件
sealed class CounterEvent {}
class Increment extends CounterEvent {}
// 状态
class CounterState {
final int count;
const CounterState(this.count);
}
// Bloc
class CounterBloc extends Bloc<CounterEvent, CounterState> {
CounterBloc() : super(const CounterState(0)) {
on<Increment>((event, emit) => emit(CounterState(state.count + 1)));
}
}
// UI
BlocBuilder<CounterBloc, CounterState>(
builder: (_, state) => Text('${state.count}'),
);
Cubit 是 Bloc 的简化版, 省掉 Event, 直接调方法 emit 新状态, 适合不需要事件溯源的场景。
优点: 架构最规范, UI 与逻辑完全解耦; 可测试性最强, 配合 bloc_test 能精确断言状态序列; BlocObserver 能全局追踪所有状态流转, 调试利器; 多人协作有标准可循。
代价: 样板代码多, 写个计数器要定义 Event/State/Bloc 三件套; 上手成本高; 简单业务用 Bloc 是杀鸡用牛刀。
适合: 大型团队、多人协作、强调规范和状态流可追踪的中大型项目。
五、GetX: 瑞士军刀, 也是维护黑洞
API 极简, 一把梭: 状态、路由、依赖注入、国际化全打包。
class CounterController extends GetxController {
var count = 0.obs;
void increment() => count++;
}
// 页面
Obx(() => Text('${controller.count.value}'));
优点: 写起来是真的爽, 代码量极少; 不需要 context; 学习成本极低, 新人十分钟起飞。
代价: 全局单例黑魔法, 脱离 Flutter 原生机制; 状态在哪更新全靠自觉, 项目大了是维护黑洞; 测试不友好; 接盘风险极高——GetX 项目交接给别人约等于友尽。
适合: 外包、毕设、个人快速出活。新项目再从 0 选 GetX, 我会明显更谨慎。
六、同一场景的代码量对比
实现同一个计数器, 不含注释:
| 方案 | 行数 | 样板感 | 类型安全 | context 依赖 |
|---|---|---|---|---|
| GetX | ~20 | 极低 | 运行时 | 无 |
| Provider | ~30 | 中 | 运行时 | 有 |
| Riverpod | ~35 | 中低(代码生成后更低) | 编译期 | 无 |
| Bloc | ~50 | 高 | 编译期 | 有 |
代码量小不等于好维护。GetX 写得最快, 维护最痛; Bloc 写得最慢, 接盘最稳。
七、我的选型框架
按项目规模和团队两个维度:
- 小项目 / Demo / 个人:
Provider或setState - 中小型新项目:
Riverpod(平衡方案, 我个人首选) - 大型团队 / 多人协作 / 强规范:
Bloc或Cubit - 老项目已深度绑定 GetX: 先稳住, 别一激动全量重构; 新模块可逐步切 Riverpod
- 新项目从 0 选 GetX: 谨慎, 除非是真的短期外包
八、关于 GetX 删库风波: 生态稳定也是选型维度
GetX 作者账户被风控、仓库一度不可用那波, 把一个以前很多人不愿正视的问题硬生生摊开了: 依赖生态的稳定性, 也是技术选型的一部分。
以前大家觉得”能跑就行”, 那次才意识到: 你今天选一个方案, 不只是选它今天写起来爽不爽, 还在选三个月后好不好改、半年后新人能不能接、一年后团队还能不能稳稳维护、真出事时有没有兜底能力。
选型要看四条: 解决什么问题、边界在哪、为什么有人爱也有人骂、生态是否稳定。只看第一条的人, 选出来的方案多半活不过一年。
九、关注 Signals: 官方实验性响应式
signals 是 Flutter 官方在做的实验性响应式 API, 思路类似 Vue 的 ref 或 SolidJS 的 signal, 无三方依赖, 自动追踪依赖。目前还在实验阶段, 生态和文档都少, 但方向值得跟踪——如果未来稳定, 可能是 Flutter 原生的”现代状态管理”答案。
能在选型时提一句”我在关注 Signals 这个官方实验方向”, 体现你不是只会用现成库, 而是在跟进演进。
小结
没有”最好”的状态管理, 只有”适合”的。选型的真正标准不是 API 优雅度, 而是这套结构在你的项目规模、团队配置、维护周期下能不能稳。Provider 是老实人, Riverpod 是现代平衡方案, Bloc 是企业堡垒, GetX 写着爽但维护痛。先想清楚你的项目和团队, 再选库, 别反过来。