Get 这波之后,我把 Flutter 状态管理重新看了一遍:新项目到底该选谁?

前言

昨天 get 的删库跑路之后发,社区和公司部门内部基本可以说是原地炸了。(早上作者说是他的github账户被风控,但是get本身的问题已经很多了...)

问得最多的,不是"这个包现在还能不能装",而是另一类更扎心的问题:

"以后 Flutter 项目状态管理到底该怎么选?"

"Get 还能不能继续用?"

"Provider、Riverpod、Bloc 这些,到底谁更靠谱?"

"你如果现在开一个新项目,你会选谁?"

我仔细思考了,发现很多讨论都有两个毛病:

  1. 只聊 API,不聊项目演进后的维护成本。
  2. 只聊自己喜欢什么,不聊团队、业务、复杂度、学习成本这些现实问题。

所以这篇文章,我想和大家聊聊我的看法。

我想做的事情很简单:

借着这次 get 的风波,把 Flutter 生态里主流的状态管理方案,重新摆到桌面上,按真实项目的标准,认认真真聊一遍。

不是聊"哪个最优雅",而是聊:

  • 它到底解决什么问题
  • 它的边界在哪
  • 它为什么有人爱,也为什么有人骂
  • 如果现在重新开一个项目,我会怎么选

为了避免这篇文章变成"空对空",我还顺手把同一个业务场景做成了一个开源 Demo,把 Provider / Riverpod / Cubit / Bloc Event 全都落地了一遍。

开源地址在这里: state_manages

后面我文中提到的一些对比,不只是嘴上说说,基本都能在这个仓库里对上代码。

先说结论

我先把结论放前面,免得大家看半天最后发现和自己预想差不多。

如果你现在问我:

"2026 年这个时间点,Flutter 新项目状态管理怎么选?"

我的答案是:

  • 小项目、单人项目、快速起步:Provider
  • 中大型新项目、我个人最愿意推荐的平衡方案:Riverpod
  • 多人协作、复杂业务、强调规范和状态流可追踪:Cubit / Bloc
  • 老项目已经深度绑定 GetX:先稳住,不要一激动就全量重构
  • 新项目再从 0 开始选 GetX:我个人会明显更谨慎

注意,我这里不是说 GetX 技术上突然一夜归零了。

而是说,这次事情把一个以前很多人不愿意正视的问题,硬生生摊开了:

状态管理从来不是"代码写起来爽不爽"这么简单,它还关乎维护、协作、生态稳定性、升级路线、团队兜底能力。

说得再直白一点:

以前大家觉得"能跑就行", 这次很多人才开始意识到:

"哦,原来依赖生态稳定,也是技术选型的一部分。"


篇章一:先把问题掰直,状态管理到底在管什么

很多人聊状态管理,一上来就对比 API:

  • setState
  • notifyListeners
  • ref.watch
  • emit
  • Obx

但这其实是表象。

状态管理真正要解决的,不是"你用哪个函数刷新页面",而是下面这几件事:

  1. 状态放在哪里
  2. 状态变化后,谁来通知 UI
  3. 异步请求、空状态、错误状态怎么建模
  4. 页面越来越复杂之后,代码会不会开始失控
  5. 团队里第二个人、第三个人接手时,还看不看得懂

你会发现,一个状态管理方案,真正的价值,不在于它能不能写出页面。

因为大家都能写。

真正拉开差距的,是当业务变成这样时:

  • 页面有列表
  • 列表要刷新
  • 请求会失败
  • 有搜索
  • 有筛选
  • 有排序
  • 还有弹 Toast、弹 Dialog、跳详情页这种一次性副作用

这时候你再看,方案之间的差异就出来了。

也就是说:

状态管理的核心,不是"能不能更新 UI",而是"当 UI 和业务越来越复杂时,这套结构还能不能顶住"。


篇章二:Provider,Flutter 状态管理里的"老实人"

如果让我给 Provider 起个外号,我会叫它:

"老实人方案。"

它最大的特点就是:

  • 不花
  • 不绕
  • 不装神秘
  • 你基本一眼就能知道状态在哪、怎么改、谁在监听

这也是为什么,很多 Flutter 新人第一个真正上手的状态管理,都是它。

Provider 到底在干嘛

