第 7 章 状态管理(完整可运行示例)
先明确边界:只影响自己页面的状态用 setState;跨页面共享的状态才需要状态管理库。这一章用两个完整程序把这件事讲透,最后进入 Riverpod。
7.1 反面教材:setState 跨页面共享有多痛
购物车数据放在首页,详情页要加购、底部栏要显示数量,就得把回调一层层往下传(prop drilling)。页面一深,参数会变成这样:
// 为了把「加购」传到第 4 层子组件,中间每个组件都要多一个参数
HomePage(onAddToCart: ..., cartCount: ...)
└ ProductList(onAddToCart: ..., cartCount: ...)
└ ProductCard(onAddToCart: ..., cartCount: ...)
└ AddButton(onAddToCart: ...) // 真正用到的地方在最底层
Riverpod 的做法是把状态放在「全局容器」里,任何组件直接 ref.watch 订阅,不需要经过中间层。
7.2 完整示例:用 Riverpod 做购物车(单文件可运行)
创建步骤
flutter create cart_demo && cd cart_demo;flutter pub add flutter_riverpod(会自动选当前兼容的最新版);- 用下面代码整体替换
lib/main.dart,flutter run运行; - 连续点右下角「加购」,观察 AppBar 角标与列表同时更新;左滑可删除。
import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
void main() => runApp(const ProviderScope(child: MyApp()));
class MyApp extends StatelessWidget {
const MyApp({super.key});
@override
Widget build(BuildContext context) {
return MaterialApp(
debugShowCheckedModeBanner: false,
theme: ThemeData(
useMaterial3: true,
colorScheme: ColorScheme.fromSeed(seedColor: const Color(0xFF2E7D6B)),
),
home: const CartPage(),
);
}
}
// ---------- ① 状态层:Notifier 里放数据 + 修改数据的方法 ----------
class CartNotifier extends Notifier<List<String>> {
@override
List<String> build() => const []; // 初始状态:空购物车
void add(String product) {
// 不要 state.add(...):Riverpod 靠对象引用判断是否变化,必须生成新列表
state = [...state, product];
}
void removeAt(int index) {
final next = [...state]..removeAt(index);
state = next;
}
void clear() => state = const [];
}
final cartProvider =
NotifierProvider<CartNotifier, List<String>>(CartNotifier.new);
// ---------- ② 派生状态:只关心数量,不重复存一份 ----------
final cartCountProvider = Provider<int>((ref) {
return ref.watch(cartProvider).length;
});
// ---------- ③ 界面层:ConsumerWidget 多一个 ref 参数 ----------
class CartPage extends ConsumerWidget {
const CartPage({super.key});
static const _products = ['拿铁', '美式', '可颂', '芝士蛋糕', '气泡水'];
@override
Widget build(BuildContext context, WidgetRef ref) {
final items = ref.watch(cartProvider); // 订阅列表
final count = ref.watch(cartCountProvider); // 订阅数量
final scheme = Theme.of(context).colorScheme;
return Scaffold(
appBar: AppBar(
title: const Text('购物车'),
actions: [
Padding(
padding: const EdgeInsets.only(right: 12),
child: Badge(
label: Text('$count'),
isLabelVisible: count > 0,
child: const Icon(Icons.shopping_cart_outlined),
),
),
if (items.isNotEmpty)
IconButton(
tooltip: '清空',
icon: const Icon(Icons.delete_sweep_outlined),
onPressed: () => ref.read(cartProvider.notifier).clear(),
),
],
),
body: items.isEmpty
? Center(
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
Icon(Icons.shopping_cart_outlined,
size: 56, color: scheme.outline),
const SizedBox(height: 12),
const Text('购物车是空的'),
],
),
)
: ListView.separated(
padding: const EdgeInsets.all(12),
itemCount: items.length,
separatorBuilder: (_, __) => const Divider(height: 1),
itemBuilder: (context, index) => Dismissible(
key: ValueKey('$index-${items[index]}'),
direction: DismissDirection.endToStart,
background: Container(
color: scheme.error,
alignment: Alignment.centerRight,
padding: const EdgeInsets.only(right: 20),
child: const Icon(Icons.delete, color: Colors.white),
),
onDismissed: (_) {
// 回调里用 read:只执行动作,不订阅
ref.read(cartProvider.notifier).removeAt(index);
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(content: Text('已移除 ${items[index]}')),
);
},
child: ListTile(
leading: CircleAvatar(child: Text('${index + 1}')),
title: Text(items[index]),
subtitle: const Text('左滑删除'),
),
),
),
floatingActionButton: FloatingActionButton.extended(
onPressed: () {
// read + notifier:拿到状态对象调用方法
final name = _products[count % _products.length];
ref.read(cartProvider.notifier).add(name);
},
icon: const Icon(Icons.add),
label: const Text('加购'),
),
);
}
}
关键点(背下来)
- watch 在 build 里,read 在回调里:
ref.watch(cartProvider)订阅数据;ref.read(cartProvider.notifier).add()执行动作。 - 状态类必须是不可变的:改列表要
[...state, item]生成新对象,直接add界面不会刷新。 - 派生数据用
Provider算出来(如数量),不要在两个地方各存一份,否则迟早不一致。 - 根组件必须包
ProviderScope,全局只能有一个;否则报No ProviderScope found。 - Riverpod 3 里
ref.watch的返回值没有valueOrNull了,用asyncValue.value取可空值。
动态效果 · 加购后角标与列表同步更新