Provider 状态管理
什么是状态管理?为什么需要 Provider?
想象一个场景:你在 App 顶部有一个购物车图标,显示商品数量;在商品列表页点击"加入购物车"后,顶部的数字要立刻更新。
问题:这两个组件不在同一个 Widget 树层级,怎么让它们共享同一份数据?
答案:这就是 状态管理 要解决的事。Provider 是 Flutter 官方推荐的方案,简单、好用、够用。
💡 一句话理解 Provider:把数据放在树顶,任何子组件都能读到,数据变了自动刷新 UI。
安装
bash
flutter pub add provider核心思路(只需记住 3 步)
1️⃣ 创建数据模型 → 2️⃣ 在顶层注入 → 3️⃣ 在子组件中读取下面用一个「计数器」例子贯穿讲解。
第一步:创建数据模型
数据模型就是你的"数据仓库"。继承 ChangeNotifier,数据变化时调用 notifyListeners() 通知 UI 刷新。
dart
import 'package:flutter/material.dart';
// ─── ★ ChangeNotifier ──────────────
class CounterModel extends ChangeNotifier {
int _count = 0;
// 对外暴露的只读属性
int get count => _count;
// 修改数据后,必须调用 notifyListeners()
void increment() {
_count++;
notifyListeners(); // 👈 告诉所有监听者:"数据变了,快刷新!"
}
void decrement() {
_count--;
notifyListeners(); // 👈 同上
}
}
// ─── ☆ ChangeNotifier ──────────────⚠️ 关键点:忘记写
notifyListeners()是新手最常见的错误——数据改了但 UI 不更新。
第二步:在顶层注入
用 ChangeNotifierProvider 把数据模型挂到 Widget 树的顶部,这样所有子组件都能访问它。
dart
void main() {
runApp(
ChangeNotifierProvider(
create: (_) => CounterModel(), // 创建数据模型
child: const MyApp(), // 你的 App
),
);
}可以理解为:在树根放了一个"公告栏",所有子组件都能看。
第三步:在子组件中读取
有两种方式,分别适用于不同场景:
方式 A:context.watch() — 显示数据
当你需要展示数据、数据变了要自动刷新 UI 时,用 watch。
dart
class CounterText extends StatelessWidget {
const CounterText({super.key});
@override
Widget build(BuildContext context) {
// watch:监听数据变化,数据变了 → 这个 build 方法会重新执行
final counter = context.watch<CounterModel>();
return Text('当前计数:${counter.count}');
}
}方式 B:context.read() — 调用方法
当你只需要调用方法(如点击按钮)、不需要刷新当前 UI 时,用 read。
dart
class IncrementButton extends StatelessWidget {
const IncrementButton({super.key});
@override
Widget build(BuildContext context) {
return ElevatedButton(
// read:只调用方法,不监听变化,不会触发重建
onPressed: () => context.read<CounterModel>().increment(),
child: const Text('+1'),
);
}
}watch 和 read 怎么选?(最重要的一节)
watch | read | |
|---|---|---|
| 用途 | 读取数据并监听变化 | 调用方法 / 一次性读取 |
| 数据变了会重建? | ✅ 会 | ❌ 不会 |
| 典型场景 | 显示文本、列表等 | 按钮点击回调 |
记住这条规则:
- 要显示数据 →
watch - 要调用方法 →
read
dart
// ✅ 正确:显示数据用 watch
Text('${context.watch<CounterModel>().count}')
// ✅ 正确:点击按钮调用方法用 read
ElevatedButton(
onPressed: () => context.read<CounterModel>().increment(),
child: const Text('+1'),
)
// ❌ 错误:在按钮回调里用 watch(会导致不必要的重建)
ElevatedButton(
onPressed: () {
context.watch<CounterModel>().increment(); // 别这么写!
},
child: const Text('+1'),
)Consumer — 精确控制重建范围
context.watch() 会让整个 build 方法重建。如果你只想重建某个小部件,用 Consumer。
dart
class MyPage extends StatelessWidget {
@override
Widget build(BuildContext context) {
return Column(
children: [
const Text('这部分不会重建'),
// 只有 Consumer 内部会重建
Consumer<CounterModel>(
builder: (context, counter, child) {
return Text('计数:${counter.count}');
},
),
const Text('这部分也不会重建'),
],
);
}
}Consumer 的 child 参数(性能优化)
Consumer 的 child 参数可以传入不需要重建的静态部件:
dart
Consumer<CounterModel>(
builder: (context, counter, child) {
return Row(
children: [
Text('计数:${counter.count}'), // 会重建
child!, // 不会重建,直接复用
],
);
},
child: const Icon(Icons.star), // 👈 这个图标不会随数据变化而重建
)初学阶段不必纠结
child参数,等遇到性能问题再优化即可。
MultiProvider — 注入多个数据模型
实际项目中会有多个数据模型,用 MultiProvider 一次注入:
dart
void main() {
runApp(
MultiProvider(
providers: [
ChangeNotifierProvider(create: (_) => UserModel()),
ChangeNotifierProvider(create: (_) => CartModel()),
ChangeNotifierProvider(create: (_) => ThemeModel()),
],
child: const MyApp(),
),
);
}子组件中正常使用 watch / read 即可,用法不变。
完整示例:购物车
dart
// ========== 1. 数据模型 ==========
class CartModel extends ChangeNotifier {
final List<String> _items = [];
List<String> get items => List.unmodifiable(_items);
int get count => _items.length;
void add(String item) {
_items.add(item);
notifyListeners();
}
void removeAt(int index) {
_items.removeAt(index);
notifyListeners();
}
void clear() {
_items.clear();
notifyListeners();
}
}
// ========== 2. 顶层注入 ==========
void main() {
runApp(
ChangeNotifierProvider(
create: (_) => CartModel(),
child: const MyApp(),
),
);
}
// ========== 3. 显示购物车数量 ==========
class CartBadge extends StatelessWidget {
@override
Widget build(BuildContext context) {
final cart = context.watch<CartModel>();
return Text('购物车 (${cart.count})');
}
}
// ========== 4. 添加商品按钮 ==========
class AddToCartButton extends StatelessWidget {
final String item;
const AddToCartButton({super.key, required this.item});
@override
Widget build(BuildContext context) {
return ElevatedButton(
onPressed: () => context.read<CartModel>().add(item),
child: const Text('加入购物车'),
);
}
}Provider 类型速查
| 类型 | 适用场景 | 示例 |
|---|---|---|
ChangeNotifierProvider | 需要修改和监听的状态(最常用) | 计数器、购物车、用户信息 |
Provider | 只读的静态值 | 主题配置、API 地址 |
FutureProvider | 异步获取的数据 | 网络请求结果 |
StreamProvider | 持续的数据流 | WebSocket、实时定位 |
ProxyProvider | 依赖其他 Provider 的值 | 依赖用户信息的订单服务 |
初学者只需掌握
ChangeNotifierProvider,其他类型遇到需求再学。
什么时候用 Provider?
| 场景 | 推荐方案 |
|---|---|
| 只在单个组件内部用 | setState 就够了 |
| 需要在多个组件间共享 | ✅ Provider |
| 超大型项目、状态逻辑极复杂 | 考虑 Riverpod / Bloc |
常见错误排查
| 现象 | 原因 | 解决 |
|---|---|---|
| 数据改了但 UI 没更新 | 忘记调用 notifyListeners() | 在修改数据的方法末尾加上 |
报错 ProviderNotFoundException | 没在顶层注入,或类型写错了 | 检查 ChangeNotifierProvider 是否在父级 |
| 按钮点击后整个页面重建 | 在回调中用了 watch | 改用 read |