最常见的写法其实很直白:

dart 复制代码
class UserViewModel extends ChangeNotifier {
  bool loading = false;
  List<String> users = [];

  Future<void> loadUsers() async {
    loading = true;
    notifyListeners();

    await Future.delayed(const Duration(seconds: 1));
    users = ['Ava', 'Noah', 'Mia'];

    loading = false;
    notifyListeners();
  }
}

// 页面里用:

ChangeNotifierProvider(
  create: (_) => UserViewModel()..loadUsers(),
  child: Consumer<UserViewModel>(
    builder: (_, vm, __) {
      if (vm.loading) {
        return const CircularProgressIndicator();
      }
      return ListView(
        children: vm.users.map(Text.new).toList(),
      );
    },
  ),
)

这套东西的优点几乎不用解释:

  • 好懂
  • 上手快
  • 学习门槛低
  • 代码量不大
  • 对小页面非常够用

Provider 为什么好用

因为它特别符合人脑最朴素的思路:

  • 我有一个对象
  • 对象里放状态
  • 改完状态
  • 通知页面刷新

这套逻辑没有什么抽象负担。

对于很多简单页面来说,这种方案不仅够用,而且其实是最划算的。

你非要拿一个很轻的用户列表页,上来就写一堆事件、状态类、派生结构,很多时候反而是技术过剩。

Provider 的问题到底在哪

问题不在它不能用,而在它太容易一路"长歪"。

最开始你只放两个字段:

  • loading
  • users

后面慢慢加:

  • errorMessage
  • query
  • selectedFilter
  • sortType
  • showVipOnly
  • currentTab
  • hasMore
  • isRefreshing

再往后还会加一堆方法:

  • loadUsers
  • refreshUsers
  • retry
  • updateQuery
  • toggleVip
  • changeSort
  • openDetail
  • showErrorToast

写着写着,一个 ChangeNotifier 就变成了一个"巨型大管家"。

它不是不能维护,但它特别考验开发者的自觉。

Provider 最大的问题,不是功能弱,而是结构约束弱。

你写得好,它很好用。 你写得随便,它也很容易烂。

所以 Provider 适合谁

我会这样建议:

适合:

  • Flutter 初学者
  • 小型项目
  • 页面级逻辑不复杂的业务
  • 想先把状态管理基本感觉建立起来的人

不太适合:

  • 中大型复杂项目当唯一主状态管理方案
  • 多人长期协作、对结构一致性要求很高的团队
  • 派生状态很多、异步链路复杂的模块

篇章三:Riverpod,我最愿意推荐给新项目的方案

如果说 Provider 是"老实人",那 Riverpod 在我眼里更像:

"脑子清楚、结构现代、能打硬仗的中生代主力。"

我为什么这么说?

因为 Riverpod 真正厉害的地方,不是"语法多高级",而是:

它很擅长把依赖关系和状态关系拆清楚。

这点在项目越做越大时,价值会越来越明显。

Riverpod 和 Provider 最本质的区别

很多人会把 Riverpod 理解成"升级版 Provider"。

这么说不算全错,但也太粗暴了。

Provider 更像是:

  • 往 Widget Tree 里塞对象
  • 下层从树里读对象
  • 对象变了,通知相关 Widget 刷新

Riverpod 更像是:

  • 先把状态和依赖拆成一个个 provider 节点
  • provider 和 provider 之间可以互相组合
  • 页面只是去消费这些节点

你把它想象成一张依赖图,会更容易理解。

Riverpod 为什么在异步场景特别舒服

我觉得 Riverpod 最讨喜的一点,是它对异步状态的表达非常自然。

比如一个最常见的异步列表:

dart 复制代码
  final usersProvider =
      AsyncNotifierProvider<UsersNotifier, List<String>>(UsersNotifier.new);

  class UsersNotifier extends AsyncNotifier<List<String>> {
    @override
    Future<List<String>> build() async {
      await Future.delayed(const Duration(seconds: 1));
      return ['Ava', 'Noah', 'Mia'];
    }

    Future<void> refreshUsers() async {
      state = const AsyncLoading();
      state = await AsyncValue.guard(() async {
        await Future.delayed(const Duration(seconds: 1));
        return ['Ava', 'Noah', 'Mia'];
      });
    }
  }

