文章

Flutter 状态管理横评:Provider / Riverpod / Bloc / GetX

Flutter 状态管理横评:Provider / Riverpod / Bloc / GetX

“Flutter 状态管理到底该用哪个”大概是社区里最经久不衰的争论。原因也不难理解:Flutter 官方只给了 setStateInheritedWidget 两块地基,上层方案百花齐放。本文横向对比四个最主流的方案——Provider、Riverpod、Bloc、GetX,用同一个例子把它们的写法、心智模型、优缺点摆在一起,最后给一份可落地的选型建议。

前置知识:状态管理的本质,是”状态变化 → 通知 UI 重建”。不管哪个方案,底层都绕不开 InheritedWidget(用于在树中向下传递依赖)和某种”可监听对象”(ChangeNotifier / Stream / ValueNotifier)。理解这一点,看每个方案就都是”换了层皮”。相关机制见《Flutter 三棵树》里对 InheritedWidgetBuildContext 的讨论。

先建立评价坐标系

抛开信仰,比较状态管理方案就看这几个维度:

维度说明
样板代码实现同一功能要写多少”仪式性”代码
学习曲线概念多不多、心智负担大不大
编译期安全拿错类型/忘了 provide,是编译报错还是运行时崩
是否依赖 BuildContext拿状态要不要 context,影响可测试性与在非 UI 层的可用性
可测试性业务逻辑能否脱离 Widget 单测
关注点分离是否强制把 UI 和逻辑分开
生态与边界只管状态,还是把路由、DI、网络都包了

带着这套坐标,逐个看。

统一示例:计数器 + 异步加载

为了公平对比,四个方案都实现同一个需求:一个同步计数器,外加一个”异步拉取用户名”的场景(覆盖 loading/error/data 三态)。

Provider —— 官方推荐的最小共识

Provider 是对 InheritedWidget 的封装,长期是 Flutter 官方文档推荐的入门方案。核心三件套:用 ChangeNotifier 装状态、用 ChangeNotifierProvider 注入树、用 context.watch / Consumer 消费。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
// 1. 状态载体:继承 ChangeNotifier,变更后 notifyListeners()
class CounterModel extends ChangeNotifier {
  int _count = 0;
  int get count => _count;

  void increment() {
    _count++;
    notifyListeners(); // 通知所有监听者重建
  }
}

// 2. 注入:在树的上层 provide
ChangeNotifierProvider(
  create: (_) => CounterModel(),
  child: const MyApp(),
);

// 3. 消费:watch 会订阅并重建,read 只取值不订阅
class CounterText extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    final count = context.watch<CounterModel>().count;
    return Text('$count');
  }
}

// 触发变更(在按钮回调里,用 read 避免不必要订阅)
onPressed: () => context.read<CounterModel>().increment(),

优点:概念少、上手快、和官方结合紧密;Consumer/Selector 能做局部重建优化。 缺点:依赖 BuildContext 取值,容易写出 Provider not found 这类运行时错误(编译期不拦截);同类型多实例要靠 ProviderKey/嵌套区分,略笨拙;异步态要自己搭 FutureProvider 或在 model 里手动管 loading/error。

Riverpod —— Provider 的”编译期安全”进化版

Riverpod 出自 Provider 同一作者(名字就是 Provider 的字母重排),目标是修掉 Provider 的两大痛点:不再依赖 BuildContext编译期安全。它的 provider 是全局声明的顶层变量,拿错、忘 provide 直接编译不过。这里用当前的 Riverpod 3.x 推荐写法(Notifier / AsyncNotifier)。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
// 1. 同步状态:Notifier + NotifierProvider
class Counter extends Notifier<int> {
  @override
  int build() => 0; // 初始状态

  void increment() => state++; // 直接改 state 即自动通知
}
final counterProvider = NotifierProvider<Counter, int>(Counter.new);

// 2. 异步状态:AsyncNotifier,天然带 loading/error/data 三态
class UserName extends AsyncNotifier<String> {
  @override
  Future<String> build() async => fetchUserName(); // 异步初始化
}
final userNameProvider = AsyncNotifierProvider<UserName, String>(UserName.new);

// 3. 注入:根部包一层 ProviderScope(整个应用一次即可)
runApp(const ProviderScope(child: MyApp()));

// 4. 消费:ConsumerWidget 提供 ref,ref.watch 订阅
class Home extends ConsumerWidget {
  @override
  Widget build(BuildContext context, WidgetRef ref) {
    final count = ref.watch(counterProvider);
    final userName = ref.watch(userNameProvider);

    // AsyncValue 用 switch 优雅处理三态
    return Column(children: [
      Text('$count'),
      switch (userName) {
        AsyncData(:final value) => Text('用户:$value'),
        AsyncError(:final error) => Text('出错:$error'),
        _ => const CircularProgressIndicator(),
      },
      ElevatedButton(
        onPressed: () => ref.read(counterProvider.notifier).increment(),
        child: const Text('+1'),
      ),
    ]);
  }
}

如果引入 riverpod_generator,还能用 @riverpod 注解 + 代码生成进一步减少样板:

1
2
@riverpod
String label(Ref ref) => 'Hello world'; // 生成 labelProvider

优点编译期安全(拿错直接报错,告别 ProviderNotFound);不依赖 BuildContext,逻辑层能脱离 UI;AsyncNotifier 把异步三态和缓存、自动 dispose 都内建了;provider 之间可组合(一个 watch 另一个)。 缺点:概念比 Provider 多(refAsyncValueautoDisposefamily);代码生成流派要配 build_runner,多一层构建;全局 provider 变量对习惯”依赖挂在树上”的人有认知转换成本。

Bloc / Cubit —— 事件驱动、强纪律

Bloc 走的是另一条路:把状态管理建模成事件流 → 状态流,强制单向数据流和关注点分离,在大型团队、复杂业务里最受青睐。它提供两个层级:轻量的 Cubit(直接方法调用 + emit)和完整的 Bloc(事件驱动)。

先看轻量的 Cubit

1
2
3
4
5
// Cubit:适合简单场景,直接方法 + emit
class CounterCubit extends Cubit<int> {
  CounterCubit() : super(0); // 初始状态 0
  void increment() => emit(state + 1);
}

再看完整的 Bloc——用 sealed class 定义事件,on<Event> 注册处理器:

1
2
3
4
5
6
7
8
9
10
// 1. 事件:sealed 保证穷尽匹配
sealed class CounterEvent {}
final class CounterIncrementPressed extends CounterEvent {}

// 2. Bloc:事件进 → 状态出
class CounterBloc extends Bloc<CounterEvent, int> {
  CounterBloc() : super(0) {
    on<CounterIncrementPressed>((event, emit) => emit(state + 1));
  }
}

UI 侧用 BlocProvider 注入、BlocBuilder 消费:

1
2
3
4
5
6
7
8
9
10
11
12
13
BlocProvider(
  create: (_) => CounterBloc(),
  child: BlocBuilder<CounterBloc, int>(
    builder: (context, state) => Column(children: [
      Text('$state'),
      ElevatedButton(
        // 派发事件
        onPressed: () => context.read<CounterBloc>().add(CounterIncrementPressed()),
        child: const Text('+1'),
      ),
    ]),
  ),
);

异步场景把 on<Event> 处理器写成 async,先 emit(Loading())emit(Success/Failure),配合一个 sealed 的 State 类穷尽三态即可。

flowchart LR
  UI[UI] -->|add Event| B[Bloc]
  B -->|on&lt;Event&gt; 处理| B
  B -->|emit State| S[State 流]
  S -->|BlocBuilder 重建| UI

优点单向数据流 + 强制关注点分离,逻辑完全脱离 UI,可读性和可维护性在大项目里最好;配套 bloc_testbloc DevTools、hydrated_bloc(持久化)等生态成熟;事件可追溯,天然适合日志、埋点、时间旅行调试。 缺点:样板代码最多(事件类、状态类、handler 一个不少),简单页面显得”杀鸡用牛刀”;学习曲线相对陡(Cubit 缓一点,Bloc 概念多)。

GetX —— 全家桶,极简样板

GetX 是四者里最”激进”的:它不只做状态管理,还把路由、依赖注入、国际化、工具方法打包进一个库,主打零样板、不依赖 BuildContext。响应式写法用 .obs + Obx

1
2
3
4
5
6
7
8
9
10
11
12
13
14
// 1. Controller:变量加 .obs 变成响应式
class CounterController extends GetxController {
  var count = 0.obs; // RxInt
  void increment() => count++; // 直接改,UI 自动更新
}

// 2. 注入 + 取用:Get.put 放入,Get.find 取出(也可 Get.lazyPut)
final c = Get.put(CounterController());

// 3. 消费:Obx 自动追踪用到的 .obs 变量并局部重建
Obx(() => Text('${c.count}'));

// 触发
onPressed: c.increment,

异步同样简洁,RxStatus 或直接用可空的 .obs + 判空即可,甚至能用 GetX 自带的 GetConnect 发网络请求。

优点样板极少、上手极快;不需要 context 就能拿状态、跳路由、弹 snackbar,写小项目/原型非常爽;一个库解决一大堆事,减少选型和依赖。 缺点把太多关注点耦合进一个库,与 Flutter 惯用法(声明式、依赖挂树上)偏离较大;Get.find 这种全局服务定位器容易滋生隐式依赖和”魔法”,大项目里可测试性和可维护性受质疑;社区对其架构争议较大,团队协作时规范约束弱。

选型提醒:GetX 的”爽”很大程度来自绕过了 Flutter 的一些约束(全局单例、无 context 访问)。小项目/个人项目收益明显;但在多人协作的大型项目里,这些”便利”往往会变成后期维护的隐性成本。用之前想清楚项目规模。

横向对比总表

维度ProviderRiverpodBloc / CubitGetX
样板代码中(codegen 更少)极少
学习曲线平缓较陡平缓
编译期安全✗(运行时报错)部分
依赖 BuildContext取值否/注入是
异步三态支持手动/FutureProvider内建 AsyncValue手动建 State手动/RxStatus
关注点分离弱约束强制弱约束
可测试性中低
生态边界仅状态仅状态(+DI 能力)状态为主,生态成熟全家桶
适合规模中小中大小/原型

