在 Provider、Bloc、Riverpod、GetX 之外,Flutter 自带的 ChangeNotifier 也是一种选择------尤其适合中小型应用。
目录
- [E-Brufen 的状态管理全景](#E-Brufen 的状态管理全景)
- 为什么需要状态管理
- [MoodStorage 的完整 ChangeNotifier 实现](#MoodStorage 的完整 ChangeNotifier 实现)
- [UI 侧的监听与解绑](#UI 侧的监听与解绑)
- 应用架构图
- 与其他状态管理方案的对比
- [当 ChangeNotifier 不够用时:方案演进路径](#当 ChangeNotifier 不够用时:方案演进路径)
- 性能考虑与优化策略
- 常见陷阱与最佳实践
- 总结
一、E-Brufen 的状态管理全景
E-Brufen 是一个情绪追踪 + 白噪音 + 呼吸练习的 Flutter 应用,面向鸿蒙 & Android 平台。整个应用使用了两种互补的状态管理模式:
┌─────────────────────────────────────────────────────────────┐
│ E-Brufen 架构总览 │
├─────────────────────────────────────────────────────────────┤
│ │
│ main.dart │
│ │ 创建 AppSettings + MoodStorage 实例 │
│ │ 通过构造函数向下传递 │
│ ▼ │
│ HomePage │
│ │ │
│ ├──► SoundscapePage ←── settings (被动读取) │
│ │ 白噪音场景选择 │
│ │ │
│ ├──► BreathePage ←── settings (被动读取) │
│ │ 呼吸模式配置 │
│ │ │
│ └──► DiaryPage ←── moodStorage (响应式订阅!) │
│ 情绪日记 CRUD │
│ │ │
│ ├── 记录 Tab ── 新增/编辑情绪 │
│ ├── 时间线 Tab ── 按月分组展示 │
│ └── 统计 Tab ── MoodChart 周柱状图 │
│ │
│ ───────────────────────────────────────────── │
│ 两种模式: │
│ │
│ 🔴 响应式(ChangeNotifier): │
│ MoodStorage ──notifyListeners()──► DiaryPage │
│ 数据变化 → UI 自动刷新 │
│ │
│ 🔵 被动式(普通 Dart 类): │
│ AppSettings ──直接 getter──► BreathePage │
│ initState 时读取,手动 setState │
│ │
└─────────────────────────────────────────────────────────────┘
关键设计决策:为什么 MoodStorage 用 ChangeNotifier,而 AppSettings 不用?
| 考量维度 | MoodStorage | AppSettings |
|---|---|---|
| 数据变更频率 | 高频(每次记录/编辑/删除) | 低频(切换场景/模式时) |
| 并发写入风险 | 多页面可能同时写入 | 单页面写入 |
| 跨组件同步需求 | 首页速记 + 日记页需同步 | 仅在当前页面生效 |
| 数据量 | 可能增长(历史记录) | 固定几个 key-value |
| 刷新粒度 | 三 Tab 同时刷新 | 当前页面 setState 即可 |
二、为什么需要状态管理
在讨论具体实现之前,先明确一个问题:为什么不直接用 setState?
场景一:单页面、单数据源 → setState 足够
dart
// 如果数据只在这个页面内使用,setState 完全够用
class _SimpleCounterState extends State<SimpleCounter> {
int _count = 0;
void _increment() {
setState(() => _count++);
}
@override
Widget build(BuildContext context) {
return Text('$_count');
}
}
场景二:跨组件共享数据 → 需要状态管理
E-Brufen 面临的实际问题:
HomePage.今日速记 ──insert()──► MoodStorage
│
notifyListeners() │
┌───────────────┤
▼ ▼
DiaryPage.记录Tab DiaryPage.统计Tab
(新增后刷新列表) (柱状图同步更新)
如果不用 ChangeNotifier:
- 首页写入后,日记页不知道数据变了,用户切换到日记页看到的是旧数据
- 需要在页面切换时手动传递 "刷新标志",耦合严重
- 每新增一个消费页面,都要修改写入方代码
ChangeNotifier 解决了这个 "一处写入,多处感知" 的问题。
三、MoodStorage 的完整 ChangeNotifier 实现

3.1 核心代码
lib/data/mood_storage.dart ------ 完整的 114 行实现:
dart
import 'dart:convert';
import 'package:flutter/foundation.dart';
import 'package:hive_ce/hive.dart';
import '../models/mood_entry.dart';
/// 纯 Hive 实现的情绪日记存储(无需原生 SQLite,鸿蒙兼容)
class MoodStorage extends ChangeNotifier { // ① 继承 ChangeNotifier
static const _boxName = 'moods';
Box? _box;
int _nextId = 1;
bool get isReady => _box != null && _box!.isOpen;
Future<void> init() async {
_box = await Hive.openBox(_boxName);
// 恢复最大 ID
if (_box!.isNotEmpty) {
_nextId = _box!.keys.fold<int>(
0,
(max, k) => k is int ? (k > max ? k : max) : max,
) + 1;
}
}
// ── CRUD ──
/// 插入一条记录,返回 ID
Future<int> insert(MoodEntry entry) async {
final id = _nextId++;
final data = entry.toJson();
data['id'] = id;
await _box?.put(id, jsonEncode(data));
notifyListeners(); // ② 写操作后广播
return id;
}
/// 获取所有记录,按时间倒序
List<MoodEntry> getAll() {
if (_box == null) return [];
final entries = <MoodEntry>[];
for (final key in _box!.keys) {
if (key is int) {
final raw = _box!.get(key);
if (raw is String) {
entries.add(_parse(key, raw));
}
}
}
entries.sort((a, b) => b.createdAt.compareTo(a.createdAt));
return entries;
}
/// 获取指定日期的记录
List<MoodEntry> getByDate(DateTime date) {
final start = DateTime(date.year, date.month, date.day);
final end = start.add(const Duration(days: 1));
return getAll().where((e) =>
e.createdAt.isAfter(start.subtract(const Duration(seconds: 1))) &&
e.createdAt.isBefore(end)).toList();
}
/// 获取指定周(周一~周日)的记录
List<MoodEntry> getByWeek(DateTime anyDay) {
final monday = _mondayOf(anyDay);
final sunday = monday.add(const Duration(days: 7));
return getAll().where((e) =>
e.createdAt.isAfter(monday.subtract(const Duration(seconds: 1))) &&
e.createdAt.isBefore(sunday)).toList();
}
/// 更新记录
Future<void> update(int id, MoodEntry updated) async {
final data = updated.toJson();
data['id'] = id;
await _box?.put(id, jsonEncode(data));
notifyListeners(); // ② 每次写操作都广播
}
/// 删除记录
Future<void> delete(int id) async {
await _box?.delete(id);
notifyListeners(); // ② 删除也不例外
}
/// 本周各心情计数
Map<int, int> getWeeklyMoodCounts(DateTime anyDay) {
final rows = getByWeek(anyDay);
final counts = <int, int>{1: 0, 2: 0, 3: 0, 4: 0, 5: 0};
for (final r in rows) {
counts[r.moodType.value] = (counts[r.moodType.value] ?? 0) + 1;
}
return counts;
}
// ── helpers ──
MoodEntry _parse(int id, String raw) {
final map = jsonDecode(raw) as Map<String, dynamic>;
return MoodEntry(
id: map['id'] as int?,
moodType: MoodType.fromValue(map['mood_type'] as int),
note: map['note'] as String?,
createdAt: DateTime.parse(map['created_at'] as String),
updatedAt: DateTime.parse(map['updated_at'] as String),
);
}
DateTime _mondayOf(DateTime d) {
return DateTime(d.year, d.month, d.day - (d.weekday - 1));
}
@override
void dispose() {
_box?.close(); // ③ 清理 Hive 资源
super.dispose();
}
}
3.2 ChangeNotifier 的三个关键锚点
┌───────────────────────────────────────────────────────┐
│ ChangeNotifier 三要素 │
├───────────────────────────────────────────────────────┤
│ │
│ ① extends ChangeNotifier │
│ │ │
│ └── 继承自 flutter/foundation.dart │
│ 获得 addListener / removeListener / │
│ notifyListeners 能力 │
│ │
│ ② notifyListeners() │
│ │ │
│ └── 在每次数据变更后调用 │
│ insert() / update() / delete() │
│ 内部调用链: │
│ notifyListeners() │
│ └── 遍历 _listeners 列表 │
│ └── 逐个调用 VoidCallback │
│ │
│ ③ dispose() │
│ │ │
│ └── 释放 ChangeNotifier 持有的资源 │
│ 关闭 Hive Box,防止 Box 泄漏 │
│ │
└───────────────────────────────────────────────────────┘
3.3 notifyListeners() 内部机制
ChangeNotifier 是 Flutter 内置的观察者模式实现,源码非常精简:
dart
// flutter/packages/flutter/lib/src/foundation/change_notifier.dart(简化版)
abstract class ChangeNotifier implements Listenable {
LinkedList<ListenerEntry>? _listeners;
@override
void addListener(VoidCallback listener) {
_listeners ??= LinkedList<ListenerEntry>();
_listeners!.add(ListenerEntry(listener));
}
@override
void removeListener(VoidCallback listener) {
for (final entry in _listeners ?? const LinkedList<ListenerEntry>.empty()) {
if (entry.listener == listener) {
_listeners!.remove(entry);
return;
}
}
}
@protected
void notifyListeners() {
for (final entry in _listeners ?? const LinkedList<ListenerEntry>.empty()) {
entry.listener();
}
}
@mustCallSuper
void dispose() {
_listeners?.clear();
}
}
要点:
- 使用
LinkedList(而非List)存储监听器,O(1) 删除 notifyListeners()是同步的------遍历所有监听器并逐个调用- 不传递任何数据------监听者需要自己去数据源拉最新数据(拉模式,而非推模式)
四、UI 侧的监听与解绑
4.1 标准三步监听模式
lib/pages/diary/diary_page.dart 中的完整实现:
dart
class DiaryPage extends StatefulWidget {
final MoodStorage moodStorage;
const DiaryPage({super.key, required this.moodStorage});
@override
State<DiaryPage> createState() => _DiaryPageState();
}
class _DiaryPageState extends State<DiaryPage>
with SingleTickerProviderStateMixin {
late TabController _tabController;
MoodType? _selectedMood;
final _noteController = TextEditingController();
DateTime _selectedDate = DateTime.now();
List<MoodEntry> _allMoods = [];
List<MoodEntry> _weekMoods = [];
int? _editingId;
@override
void initState() {
super.initState();
_tabController = TabController(length: 3, vsync: this);
// ── 第 1 步: 注册监听 ──
widget.moodStorage.addListener(_loadMoods);
// ── 首次加载 ──
_loadMoods();
}
// ── 第 2 步: 回调中刷新本地状态 ──
void _loadMoods() {
setState(() {
_allMoods = widget.moodStorage.getAll();
_weekMoods = widget.moodStorage.getByWeek(DateTime.now());
});
}
// ...
@override
void dispose() {
// ── 第 3 步: 移除监听(极其重要!) ──
widget.moodStorage.removeListener(_loadMoods);
_tabController.dispose();
_noteController.dispose();
super.dispose();
}
}
4.2 监听生命周期图
Widget 生命周期 ChangeNotifier 内部状态
initState()
│
├─ addListener(callback) → _listeners.add(callback) [count: 1]
│
├─ _loadMoods() callback() 被主动调用一次
│
▼
build() Widget 渲染,显示数据
│
│ ... 用户操作(insert/delete)...
│
├─ notifyListeners() callback() ← 自动触发
│ │ │
│ └─────────────────────────┘
│ _loadMoods() → setState() → build()
│
▼
dispose()
│
└─ removeListener(callback) → _listeners.remove(callback) [count: 0]
✅ 安全释放
4.3 内存泄漏警告:忘记 removeListener 的后果
┌───────────────────────────────────────────────────────────┐
│ ⚠️ 内存泄漏场景分析 │
├───────────────────────────────────────────────────────────┤
│ │
│ 1. DiaryPage 被 Navigator.pop() 弹出栈 │
│ │ │
│ 2. State 对象 dispose() 被调用 │
│ │ │
│ 3. 但 removeListener() 被遗漏了! │
│ │ │
│ 4. MoodStorage._listeners 仍然持有 _loadMoods 引用 │
│ │ _loadMoods 是 State 的成员方法 │
│ │ 成员方法隐式持有 State 的 this 引用 │
│ │ │
│ 5. 结果: │
│ ┌─────────────────────┐ │
│ │ MoodStorage (单例) │ │
│ │ └─ _listeners │ │
│ │ └─ _loadMoods │──► _DiaryPageState (已销毁!) │
│ │ │ └─ context (dangling) │
│ │ │ └─ _noteController │
│ │ │ └─ _tabController │
│ └─────────────────────┘ │
│ │
│ 后果: │
│ - State 及其持有的所有资源无法被 GC 回收 │
│ - 每次打开/关闭 DiaryPage 就泄漏一个 State 实例 │
│ - 泄露的监听器在 notifyListeners 时被调用 │
│ → setState() on disposed widget │
│ → FlutterError: setState() called after dispose() │
│ │
└───────────────────────────────────────────────────────────┘
修复方案(按可靠性排序):
dart
// 方案 A:手动 removeListener(当前 E-Brufen 的做法)
@override
void dispose() {
widget.moodStorage.removeListener(_loadMoods); // ✅ 可靠
super.dispose();
}
// 方案 B:使用 mixin 自动解绑(适合监听较多的情况)
mixin ChangeNotifierDisposer<T extends StatefulWidget> on State<T> {
final _listeners = <ChangeNotifier, VoidCallback>{};
void listenTo(ChangeNotifier notifier, VoidCallback callback) {
notifier.addListener(callback);
_listeners[notifier] = callback;
}
@override
void dispose() {
for (final entry in _listeners.entries) {
entry.key.removeListener(entry.value);
}
super.dispose();
}
}
// 方案 C:静态检查------添加 lint 规则
// 在 analysis_options.yaml 中启用:
// flutter: { rules: { use_dispose: true } }
// (注意:lint 能检测 _tabController.dispose() 遗漏,但无法
// 检测 removeListener 遗漏,因为它是自定义方法调用)
// 方案 D:在 addListener 时记录调用栈(调试用)
// 在开发模式下追踪监听器是否在 dispose 后被调用
@override
void addListener(VoidCallback listener) {
super.addListener(listener);
// debug only
assert(() {
debugPrint('[MoodStorage] Listener added, total: ${listenerCount}');
return true;
}());
}
五、应用架构图
5.1 数据流全景
┌──────────────┐
│ main.dart │
│ 创建并持有 │
│ MoodStorage │
│ AppSettings │
└──────┬───────┘
│ 构造函数注入(手动 DI)
┌──────────────────┼──────────────────┐
▼ ▼ ▼
┌──────────┐ ┌──────────┐ ┌──────────┐
│HomePage │ │ HomePage │ │ HomePage │
│ 今日速记 │ │ 白噪音卡 │ │ 呼吸卡 │
└────┬─────┘ └────┬─────┘ └────┬─────┘
│ insert() │ │
▼ ▼ ▼
┌──────────────┐ ┌──────────┐ ┌──────────┐
│ MoodStorage │ │Soundscape│ │ Breathe │
│ │ │ Page │ │ Page │
│ notifyListen │ │ │ │ │
│ ers() ───────┤ │ 被动读取 │ │ 被动读取 │
│ │ │ │ settings │ │ settings │
└───────┼──────┘ └──────────┘ └──────────┘
│
│ addListener 触发
▼
┌──────────────┐
│ DiaryPage │
│ │
│ _loadMoods() │
│ │ │
│ ▼ │
│ getAll() │──► _allMoods ──► 时间线 Tab
│ getByWeek() │──► _weekMoods ──► 统计 Tab (MoodChart)
│ │
│ setState() │──► 三 Tab 同步刷新
└──────────────┘
5.2 依赖注入方式对比
E-Brufen 使用构造函数注入(手动 DI),而非依赖注入框架:
dart
// main.dart --- 对象创建与注入
final moodStorage = MoodStorage();
await moodStorage.init();
runApp(EBrufenApp(
settings: settings,
moodStorage: moodStorage, // 传递给根 Widget
));
// EBrufenApp --- 透传
HomePage(moodStorage: moodStorage, settings: settings)
// HomePage --- 按需分发
DiaryPage(moodStorage: moodStorage) // 需要响应式
SoundscapePage(settings: settings) // 只需要被动读取
与框架化 DI 的对比:
| DI 方式 | 优点 | 缺点 |
|---|---|---|
| 构造函数传递(当前方案) | 零依赖、类型安全、显式 | 层级深时参数多 |
| Provider.of / context.read | 解耦、可按需获取 | 需要 Provider 包、隐式依赖 |
| InheritedWidget(手动) | Flutter 内置、无额外依赖 | 模板代码多 |
| GetIt(Service Locator) | 全局可访问、简单 | 隐藏依赖、难以测试 |
E-Brufen 只有 2 个共享服务,页面层级不深,构造函数注入是最合适的选择。
5.3 ChangeNotifier vs setState 决策树
数据是否需要跨 Widget 共享?
│
┌─────────┴─────────┐
│ 否 │ 是
▼ ▼
直接用 setState 数据变更是否频繁?
│
┌─────────┴─────────┐
│ 低频 │ 高频
▼ ▼
回调/Callback 用 ChangeNotifier
或 ValueNotifier │
是否需要细粒度更新?
│
┌───────┴───────┐
│ 否 │ 是
▼ ▼
ChangeNotifier 考虑 Provider
或 Riverpod
六、与其他状态管理方案的对比
6.1 全面对比表
| 维度 | ChangeNotifier | Provider | Bloc/Cubit | Riverpod | GetX |
|---|---|---|---|---|---|
| Flutter 内置 | ✅ 是 | ❌ 三方包 | ❌ 三方包 | ❌ 三方包 | ❌ 三方包 |
| pub 依赖 | 0 | provider | flutter_bloc | flutter_riverpod | get |
| 最小额外代码 | 5 行 | 10 行 | 30 行+ | 15 行 | 5 行 |
| 样板代码量 | 极少 | 少 | 多 | 中 | 极少 |
| 学习曲线 | 低 | 中 | 高 | 中高 | 低(但反模式多) |
| 强制架构 | 无 | 弱 | 强(Event/State) | 强(Provider/Notifier) | 无 |
| 可测试性 | 好 | 好 | 优秀 | 优秀 | 一般 |
| 鸿蒙兼容性 | 100% | 需验证 | 需验证 | 需验证 | 需验证 |
| 热重载友好 | ✅ | ✅ | ⚠️ | ✅ | ⚠️ |
| 细粒度重建 | ❌ | ✅ Consumer | ✅ BlocBuilder | ✅ select | ✅ Obx |
| 编译时安全 | ✅ | ✅ | ✅ | ✅ | ❌ (动态路由) |
| 适合规模 | 小中型 | 中大型 | 大型 | 中大型 | 中小型 |
6.2 代码对比:同一个 DiaryPage 用不同方案实现
ChangeNotifier(E-Brufen 当前实现)
dart
// 数据层
class MoodStorage extends ChangeNotifier {
Future<int> insert(MoodEntry entry) async {
// ...写入逻辑...
notifyListeners();
return id;
}
}
// UI 层
class _DiaryPageState extends State<DiaryPage> {
@override
void initState() {
super.initState();
widget.moodStorage.addListener(_loadMoods);
_loadMoods();
}
void _loadMoods() {
setState(() {
_allMoods = widget.moodStorage.getAll();
});
}
@override
void dispose() {
widget.moodStorage.removeListener(_loadMoods);
super.dispose();
}
}
Provider
dart
// 数据层 --- 完全一样
class MoodStorage extends ChangeNotifier {
// ... 相同实现 ...
}
// main.dart --- 需要额外注册
runApp(
ChangeNotifierProvider(
create: (_) => MoodStorage()..init(),
child: EBrufenApp(),
),
);
// UI 层
class DiaryPage extends StatelessWidget {
@override
Widget build(BuildContext context) {
final moods = context.watch<MoodStorage>().getAll();
// ✅ 优点:无需手动 addListener/removeListener
// ✅ 优点:Widget 树中任意层级都能获取
// ⚠️ 代价:依赖 Provider 包
// ⚠️ 代价:隐式依赖(不看代码不知道谁提供了 MoodStorage)
return ListView(...);
}
}
Bloc/Cubit
dart
// 数据层 --- 需要完全重写
// mood_cubit.dart
class MoodCubit extends Cubit<MoodState> {
MoodCubit() : super(MoodState.initial());
Future<void> insert(MoodEntry entry) async {
// ...写入逻辑...
final all = _box.getAll();
emit(MoodState(allMoods: all, weekMoods: _getByWeek(DateTime.now())));
}
}
// mood_state.dart
class MoodState {
final List<MoodEntry> allMoods;
final List<MoodEntry> weekMoods;
const MoodState({required this.allMoods, required this.weekMoods});
factory MoodState.initial() => MoodState(allMoods: [], weekMoods: []);
}
// UI 层
class DiaryPage extends StatelessWidget {
@override
Widget build(BuildContext context) {
return BlocBuilder<MoodCubit, MoodState>(
builder: (context, state) {
// ✅ 优点:状态不可变、可追踪
// ❌ 缺点:样板代码暴增(State 类、Event 类)
return ListView(...);
},
);
}
}
Riverpod
dart
// 数据层 --- 需要改写为 Notifier
// mood_provider.dart
final moodStorageProvider = NotifierProvider<MoodNotifier, List<MoodEntry>>(
MoodNotifier.new,
);
class MoodNotifier extends Notifier<List<MoodEntry>> {
@override
List<MoodEntry> build() => [];
Future<void> insert(MoodEntry entry) async {
// ...写入逻辑...
ref.read(_boxProvider).put(id, data);
state = [...state, entry]; // 不可变更新
}
}
// UI 层
class DiaryPage extends ConsumerWidget {
@override
Widget build(BuildContext context, WidgetRef ref) {
final moods = ref.watch(moodStorageProvider);
// ✅ 优点:编译时安全、自动 dispose、可组合
// ⚠️ 代价:全新的 Provider 体系、学习成本
return ListView(...);
}
}
GetX
dart
// 数据层
class MoodController extends GetxController {
var moods = <MoodEntry>[].obs; // .obs 使其变为响应式
Future<void> insert(MoodEntry entry) async {
// ...写入逻辑...
moods.add(entry); // 自动触发 UI 更新
}
}
// UI 层
class DiaryPage extends StatelessWidget {
final controller = Get.find<MoodController>();
@override
Widget build(BuildContext context) {
return Obx(() {
// ❌ 隐性依赖:controller 来自全局 Service Locator
// ❌ 无法在 Widget 树的子树中注入不同的实例
return ListView(...);
});
}
}
6.3 何时选择哪种方案
应用规模 推荐方案 理由
─────────────────────────────────────────────────────
1-3 页面 ChangeNotifier 零依赖、够用
(如 E-Brufen) 或 setState
4-10 页面 Provider 自动 dispose、
或 ChangeNotifier 但多了依赖
10-30 页面 Riverpod 编译安全、
或 Bloc 强架构约束
30+ 页面 Bloc + Clean Arch 需要严格的
或 Riverpod + 分层 架构规范
需要离线优先 ChangeNotifier 三方库可能不
/鸿蒙适配 或 Riverpod 兼容鸿蒙
七、当 ChangeNotifier 不够用时:方案演进路径
7.1 演进阶梯
Bloc/Cubit
┌──────────┐
│ Event ──►│ 需要严格的事件溯源?
│ State │ 需要中间件?
│ │ 团队熟悉 Redux 模式?
└────┬─────┘
│
Riverpod │
┌───────────┘
│ Provider 代码自动生成?
│ + 不可变 需要编译时安全?
│ + 自动 dispose
│ + 组合优于继承
└────┬───────┘
│
Provider │
┌──────────┘
│ ChangeNotifier + InheritedWidget
│ Context.watch / Context.read
│ 自动 addListener / removeListener
│ 自动 dispose
└────┬───────┘
│
ChangeNotifier ←── 你现在在这里
┌──────────┘
│ 零依赖
│ 手动 addListener / removeListener
│ 手动 DI(构造函数传递)
│ 够用、简单、显式
└─────────────
7.2 E-Brufen 的演进路径预测
当前阶段(5 页面,2 数据源):
ChangeNotifier + 构造函数 DI ✅
如果增长到 10+ 页面,多个数据源:
Provider 包装 ChangeNotifier
- Context.watch 替代手动 addListener
- 自动 dispose(Provider 管理生命周期)
- 保持 ChangeNotifier 数据层不变
如果再增长到 30+ 页面,复杂业务逻辑:
Riverpod
- 不可变状态(防止意外修改)
- 异步 Provider(FutureProvider / StreamProvider)
- 代码生成(riverpod_generator 减少模板代码)
关键原则:不需要提前为 "未来可能的需求" 引入复杂方案。 ChangeNotifier → Provider 的迁移成本很低(Provider 本身就用 ChangeNotifier),不需要过早优化。
八、性能考虑与优化策略
8.1 当前实现的性能特征
dart
void _loadMoods() {
setState(() {
_allMoods = widget.moodStorage.getAll(); // ① 全量查询 + 排序
_weekMoods = widget.moodStorage.getByWeek(DateTime.now()); // ② 又查一遍
});
}
每次 notifyListeners 触发的开销链:
notifyListeners()
└── _loadMoods()
├── getAll() // 遍历 Hive Box 全部 key
│ ├── _parse() × N // 每条 JSON 反序列化
│ └── sort() O(N log N) // 全量排序
│
└── getByWeek()
└── getAll() // 又遍历一遍!
└── _parse() × N // 又反序列化一遍!
8.2 数据量基准
| 记录数 | getAll 耗时(估算) | 对 UI 影响 |
|---|---|---|
| 50 条 | < 1ms | 无感知 |
| 500 条 | ~5ms | 无感知 |
| 5000 条 | ~50ms | 可能掉帧 |
| 50000 条 | ~500ms | 明显卡顿 |
E-Brufen 的使用场景:每人每天 1-3 条记录,一年约 1000 条。对普通用户来说,全量刷新足够。
8.3 优化策略(按实施顺序)
优化 1:避免重复查询
dart
void _loadMoods() {
// ❌ 原来:getAll() 被调用了两次(getByWeek 内部又调用了一次)
// _allMoods = widget.moodStorage.getAll();
// _weekMoods = widget.moodStorage.getByWeek(DateTime.now());
// ✅ 优化:复用一次查询结果
setState(() {
_allMoods = widget.moodStorage.getAll();
_weekMoods = _filterByWeek(_allMoods, DateTime.now());
});
}
List<MoodEntry> _filterByWeek(List<MoodEntry> all, DateTime d) {
final monday = _mondayOf(d);
final sunday = monday.add(const Duration(days: 7));
return all.where((e) =>
e.createdAt.isAfter(monday.subtract(const Duration(seconds: 1))) &&
e.createdAt.isBefore(sunday)).toList();
}
优化 2:懒解析 + 缓存
dart
class MoodStorage extends ChangeNotifier {
List<MoodEntry>? _cache; // 缓存最近一次查询结果
int _version = 0; // 数据版本号
Future<int> insert(MoodEntry entry) async {
// ...
_version++; // 版本递增 → 缓存失效
_cache = null;
notifyListeners();
return id;
}
List<MoodEntry> getAll() {
if (_cache != null) return _cache!;
// ... 查询逻辑 ...
_cache = entries;
return entries;
}
}
优化 3:增量通知(细粒度更新)
dart
enum MoodChangeType { inserted, updated, deleted }
class MoodChange {
final MoodChangeType type;
final int id;
final MoodEntry? entry;
const MoodChange(this.type, this.id, [this.entry]);
}
class MoodStorage extends ChangeNotifier {
// 为兼容性,保留原有的 VoidCallback 监听
// 新增细粒度监听
final _changeListeners = <void Function(MoodChange)>[];
void addChangeListener(void Function(MoodChange) listener) {
_changeListeners.add(listener);
}
void _notifyChange(MoodChange change) {
notifyListeners(); // 粗粒度
for (final l in _changeListeners) {
l(change); // 细粒度
}
}
// UI 侧可以根据 change 类型做增量更新:
// - inserted → 在列表头部插入
// - deleted → 从列表移除
// - updated → 更新对应项
}
优化 4:将耗时操作移到 Isolate
dart
// 当数据量达到万级别时考虑
Future<List<MoodEntry>> getAll() async {
if (_box == null) return [];
final raw = <String>[];
for (final key in _box!.keys) {
raw.add(_box!.get(key) as String);
}
// 反序列化 + 排序在 Isolate 中进行
return compute(_parseAndSort, raw);
}
List<MoodEntry> _parseAndSort(List<String> raw) {
// 在独立 Isolate 中执行
final entries = raw.map((r) => _parseStatic(r)).toList();
entries.sort((a, b) => b.createdAt.compareTo(a.createdAt));
return entries;
}
8.4 性能决策树
当前数据量 < 1000 条?
│
┌─────────┴─────────┐
│ 是 │ 否
▼ ▼
不需要优化 是否有用户反馈卡顿?
保持简单 │
┌─────────┴─────────┐
│ 否 │ 是
▼ ▼
加埋点监控 实施优化 1-2
│
仍有问题?───► 优化 3-4
九、常见陷阱与最佳实践
9.1 陷阱清单
| # | 陷阱 | 症状 | 正确做法 |
|---|---|---|---|
| 1 | 忘记 removeListener | 内存泄漏 + dispose 后 setState 报错 | dispose 中始终解绑 |
| 2 | 在 build 中 addListener | 每次 build 都重复注册,监听器数量爆增 | 只在 initState 中注册 |
| 3 | notifyListeners 在异步操作前 | UI 刷新时数据还没真正写入持久层 | 在 await 之后调用 |
| 4 | 循环通知 | 监听回调用又触发了写入 → 无限循环 | 加 guard 标记位 |
| 5 | 在 dispose 后调用 notifyListeners | 运行时异常 | 使用 _disposed 标记位 |
| 6 | 在非 UI 线程调用 notifyListeners | 状态不同步、难以调试 | 始终在主线程(Isolate)调用 |
| 7 | 过度通知(不必要地调用) | 无意义 rebuild,浪费性能 | 仅在数据确实变化时调用 |
| 8 | 忘记调用 super.dispose() | ChangeNotifier 内部资源未清理 | dispose 中始终 super.dispose() |
9.2 陷阱详细拆解
陷阱 4:循环通知
dart
// ❌ 错误:循环通知导致无限 rebuild
class _BadState extends State<BadWidget> {
@override
void initState() {
super.initState();
widget.storage.addListener(() {
// 监听回调中又触发了修改!
widget.storage.insert(someEntry); // ──┐
// ↑ notifyListeners() 又被调用 ──────┘ 循环!
});
}
}
// ✅ 正确:加 guard
class _GoodState extends State<GoodWidget> {
bool _updating = false;
@override
void initState() {
super.initState();
widget.storage.addListener(_onChange);
}
void _onChange() {
if (_updating) return; // ← 防止重入
_updating = true;
setState(() {
// ...安全的更新逻辑...
});
_updating = false;
}
}
陷阱 5:dispose 后的通知
dart
// ✅ 在 ChangeNotifier 中加保护
class SafeMoodStorage extends ChangeNotifier {
bool _disposed = false;
@override
void notifyListeners() {
if (!_disposed) {
super.notifyListeners();
}
}
@override
void dispose() {
_disposed = true;
_box?.close();
super.dispose();
}
}
陷阱 8:忘记 super.dispose()
dart
// ChangeNotifier 的 dispose 会清空 _listeners(LinkedList)
// 如果子类 override dispose 但不调用 super.dispose(),
// ChangeNotifier 内部的资源不会被释放
@override
void dispose() {
_box?.close();
super.dispose(); // ← 这行必须有!
}
9.3 最佳实践清单
dart
/// ✅ 推荐的 ChangeNotifier 模板
class RecommendedStorage extends ChangeNotifier {
// ── 1. 状态字段 ──
bool _disposed = false;
bool _initialized = false;
// ── 2. 初始化与清理 ──
Future<void> init() async {
// ...初始化资源...
_initialized = true;
}
@override
void dispose() {
_disposed = true;
// ...释放资源...
super.dispose(); // ← 始终调用
}
// ── 3. 安全通知 ──
void _safeNotify() {
if (!_disposed) {
notifyListeners();
}
}
// ── 4. 仅在数据实际变更时通知 ──
Future<void> updateIfChanged(int id, MoodEntry updated) async {
final existing = _box?.get(id);
final newData = jsonEncode(updated.toJson());
if (existing == newData) return; // ← 相同数据不通知
await _box?.put(id, newData);
_safeNotify();
}
// ── 5. 提供 hasListeners 检查(Flutter SDK 已提供) ──
Future<void> expensiveUpdate() async {
// ...耗时操作...
if (hasListeners) { // ← 没人监听就不通知
_safeNotify();
}
}
}
9.4 调试技巧
dart
// 在开发模式下追踪监听器生命周期
class DebugMoodStorage extends MoodStorage {
@override
void addListener(VoidCallback listener) {
super.addListener(listener);
assert(() {
debugPrint(
'[MoodStorage] ➕ Listener added '
'(total: ${listenerCount})',
);
return true;
}());
}
@override
void removeListener(VoidCallback listener) {
super.removeListener(listener);
assert(() {
debugPrint(
'[MoodStorage] ➖ Listener removed '
'(total: ${listenerCount})',
);
return true;
}());
}
@override
void notifyListeners() {
assert(() {
debugPrint(
'[MoodStorage] 📢 Notifying ${listenerCount} listener(s)',
);
return true;
}());
super.notifyListeners();
}
int get listenerCount {
// 通过检查 _listeners 的 LinkedList 长度(反射或计数)
// 简单做法:维护自己的计数器
return _listenerCount;
}
int _listenerCount = 0;
}
十、总结
核心观点
- ChangeNotifier 是 Flutter 中最简单的状态管理------它只是观察者模式的一个标准化实现,没有额外的概念负担。
- 规模匹配比流行度重要------对于 E-Brufen 这种 5 页面、2 数据源的应用,ChangeNotifier + 构造函数注入提供的功能刚好够用,没有过度工程化的负担。
- 三步监听模式是唯一需要记住的 :
addListener→setState→removeListener。其中removeListener是最容易被遗忘、后果最严重的一步。 - 架构可以演进------从 ChangeNotifier 升级到 Provider 几乎零成本(Provider 本身就用 ChangeNotifier),不需要提前引入复杂方案。
- 性能问题先测量再优化------在 1000 条数据以内,全量刷新完全够用。过早优化是万恶之源。
关键文件索引
| 文件 | 角色 |
|---|---|
lib/data/mood_storage.dart |
ChangeNotifier 实现(数据层) |
lib/pages/diary/diary_page.dart |
监听者(UI 层),三步监听模式 |
lib/data/settings.dart |
对比:不使用 ChangeNotifier 的被动式 |
lib/main.dart |
依赖注入:手动构造函数传递 |
lib/models/mood_entry.dart |
数据模型,支持 JSON 序列化 |
lib/widgets/mood_chart.dart |
跨 Tab 共享数据更新的消费者 |
一句话总结
ChangeNotifier 不是最强大的状态管理方案,但它是最简单的 。在选择技术方案时,规模匹配比"最新最热"更重要。先用简单方案把产品做出来,等真正遇到瓶颈时再演进------这比一开始就引入重型框架、被框架束缚了手脚,要好得多。
作者简介 :E-Brufen Dev,Flutter & 鸿蒙开发者,专注于跨平台移动应用开发与心理健康数字化。E-Brufen 是 AtomGit 上的开源 Flutter 鸿蒙应用,使用纯 Hive + ChangeNotifier 架构,零原生依赖,项目地址:AtomGit - E-Brufen。