//   页面里:

  final asyncUsers = ref.watch(usersProvider);

  return asyncUsers.when(
    loading: () => const CircularProgressIndicator(),
    error: (e, _) => Text('出错了:$e'),
    data: (users) => ListView(
      children: users.map(Text.new).toList(),
    ),
  );

你会发现这里有个很明显的优势:

异步状态本身就是框架的一等公民。

不是你自己去维护:

  • isLoading
  • errorMessage
  • hasData

而是用 AsyncValue 直接把这些状态表达出来。

这个对实际开发体验影响很大。

Riverpod 真正强的,不是异步,而是"组合能力"

如果你只拿一个简单异步列表示例去看 Riverpod,其实还没看到它最强的地方。

它真正强的是这种场景:

  • 原始用户列表一个 provider
  • 搜索关键词一个 provider
  • VIP 开关一个 provider
  • 排序方式一个 provider
  • 最终可见列表再是一个派生 provider
  • 用户详情页再用 family

也就是说:

Riverpod 不是鼓励你写一个"大而全的状态类",而是鼓励你把不同职责拆成多个 provider,再组合起来。

这会带来两个很现实的好处:

  1. 结构更清楚
  2. 重建范围更好控制

Riverpod 有什么代价

它当然也不是白给的。

代价主要有三个:

1. 学习曲线比 Provider 高

你第一次看 Riverpod,脑子里经常会冒出几个问题:

  • 为什么一个页面拆这么多 provider
  • 为什么这里 watch,那边 read
  • 为什么这里要 listen
  • 为什么 provider 还要依赖 provider

这很正常,因为 Riverpod 不是在教你"存一个对象",而是在教你"组织一组状态节点"。

2. 写不好会显得很碎

Riverpod 的优点是可拆分,但坏处也正是可拆分。

如果一个团队没有统一规范,很容易出现:

  • provider 命名混乱
  • 分层过细
  • 逻辑散落到各处

最后导致不是"结构清晰",而是"文件一大堆,人都找不到"。

3. 对抽象能力有要求

Riverpod 更适合那种愿意先想清楚状态边界,再写代码的人。

如果一个人习惯先堆功能,再慢慢补结构,那 Riverpod 反而不一定让他更轻松。

所以 Riverpod 适合谁

我的建议是:

非常适合:

  • 中大型新项目
  • 需要长期维护的项目
  • 依赖关系复杂、派生状态较多的模块
  • 想把局部注入、测试隔离做得更清晰的团队

如果你问我现在新项目更倾向推荐谁,

我个人会优先推荐 Riverpod。

不是因为它最火,也不是因为它"最优雅",而是因为它在:

  • 开发体验
  • 异步表达
  • 可组合性
  • 可维护性
  • 模块化能力

这几个维度上,整体太均衡了。

篇章四:Bloc / Cubit,这套东西不是"重",而是"规矩大"

很多人一提到 Bloc,第一反应就是:

"太重了。"

这句话不能说错,但我觉得它只说了一半。

更准确一点的说法应该是:

Bloc 不是单纯地重,它是规矩大。

它会逼着你把一些以前可以"糊着写"的东西,全部摊开来写清楚。

比如:

  • 页面到底触发了什么动作
  • 动作进来后,状态怎么变
  • 哪些地方是副作用
  • 哪些地方只是纯渲染

这套思路,在简单页面里确实显得重。

但一旦业务复杂起来,它的价值就会越来越大。

先说 Cubit,它比你想象中实用

我其实很想先替 Cubit 正个名。

因为很多人把 Bloc 体系一股脑都理解成:

  • 一堆 event
  • 一堆 state
  • 一堆 boilerplate

但 Cubit 不是这样的。

Cubit 更像是:

"有明确状态对象的、工程化一点的 ViewModel。"

比如:

dart 复制代码
  class UsersCubit extends Cubit<UsersState> {
    UsersCubit() : super(const UsersState());

    Future<void> loadUsers() async {
      emit(state.copyWith(loading: true));

      await Future.delayed(const Duration(seconds: 1));

      emit(
        state.copyWith(
          loading: false,
          users: ['Ava', 'Noah', 'Mia'],
        ),
      );
    }
  }

