AtomGit Flutter 鸿蒙客户端: ChangeNotifier 模式

在 Provider、Bloc、Riverpod、GetX 之外,Flutter 自带的 ChangeNotifier 也是一种选择------尤其适合中小型应用。


目录

  1. [E-Brufen 的状态管理全景](#E-Brufen 的状态管理全景)
  2. 为什么需要状态管理
  3. [MoodStorage 的完整 ChangeNotifier 实现](#MoodStorage 的完整 ChangeNotifier 实现)
  4. [UI 侧的监听与解绑](#UI 侧的监听与解绑)
  5. 应用架构图
  6. 与其他状态管理方案的对比
  7. [当 ChangeNotifier 不够用时:方案演进路径](#当 ChangeNotifier 不够用时:方案演进路径)
  8. 性能考虑与优化策略
  9. 常见陷阱与最佳实践
  10. 总结

一、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;
}

十、总结

核心观点

  1. ChangeNotifier 是 Flutter 中最简单的状态管理------它只是观察者模式的一个标准化实现,没有额外的概念负担。
  2. 规模匹配比流行度重要------对于 E-Brufen 这种 5 页面、2 数据源的应用,ChangeNotifier + 构造函数注入提供的功能刚好够用,没有过度工程化的负担。
  3. 三步监听模式是唯一需要记住的addListenersetStateremoveListener。其中 removeListener 是最容易被遗忘、后果最严重的一步。
  4. 架构可以演进------从 ChangeNotifier 升级到 Provider 几乎零成本(Provider 本身就用 ChangeNotifier),不需要提前引入复杂方案。
  5. 性能问题先测量再优化------在 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

相关推荐
取地址符1 小时前
C++学习笔记(基于learn-cxx)(1)
c++·经验分享·笔记·学习
国服第二切图仔2 小时前
HarmonyOS APP《画伴梦工厂》开发第59篇-碰一碰精准分享——跨设备素材插入
华为·harmonyos
hqzing2 小时前
在鸿蒙 PC 上使用 Claude Code(最新的 Bun 版本)
华为·harmonyos
恣逍信点3 小时前
主观是客观的聚焦
人工智能·学习·程序人生·生活·业界资讯·交友·哲学
打球她不香吗3 小时前
本周ALLegro学习内容的总结
学习·allegro
GitCode官方4 小时前
开源鸿蒙跨平台直播|QRN 跨端鸿蒙一体化实战
人工智能·华为·开源·harmonyos·atomgit
其实防守也摸鱼4 小时前
蓝队相关知识学习(1)---常用堡垒机和服务器
服务器·功能测试·学习·网络安全·网络攻击模型·攻防对抗·蓝队
ZZZMMM.zip4 小时前
演示架构师-PPT大纲生成的HarmonyOS开发实践
人工智能·华为·powerpoint·harmonyos·鸿蒙·鸿蒙系统
冰可乐配4 小时前
fmpeg音频编码组件aac(Advanced Audio Coding (AAC) encoder)学习
学习·音视频·aac