状态管理选型:Provider 与 Riverpod
状态管理是 Flutter 社区争论最多的话题,但有一条底线:不要为了用而用。只有局部状态的页面,setState 就是最优解;当状态需要跨页面共享、需要缓存服务端数据、或需要独立可测试时,才值得引入状态管理库。本章先给判断标准,再给 provider 与 riverpod 的完整示例,最后给选型对照表。
什么时候真的需要状态管理
| 信号 | 是否必须 | 更轻的替代 |
|---|---|---|
| 状态只在单个页面内 | 不需要 | StatefulWidget + setState |
| 兄弟组件共享一层状态 | 不需要 | 状态提升到父级 |
| 三层以上跨层传递 | 需要 | 先试 InheritedWidget,再上库 |
| 多页面共享登录态、购物车 | 需要 | Provider / Riverpod |
| 服务端数据的加载与缓存 | 需要 | Riverpod 的异步 Provider 更顺 |
InheritedWidget 一句话原理:它是内置的"沿树向下广播数据"机制,子级订阅后只有订阅者重建;Theme、MediaQuery 都是它的实现,provider 与 Riverpod 都是在这个机制上加了一层好用的 API。
provider:ChangeNotifier + ChangeNotifierProvider + Consumer
flutter pub add provider
import 'package:flutter/material.dart';
import 'package:provider/provider.dart';
// 状态模型:继承 ChangeNotifier,改完数据调用 notifyListeners
class CartModel extends ChangeNotifier {
final List<String> _items = [];
List<String> get items => List.unmodifiable(_items); // 对外只读
void add(String name) {
_items.add(name);
notifyListeners(); // 通知所有监听者刷新
}
}
void main() => runApp(
// 在根部 Provide,全树可见
ChangeNotifierProvider(create: (_) => CartModel(), child: const MyApp()),
);
class CartPage extends StatelessWidget {
const CartPage({super.key});
@override
Widget build(BuildContext context) {
final count = context.watch<CartModel>().items.length; // 订阅:变化即重建
return Scaffold(
appBar: AppBar(title: Text('购物车($count)')),
body: Consumer<CartModel>( // Consumer 把重建范围收窄到 builder 内部
builder: (context, cart, _) => ListView(children: [for (final n in cart.items) ListTile(title: Text(n))]),
),
floatingActionButton: FloatingActionButton(
// 回调里只取实例、不订阅,用 read(等价于 listen: false)
onPressed: () => context.read<CartModel>().add('商品 ${DateTime.now().millisecond}'),
child: const Icon(Icons.add),
),
);
}
}
riverpod:ProviderScope + ref.watch + AsyncValue
flutter pub add flutter_riverpod
import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
// 同步状态:Notifier 的 build 返回初始值,state 读写由框架托管
final counterProvider = NotifierProvider<CounterNotifier, int>(CounterNotifier.new);
class CounterNotifier extends Notifier<int> {
@override
int build() => 0;
void add() => state = state + 1;
}
// 异步状态:FutureProvider 自动托管 loading / error / data
final newsProvider = FutureProvider<List<String>>((ref) async {
await Future<void>.delayed(const Duration(seconds: 1)); // 换成 dio 请求
return const ['Flutter 3.x 发布', 'Impeller 默认启用'];
});
void main() => runApp(const ProviderScope(child: MyApp())); // 必须包一层 ProviderScope
class NewsPage extends ConsumerWidget {
const NewsPage({super.key});
@override
Widget build(BuildContext context, WidgetRef ref) {
final count = ref.watch(counterProvider); // watch:订阅并随变化重建
final news = ref.watch(newsProvider);
return Scaffold(
appBar: AppBar(title: Text('计数 $count')),
body: news.when(
// AsyncValue.when 强制覆盖三种状态,漏写编译不过
loading: () => const Center(child: CircularProgressIndicator()),
error: (e, st) => Center(child: Text('加载失败:$e')),
data: (list) => ListView(children: [for (final s in list) ListTile(title: Text(s))]),
),
floatingActionButton: FloatingActionButton(
onPressed: () => ref.read(counterProvider.notifier).add(), // read:回调里只取一次
child: const Icon(Icons.add),
),
);
}
}
ref.watch 会订阅、值变化时重建;ref.read 不订阅,只能用在点击回调、initState 这类"只要一次"的地方。Riverpod 还有 AsyncNotifierProvider(异步状态 + 写方法)与 StreamProvider,都能沿用上面的写法。
选型对照表
| 维度 | setState | provider | riverpod |
|---|---|---|---|
| 学习成本 | 最低 | 低 | 中 |
| 样板代码量 | 最少 | 少 | 中(需声明 Provider) |
| 跨页面共享 | 很麻烦 | 方便 | 方便 |
| 编译期安全 | —— | 弱,ProviderNotFoundException 运行期才报 | 强 |
| 异步数据三态 | 自己写 | 自己写 | AsyncValue 内置 |
| 适合项目规模 | 单页小应用 | 中小型、存量 Provider 生态 | 中大型新项目 |
取舍建议:单人小工具只用 setState;中型业务(10~30 个页面)用 provider 足够,尤其团队已熟悉 ChangeNotifier;新项目且服务端数据多,直接上 riverpod,AsyncValue 能省下大量手写状态样板;已用 BLoC 的团队不必为追新而迁移。
反例:小项目上重型方案
一个只有计数器的小页面,用 Riverpod 包了 ProviderScope → Provider → ConsumerWidget → ref.watch → Notifier 五层,实际等价于一句 setState(() => _count++)。
判断公式很朴素:状态管理库的成本 ≈ 新增概念数 × 新增文件数 ÷ 参与人数。另一个常见反例是把所有东西都塞进全局 Provider,任何一处改动都触发大片重建——状态该近就近,跨层才共享。
flutter analyze # 检查 Provider 泛型写错、依赖未声明
flutter test
小结:先用 setState,跨层共享再上状态管理;provider 用 ChangeNotifier + ChangeNotifierProvider + Consumer/watch,riverpod 用 ProviderScope + NotifierProvider/FutureProvider + ref.watch/ref.read + AsyncValue.when;build 里 watch、回调里 read;选型看项目规模与团队习惯,不为用而用。