你看,它其实很好懂:

  • 有状态类
  • 有方法
  • 改状态时 emit
  • 页面用 BlocBuilder 监听

对很多简单到中等复杂度页面来说,Cubit 是非常实用的。

它比 Provider 更有"状态层"的味道, 又比完整 Bloc Event 版轻很多。

那为什么还需要 Bloc Event 版

因为业务一复杂,方法驱动就开始不够清楚了。

比如一个页面同时有:

  • 首次加载
  • 刷新
  • 重试
  • 搜索变更
  • 排序切换
  • 筛选切换
  • Toast 提示
  • 并发请求控制

这时候你再全靠方法名去表达,就会慢慢开始乱。

而 Event 版会逼你把事情说清楚:

  • UsersRequested
  • UsersRefreshed
  • UsersRetried
  • SearchChanged
  • FilterChanged

这不是为了多写几个类,而是为了让状态变化路径可追踪。

尤其在多人协作里,这点特别重要。

Bloc 这套方案最值钱的地方

我觉得有三点。

1. 事件语义明确

一眼就能看出:

"页面现在到底发生了什么业务动作。"

2. 副作用边界清楚

用 BlocBuilder 渲染 UI, 用 BlocListener 处理 Toast、Dialog、路由跳转。

这比很多项目里"状态逻辑和副作用搅成一锅粥"的写法,要清爽太多。

3. 复杂交互下更稳

比如并发控制。

Bloc 生态里你可以明确去处理:

  • 重复点击刷新怎么办
  • 搜索输入连发怎么办
  • 模式切换时要保留最后一次还是顺序执行

这些东西,在复杂项目里不是"锦上添花",而是迟早会遇到的坑。

Bloc 的代价是什么

说实话,代价也很明显:

  • 样板代码更多
  • 初学者上手成本更高
  • 简单页面里容易显得重炮打蚊子

所以我不会无脑推荐所有人都上 Bloc Event 版。

但如果你在的团队是这种风格:

  • 人多
  • 业务重
  • 状态复杂
  • 强调规范
  • 维护周期长

那 Bloc 的价值,真的会越来越明显。

所以 Bloc / Cubit 适合谁

我的建议是:

  • 简单页面:Cubit 很香
  • 复杂业务模块:Bloc Event 版很稳
  • 团队已经全套 flutter_bloc:不要轻易引第二套主状态管理
  • 如果只是个人小项目,别一上来就给自己加戏

篇章五:GetX,到底该怎么重新看

这部分我不想写成"清算大会"。

因为说实话,GetX 当年能火,不是没原因的。

它确实帮很多 Flutter 开发者解决过实际问题。

尤其是早几年,Flutter 生态还没现在这么成熟时,GetX 的那种"开箱快、上手爽、什么都给你带一点"的感觉,对很多人真的很有吸引力。

GetX 当年为什么能打

原因其实很现实:

  • 学起来快
  • 写起来省事
  • 状态管理、路由、依赖注入几乎一把梭
  • 对很多从前端框架过来的人很有亲和力

你写页面时会感觉:

"卧槽,这也太快了吧。"

这就是 GetX 当年最强的传播力来源。

但它的问题也一直存在

只不过以前很多人选择忽略。

我自己总结,主要有这几类:

1. 职责容易混在一起

GetX 很容易让人一路写成这种结构:

  • 状态也在 controller
  • 路由也在 controller
  • 依赖注入也在 controller
  • 页面副作用也在 controller
  • 工具方法也在 controller

最后 controller 既像 ViewModel,又像 Service,又像 Router。

短期开发很爽,长期看边界其实很容易糊。

2. "魔法感"很强

很多写法前期很丝滑,但越往后越容易出现一种感觉:

项目能跑,但你说不清它到底靠什么机制在跑。

这对个人项目问题不大, 但对团队维护来说,是个隐患。

3. 风险被低估了

以前大家更多讨论的是:

  • 性能
  • 写法
  • 学习曲线

这次事情之后,大家终于被迫意识到另一层风险:

生态稳定性和治理能力,也是技术选型的一部分。

所以我现在对 GetX 的态度是:

  • 我不会否认它历史上的价值
  • 我也不会说所有 GetX 项目都得马上重构
  • 但如果你现在让我从 0 开一个新项目,我会明显更谨慎