选型建议:没有银弹,只有场景

flowchart TD
  Q0{项目规模与团队?} 
  Q0 -->|个人小项目/快速原型| G[GetX 或 Provider]
  Q0 -->|中型、想要安全与可测试| R[Riverpod]
  Q0 -->|大型、多人、强纪律| B[Bloc]
  Q0 -->|刚入门、只想理解本质| P[Provider]

落到一句话建议:

  • 想先理解状态管理本质、或维护老项目Provider。它离 InheritedWidget 最近,学会它再看别的都是变体。
  • 新项目、追求编译期安全和可测试性Riverpod。它基本是”Provider 做对了的样子”,异步场景尤其省心,是目前社区新项目的主流推荐。
  • 大型项目、多人协作、业务复杂、重视可维护性与调试Bloc。样板换来的是纪律和可追溯性,规模越大越划算。
  • 个人项目、原型、追求开发速度GetX。爽是真爽,但要清楚它的架构取舍,别在大项目里为一时便利埋雷。

一个务实的观点:状态管理方案没有绝对优劣,团队统一 + 用对场景远比”选到最强的那个”重要。一个项目里最忌讳的是四种方案混着用。

面试回答话术

Q1:Flutter 有这么多状态管理方案,它们的本质区别是什么?

“本质上它们都在做同一件事——’状态变化后通知 UI 重建’,底层都绕不开 InheritedWidget 做依赖传递,加上某种可监听对象比如 ChangeNotifier、Stream 或 ValueNotifier。区别在于’皮’:Provider 是对 InheritedWidget 的轻封装;Riverpod 把 provider 提成全局变量做到编译期安全、不依赖 context;Bloc 用事件流到状态流强制单向数据流和关注点分离;GetX 则是响应式加全家桶。所以我理解它们不是谁取代谁,而是在’样板量、安全性、纪律性’之间做不同取舍。”

Q2:Provider 和 Riverpod 什么关系?为什么会有 Riverpod?

“Riverpod 是 Provider 同一个作者写的,名字就是 Provider 的字母重排,可以理解成’Provider 做对了的版本’。它主要修两个痛点:一是 Provider 取值依赖 BuildContext,容易在运行时抛 ProviderNotFound;二是 Provider 编译期不安全。Riverpod 把 provider 变成全局顶层变量,拿错或忘了 provide 直接编译不过,也不再需要 context,逻辑层能脱离 UI。它的 AsyncNotifier 还把异步的 loading/error/data 三态和自动 dispose 内建了,异步场景特别省心。”

Q3:Bloc 的核心思想是什么?和 Cubit 有什么区别?

“Bloc 的核心是单向数据流:UI 派发 Event,Bloc 在 on 里处理并 emit 出新的 State,UI 再根据 State 重建,整条链路可追溯。它强制把业务逻辑和 UI 分开,所以在大型项目和多人协作里可维护性最好,配套的 bloc_test、DevTools、时间旅行调试也很成熟。Cubit 是 Bloc 的简化版,去掉了 Event 层,直接暴露方法调用 emit,适合逻辑较简单的场景;需要事件可追溯、可穷尽时再上完整的 Bloc。"

Q4:GetX 争议在哪?你会在什么项目里用它?

“GetX 争议主要在于它把状态、路由、依赖注入、工具方法全耦合进一个库,而且靠全局单例和服务定位器 Get.find 绕过了 Flutter 的很多约束,写起来样板极少、不用 context 很爽。但代价是隐式依赖多、和 Flutter 声明式惯用法偏离、大项目里可测试性和规范约束偏弱。所以我会在个人项目、快速原型这种追求速度的场景用它;多人协作的大型项目我更倾向 Riverpod 或 Bloc。”

Q5:如果让你给一个新的中大型项目选型,你怎么选?

“我会优先考虑 Riverpod 或 Bloc。如果团队更看重开发效率和编译期安全、异步场景多,选 Riverpod;如果业务特别复杂、多人协作、需要强纪律和可追溯的调试,选 Bloc。Provider 我一般留给小项目或维护老代码。最关键的一条是团队统一,一个项目里最忌讳几种方案混用,那比选哪个都更伤维护性。”

小结

  • 所有方案的本质都是”状态变化 → 通知重建”,底层是 InheritedWidget + 可监听对象,差异在样板量、安全性和纪律性的取舍。
  • Provider:官方最小共识,概念少但运行时才报错;Riverpod:编译期安全、不依赖 context、异步内建,新项目主流;Bloc:单向数据流、强制分离,大项目最稳;GetX:全家桶、样板极少,小项目爽、大项目慎用。
  • 选型没有银弹:按项目规模和团队选,并全项目统一,比追”最强方案”重要得多。
  • 想深入理解它们共同的地基,回看《Flutter 三棵树》里的 InheritedWidgetBuildContext,以及《Flutter 渲染管线》里状态变化如何驱动重建。
本文由作者按照 CC BY 4.0 进行授权