用 ChangeNotifier 和 Provider 共享应用状态,同时掌握 watch、read 和 select,实现有针对性的重建。
在 Kodokon 中打开本课只要状态随单个界面而生、随它而灭,setState 就够用了。可一旦数据要被共享 - 购物车、会话、偏好设置 - 把它一层层从构造函数往下传就会变得无法管理。ChangeNotifier 提供了 SDK 中最精简的可观察对象;provider 把它注入到组件树中,并且只重建那些订阅了它的 widget。和 Riverpod 或 Bloc 相比,这套组合仍然是最容易审查的:几乎没有魔法,只有一个依赖,一套你能用一句话讲清楚的机制。
flutter pub add provider一个设计良好的 notifier 会做好封装:私有字段、只读的 getter,一切修改都只通过以 notifyListeners() 结尾的方法来进行。正是这份约定,保证了没有任何状态变化会绕过 UI。
import 'package:flutter/foundation.dart';
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 remove(String item) {
_items.remove(item);
notifyListeners();
}
}三个入口,三种用途。context.watch<T>() 会让 widget 订阅:每次 notifyListeners 它都会重建。context.read<T>() 只读取而不订阅:它是像 onPressed 这样的回调所用的工具。context.select<T, R>() 只订阅一个投影:只有当那个确切的值发生变化时,widget 才会重建。黄金法则是:watch 和 select 只用在 build 里,read 用在处理函数里 - 千万不要反过来。
import 'package:flutter/material.dart';
import 'package:provider/provider.dart';
class CounterModel extends ChangeNotifier {
int _value = 0;
int get value => _value;
void increment() {
_value++;
notifyListeners();
}
}
void main() {
runApp(
ChangeNotifierProvider(
create: (_) => CounterModel(),
child: const CounterApp(),
),
);
}
class CounterApp extends StatelessWidget {
const CounterApp({super.key});
@override
Widget build(BuildContext context) {
final value = context.watch<CounterModel>().value;
return MaterialApp(
home: Scaffold(
body: Center(child: Text('Count: $value')),
floatingActionButton: FloatingActionButton(
onPressed: () =>
context.read<CounterModel>().increment(),
child: const Icon(Icons.add),
),
),
);
}
}import 'package:flutter/material.dart';
import 'package:provider/provider.dart';
class CartModel extends ChangeNotifier {
final List<String> _items = [];
int get count => _items.length;
}
class CartBadge extends StatelessWidget {
const CartBadge({super.key});
@override
Widget build(BuildContext context) {
final count = context.select(
(CartModel cart) => cart.count,
);
return Badge(
label: Text('$count'),
child: const Icon(Icons.shopping_cart),
);
}
}
onPressed 内部,如何在不产生多余订阅的情况下触发 cart.add(...)?context.watch<CartModel>().add(...)context.read<CartModel>().add(...)context.select((CartModel c) => c.add(...))notifyListeners() 到底会触发什么?List.unmodifiable(_items),而不是直接暴露 _items?notifyListeners 的情况下改变状态,从而也不触发重建。