篇章六:如果今天重新开一个项目,我会怎么选

这一段我尽量说人话,不打太极。

场景一:单人项目、验证想法、快速上线

我会优先考虑:

  • Provider
  • Cubit

原因很简单:

  • 成本低
  • 起步快
  • 心智负担小

别把事情搞太复杂。

场景二:中小团队新项目,业务会持续增长

我会优先考虑:

  • Riverpod

原因是它太平衡了。

它既没有 Bloc Event 那么重, 又比 Provider 更容易把结构撑住。

如果团队里成员整体水平还不错,Riverpod 是一个非常舒服的主状态管理方案。

场景三:复杂业务、多人协作、强调规范

我会优先考虑:

  • Cubit + Bloc Event

简单模块用 Cubit, 复杂模块上 Bloc Event。

这种搭配很实用。

场景四:老项目已经深度用了 GetX

我不会建议你们一夜重构。

我的建议是:

  1. 先锁版本
  2. 先备份依赖来源
  3. 先把最关键模块稳住
  4. 新模块逐步减少对 GetX 的继续扩散
  5. 再考虑渐进迁移

因为状态管理迁移这种事,一旦上头,很容易把"风险治理"做成"二次事故"。


最后

这次 get 的事情,对我来说最大的提醒,不是"某个包危险",而是另一件更本质的事:

技术选型从来不是一锤子买卖。

你今天选一个方案,不只是选它今天写起来爽不爽, 你其实是在选:

  • 三个月后它还好不好改
  • 半年后新人能不能接
  • 一年后团队还能不能稳稳维护
  • 真出事时,你们有没有兜底能力

所以如果你现在问我:

"Get 这波之后,Flutter 状态管理该怎么重新看?"

我的答案是:

  • Provider,适合入门和轻量场景
  • Riverpod,是我目前最愿意推荐给新项目的平衡方案
  • Cubit / Bloc,适合复杂业务和多人协作
  • GetX,不是不能用,但以后别再只看"写起来爽不爽"了

说到底,真正好的状态管理方案,不是"最酷"的那个。

而是当项目做大之后,它还能让你回答清楚下面这句话:

"状态从哪来,为什么变,谁在监听,副作用在哪发生。"

如果这个问题它还能帮你解释清楚,那它就值钱。 如果它让这些东西越来越糊,那它迟早会反噬你。

这也是我这两天重新看 Flutter 状态管理生态之后,最真实的感受。

如果你们团队现在也在重新评估状态管理路线,希望这篇能帮你少踩几个坑。


如果你想让我继续写

这篇如果大家爱看,后面我可以继续写三篇:

  1. Provider、Riverpod、Bloc,我做了一个同业务 Demo,带你看真实代码差异
  2. GetX 老项目怎么渐进式迁移,不推倒重来
  3. Flutter 状态管理怎么选,别只看 API,得看团队结构

如果你觉得有用,评论区告诉我,我就继续更。

往期文章回顾

Get 删库风波

Web 前端转 Flutter

Flutter 图片编辑器

Flutter 全链路监控 SDK

相关推荐
一天睡25小时2 小时前
做产品前,先别急着写代码:我是怎么判断一个点子值不值得做的
前端
霍理迪2 小时前
TS—函数、类、泛型
前端
键盘鼓手苏苏3 小时前
Flutter 三方库 persistent_cache_simple 的鸿蒙化适配指南 - 实现具备磁盘溢出淘汰与极简 API 的本地持久化缓存、支持端侧资源异步落地与状态秒开实战
flutter·缓存·harmonyos
cc.ChenLy3 小时前
浏览器缓存机制详解:如何彻底解决前端代码更新后的缓存问题
前端
XTTX1103 小时前
Vue3+Cesium电子围栏效果
前端·javascript·vue.js
AIData搭子3 小时前
Vector 基于多索引表架构的大规模向量检索
架构
KevinWang_3 小时前
AI 基础设施及其应用
前端
AIFarmer3 小时前
npm : 无法将“npm”项识别为 cmdlet、函数、脚本文件或可运行程序的名称。请检查名称的拼写,如果包括路径,请确保路径正确, 然后再试一次。
前端·npm·node.js