Flutter状态管理选型:Provider、Riverpod还是Bloc
Flutter状态管理选型:Provider、Riverpod还是Bloc
Flutter 状态管理是一个老生常谈但又绕不开的话题。团队每次新开项目,选型阶段总是吵得不可开交:有人觉得 Provider 够用,有人坚持要上 Riverpod,还有人认为只有 BLoC 才能应对复杂场景。
我经历过多次这种讨论,也踩过不少坑。这篇想聊聊这三个方案的实际适用场景,以及怎么根据团队和项目情况做选择。
先说结论
如果你在选型阶段纠结,可以先看这个判断逻辑:
- 小团队、快速迭代、团队成员水平不一:选 Provider,上手成本最低
- 中型项目、需要一定扩展性、想减少样板代码:选 Riverpod
- 大型复杂项目、团队有函数式编程背景、需要严格分层:选 BLoC
但结论永远只是参考,具体还是要看场景。
Provider:够用,但有边界
Provider 是 Flutter 官方推荐的方案,生态成熟,文档丰富。接入成本低,一个 ChangeNotifierProvider 就能把状态管起来:
class CartModel extends ChangeNotifier {
final List<Product> _items = [];
List<Product> get items => _items;
void add(Product product) {
_items.add(product);
notifyListeners();
}
}
ChangeNotifierProvider(
create: (_) => CartModel(),
child: CartPage(),
)
这套模式对于电商购物车、简单设置页、单个业务模块完全够用。问题是随着页面变多、状态关联变复杂,Provider 的问题就开始暴露:
问题 1:依赖地狱
页面 A 需要同时获取用户信息、购物车、搜索历史,嵌套写出来大概是这样:
MultiProvider(
providers: [
Provider(create: (_) => UserService()),
Provider(create: (_) => ProductService()),
ChangeNotifierProxyProvider<UserService, CartModel>(
create: (_) => CartModel(),
update: (_, user, cart) => cart!..setUser(user),
),
ChangeNotifierProxyProvider2<CartModel, ProductService, SearchHistory>(
create: (_) => SearchHistory(),
update: (_, cart, products, history) => history!..update(cart, products),
),
],
)
ProxyProvider 的嵌套层数一多,代码就很难维护,而且类型推断经常出问题。
问题 2:重建粒度粗
Provider 的消费者默认是整 Widget 重建:
Consumer<CartModel>(
builder: (_, cart, __) {
// cart.items 变化时,整个 build 方法都会执行
return Column(
children: [
Text('共 ${cart.items.length} 件商品'),
// 其他 Widget...
],
);
},
)
如果你只想监听 cart.totalPrice,但 cart 对象任何变化都会触发重建。这在复杂页面里是性能隐患。
问题 3:dispose 容易漏
ChangeNotifier 的 dispose 需要手动调用,大型应用里很容易出现状态没被正确清理的情况,尤其是页面跳转逻辑复杂时。
Riverpod:Provider 的升级版
Riverpod 可以理解为 Provider 的完全替代方案,核心改进是:编译时安全、依赖注入更清晰、重建粒度更精细。
核心概念:Provider
Riverpod 的 Provider 体系比 Provider 丰富很多:
// 最基础的 provider
final userNameProvider = Provider<String>((ref) => 'Guest');
// 带状态的 provider
final cartProvider = StateNotifierProvider<CartNotifier, CartState>((ref) {
return CartNotifier();
});
// 异步数据 provider
final productsProvider = FutureProvider<List<Product>>((ref) async {
return ProductService().fetchProducts();
});
依赖另一个 Provider
final userProvider = StateNotifierProvider<UserNotifier, UserState>((ref) {
return UserNotifier(ref.read(authProvider));
});
final cartTotalProvider = Provider<double>((ref) {
final cart = ref.watch(cartProvider);
return cart.items.fold(0, (sum, item) => sum + item.price);
});
这里 ref.watch 和 ref.read 的区别很重要:watch 用于响应式依赖,当被依赖的 Provider 变化时,当前 Provider 会自动重建;read 是只读访问,不会建立响应式依赖,常用于事件处理中。
自动 dispose
Riverpod 的 Provider 有完整的生命周期管理,不存在 ChangeNotifier 漏 dispose 的问题:
final repositoryProvider = Provider<ArticleRepository>((ref) {
final repository = ArticleRepository();
ref.onDispose(() => repository.dispose());
return repository;
});
实战建议
Riverpod 在这些场景下优势明显:
跨页面共享状态:比如登录状态、用户权限信息,Provider 需要 MultiProvider 嵌套,Riverpod 直接在任意地方
ref.watch复杂计算派生状态:购物车总价、筛选后的列表,Riverpod 的
Provider组合比 ChangeNotifier 更轻量异步数据流:网络请求状态(loading、error、data),用
AsyncValue处理比手动管理状态干净太多
final articleProvider = FutureProvider.family<Article, String>((ref, id) async {
return ArticleRepository().fetchArticle(id);
});
// 消费端
articleProvider.when(
data: (article) => ArticleView(article: article),
loading: () => CircularProgressIndicator(),
error: (e, _) => ErrorView(error: e),
)
BLoC:严格分层的代价与收益
BLoC 是这三个方案里最重的,但换来的是最清晰的分层和最严格的单向数据流。
核心模式
BLoC 把 UI 和业务逻辑完全隔开,靠 Events 输入、States 输出:
// Events
abstract class CartEvent {}
class AddItem extends CartEvent { final Product product; }
class RemoveItem extends CartEvent { final String productId; }
class Checkout extends CartEvent {}
// State
class CartState {
final List<CartItem> items;
final bool isCheckingOut;
final String? error;
}
// BLoC
class CartBloc extends Bloc<CartEvent, CartState> {
CartBloc() : super(CartState(items: [])) {
on<AddItem>((event, emit) {
emit(state.copyWith(items: [...state.items, event.product]));
});
on<RemoveItem>((event, emit) {
emit(state.copyWith(
items: state.items.where((i) => i.id != event.productId).toList(),
));
});
on<Checkout>((event, emit) async {
emit(state.copyWith(isCheckingOut: true));
try {
await CheckoutService().submit(state.items);
emit(state.copyWith(isCheckingOut: false, items: []));
} catch (e) {
emit(state.copyWith(isCheckingOut: false, error: e.toString()));
}
});
}
}
UI 层干净
BlocBuilder<CartBloc, CartState>(
builder: (context, state) {
if (state.isCheckingOut) {
return LoadingView();
}
return CartView(
items: state.items,
onAdd: (p) => context.read<CartBloc>().add(AddItem(p)),
onRemove: (id) => context.read<CartBloc>().add(RemoveItem(id)),
onCheckout: () => context.read<CartBloc>().add(Checkout()),
);
},
)
BLoC 的真实成本
BLoC 不是银弹,它有明显的接入成本:
- 学习曲线:团队必须理解 Event-State 模式,理解
emit是不可变的,理解为什么不能同步调用add再read状态 - 样板代码多:每个业务模块都要写 Event、State、BLoC 三个类,即使是很简单的逻辑
- 依赖注入复杂:BLoC 通常需要通过
BlocProvider注入,跨模块依赖时 provider 嵌套不比 Provider 好多少
什么时候值得用 BLoC
BLoC 的价值在于强制约束。当你的团队满足以下条件时,BLoC 的约束反而是保护:
- 项目成员超过 5 人,代码一致性要求高
- 业务逻辑复杂,有大量异步流程和状态转换
- 需要做详细的单元测试,BLoC 的纯函数式设计天然适合测试
- 产品后期维护周期长,需要严格的代码规范
对于简单活动页、内部工具类项目,BLoC 的重量会拖累开发速度。
实际项目中的混用策略
这三个方案不是非此即彼的关系。我目前项目里的做法是:
- 全局状态用 Riverpod:用户登录态、主题设置、全局配置
- 业务模块用 BLoC:订单流程、支付流程、复杂表单
- 局部 UI 状态用 setState:弹窗显隐、展开收起、微交互
// 全局配置 - Riverpod
final settingsProvider = StateNotifierProvider<SettingsNotifier, Settings>((ref) {
return SettingsNotifier();
});
// 订单模块 - BLoC
class OrderBloc extends Bloc<OrderEvent, OrderState> {
// 订单相关的复杂逻辑
}
// 页面内局部状态 - setState
class ProductDetailPage extends StatefulWidget {
State<ProductDetailPage> createState() => _ProductDetailPageState();
}
class _ProductDetailPageState extends State<ProductDetailPage> {
bool _isExpanded = false;
// 局部 UI 状态,不需要上升全局
}
这种分层的好处是:全局状态集中管理,复杂业务模块独立测试,局部状态不引入额外复杂度。
选型建议的量化参考
如果还是纠结,可以看这几个指标:
| 指标 | Provider | Riverpod | BLoC |
|---|---|---|---|
| 上手难度 | 低 | 中 | 高 |
| 样板代码量 | 少 | 少 | 多 |
| 编译时安全 | 无 | 有 | 部分有 |
| 测试难度 | 中 | 低 | 低 |
| 团队门槛 | 低 | 中 | 高 |
| 适合项目规模 | 小 | 中 | 大 |
还有一个实操判断:如果你的页面 build() 方法里出现了超过 3 层的 Consumer 或 context.watch,就该考虑迁移到 Riverpod 或者 BLoC 了。
最后
状态管理选型没有标准答案,但有一个避坑原则:不要为了架构而架构。
很多团队上来就决定用 BLoC,结果半年后项目黄了,代码倒是写得挺 BLoC。技术选型要跟着业务节奏走,小步快跑的项目用 Provider 快速落地,等业务稳定了再根据需要引入更重的方案。
最怕的是一开始选了最轻量的方案,结果业务复杂了硬撑,导致状态逻辑一团乱麻。这种情况不如早点用 Riverpod 重构,至少类型安全能帮你少踩很多坑。
