AtomGit Flutter 鸿蒙客户端:观察者模式的多种实现从 ValueNotifier 到 Stream
Flutter 中实现观察者模式的全景图——选择最适合你场景的方案
目录
- 观察者模式的本质:一对多依赖关系
- 实现一:VoidCallback 回调函数
- 实现二:ValueNotifier + ValueListenableBuilder
- 实现三:ChangeNotifier + ListenableBuilder
- 实现四:Stream + StreamBuilder
- 实现五:BehaviorSubject(RxDart)
- 实现六:InheritedWidget + dependOnInheritedWidgetOfExactType
- 六种实现全景对比表
- 实战一:MoodStorage 的三种实现方式对比
- 实战二:AppSettings 用 ValueNotifier 重构
- 实战三:全局主题切换用 InheritedWidget 实现
- 决策树:选择正确的观察者实现
- 常见陷阱与避坑指南
- 总结
一、观察者模式的本质:一对多依赖关系
观察者模式(Observer Pattern)是软件工程中最经典的行为设计模式之一。GoF 对它的定义简洁而精确:
定义对象间的一种一对多依赖关系,使得每当一个对象改变状态,所有依赖于它的对象都会得到通知并自动更新。
在 Flutter 应用开发中,观察者模式几乎无处不在。当一个数据源发生变化时,所有关心这个数据的 Widget 都需要重建(rebuild)。这正是观察者模式中"主题(Subject)通知观察者(Observer)"的标准场景。
在分析具体实现之前,我们先理清观察者模式中的四个核心角色在 Flutter 语境下的映射:
┌─────────────────────────────────────────────────────────────────┐
│ 观察者模式四角色(Flutter 映射) │
├─────────────────────────────────────────────────────────────────┤
│ │
│ Subject(主题/被观察者) │
│ │ 数据的持有者,维护一个观察者列表 │
│ │ Flutter 中:ChangeNotifier / ValueNotifier / Stream │
│ │ │
│ ├── addObserver() 注册观察者 │
│ ├── removeObserver() 移除观察者 │
│ └── notifyObservers() 通知所有观察者 │
│ │
│ Observer(观察者) │
│ │ 接收主题通知后执行更新逻辑 │
│ │ Flutter 中:VoidCallback / State.setState / StreamBuilder │
│ │ │
│ └── update() 收到通知后的响应方法 │
│ │
│ ConcreteSubject(具体主题) │
│ │ 实际持有业务数据的类 │
│ │ Flutter 中:MoodStorage / AppSettings / AudioController │
│ │ │
│ └── getData() 观察者通过拉取模式获取最新数据 │
│ │
│ ConcreteObserver(具体观察者) │
│ │ 实际的 Widget 或回调函数 │
│ │ Flutter 中:DiaryPage / BreathePage / SoundscapePage │
│ │ │
│ └── rebuild() 收到通知后重建 UI │
│ │
└─────────────────────────────────────────────────────────────────┘
Flutter 框架和 Dart 语言本身提供了多种实现观察者模式的方式。在 E-Brufen 项目中,我们实际使用了其中四种。本文将从最简到最复杂,逐一剖析这六种实现方式,并给出它们在不同场景下的选择决策树。
二、实现一:VoidCallback 回调函数
这是观察者模式最原始、最简单的形态。没有类继承,没有泛型,没有框架支持——只有一个 VoidCallback(即 void Function())类型的回调引用。
2.1 基本原理
Subject Observer
│ │
│ onPhaseChange = _callback; │ 注册:把函数引用赋值给 Subject 的属性
│ ◄──────────────────────────────│
│ │
│ onPhaseChange('吸气'); │ 通知:Subject 调用这个函数
│ ──────────────────────────────►│
│ │ _callback('吸气')
│ │ └── setState(() => _phase = '吸气')
2.2 E-Brufen 中的实际代码

在 BreathingCircle 组件中,我们使用了多个 VoidCallback 来实现观察者模式:
// lib/widgets/breathing_circle.dart(当前版本)
class BreathingCircle extends StatefulWidget {
final BreathePattern pattern;
final int totalMinutes;
// 三个 VoidCallback 观察者
final VoidCallback onComplete; // 练习完成时通知
final ValueChanged<String> onPhaseChange; // 阶段变化时通知(带参数的变体)
final ValueChanged<int>? onTick; // 每秒流逝时通知(带参数的变体)
const BreathingCircle({
super.key,
required this.pattern,
required this.totalMinutes,
required this.onComplete,
required this.onPhaseChange,
this.onTick,
});
State<BreathingCircle> createState() => BreathingCircleState();
}
class BreathingCircleState extends State<BreathingCircle>
with SingleTickerProviderStateMixin {
// ...
void _startTimer(int durationSec) {
_timer?.cancel();
_secondsInPhase = 0;
_timer = Timer.periodic(const Duration(seconds: 1), (timer) {
if (_isPaused) return;
_secondsInPhase++;
_totalElapsedSeconds++;
// 通知观察者1:每秒流逝
widget.onTick?.call(_totalElapsedSeconds);
// 检查总时长
final totalSec = widget.totalMinutes * 60;
if (_totalElapsedSeconds >= totalSec) {
timer.cancel();
_timer = null;
// 通知观察者2:练习完成
widget.onComplete();
return;
}
if (_secondsInPhase >= durationSec) {
_phaseIndex++;
// ...
// 通知观察者3:阶段改变
widget.onPhaseChange(widget.pattern.labelFor(nextPhase.phase));
}
});
}
}
在 BreathePage 中注册观察者:
// lib/pages/breathe/breathe_page.dart
BreathingCircle(
key: _circleKey,
pattern: _selectedPattern,
totalMinutes: _selectedMinutes,
// 注册观察者1:完成后弹出对话框
onComplete: () {
setState(() => _isRunning = false);
showDialog(...);
},
// 注册观察者2:阶段改变时更新文案
onPhaseChange: (phase) {
setState(() => _currentPhase = phase);
},
// 注册观察者3:每秒更新剩余时间
onTick: (elapsed) {
setState(() {
_remainingSeconds = (_selectedMinutes * 60) - elapsed;
if (_remainingSeconds < 0) _remainingSeconds = 0;
});
},
)
2.3 VoidCallback 模式的特征分析
| 维度 | 评价 |
|---|---|
| 代码量 | 最少(一行属性声明 + 一行调用) |
| 重建粒度 | 由观察者自己在 setState 中控制 |
| 内存占用 | 几乎为零(一个函数引用) |
| 适用场景 | 一对一通信,Widget 向 Parent 回传事件 |
| 局限 | 只能注册一个观察者;无生命周期管理;参数类型不统一 |
VoidCallback 模式最适合的场景是子 Widget 向父 Widget 的单向事件通知。它的局限也很明显:如果你想通知多个观察者,需要在 Subject 中维护一个回调列表,这就进入了手动观察者模式管理的领域——而 ChangeNotifier 已经为你做好了这件事。
三、实现二:ValueNotifier + ValueListenableBuilder
ValueNotifier<T> 是 Flutter 框架提供的最简单的响应式容器。它封装了一个单一的值 T,当这个值变化时,自动通知所有监听者。
3.1 基本原理
ValueNotifier 是 ChangeNotifier 的一个特化子类,源码极其精简:
// Flutter SDK 源码简化版
class ValueNotifier<T> extends ChangeNotifier implements ValueListenable<T> {
ValueNotifier(this._value);
T _value;
T get value => _value;
set value(T newValue) {
if (_value == newValue) return; // 值相同则不通知(性能优化)
_value = newValue;
notifyListeners();
}
String toString() => '${describeIdentity(this)}($value)';
}
关键点:当且仅当新值与旧值不同时,才调用 notifyListeners()。这个简单的优化避免了大量无意义的 UI 重建。
ValueListenableBuilder<T> 是与 ValueNotifier 配套的 Widget。它会自动注册监听,在值变化时重建其 builder。
3.2 在 E-Brufen 中的潜在应用场景
E-Brufen 中当前的 AppSettings 是一个纯数据类(不继承任何 Notifier),读取方式是 initState 中手动读取 + 手动 setState。这是一个适合用 ValueNotifier 优化的场景。
// 用 ValueNotifier 重写计时器时长设置(示例)
/// 单个设置的响应式容器
class TimerDurationSetting {
final ValueNotifier<int> duration;
TimerDurationSetting(int initialMinutes)
: duration = ValueNotifier(initialMinutes);
void update(int minutes) {
duration.value = minutes; // 自动触发通知(如果值确实变化了)
}
void dispose() {
duration.dispose();
}
}
// UI 侧使用 ValueListenableBuilder
Widget build(BuildContext context) {
return ValueListenableBuilder<int>(
valueListenable: timerSetting.duration,
builder: (context, minutes, child) {
return Wrap(spacing: 8, children: [
...durations.map((d) => ChoiceChip(
label: Text('$d分'),
selected: minutes == d,
onSelected: (_) => timerSetting.update(d),
)),
]);
},
);
}
ValueListenableBuilder 的核心优势是自动管理生命周期:它在 initState 时注册监听,在 dispose 时自动移除监听。你不需要手动写 addListener / removeListener。
3.3 特征分析
| 维度 | 评价 |
|---|---|
| 代码量 | 少(ValueNotifier 5 行 + ValueListenableBuilder 5 行) |
| 重建粒度 | 细(只重建 builder 包裹的部分,不触发整个页面 setState) |
| 内存占用 | 低(一个 ChangeNotifier 实例 + 一个泛型值) |
| 适用场景 | 单一值的变化监听(计数器、开关、主题模式) |
| 局限 | 只能通知单一值的变化;多个 ValueNotifier 需要嵌套使用 |
四、实现三:ChangeNotifier + ListenableBuilder
ChangeNotifier 是 Flutter 中最常用的观察者模式实现。与 ValueNotifier 不同,ChangeNotifier 不绑定单一值——它可以在任何数据变更后手动调用 notifyListeners()。
4.1 与 ValueNotifier 的区别
ValueNotifier<T> ChangeNotifier
───────────────── ──────────────
封装单个值 T 管理多个字段
set value 自动比较并通知 手动调用 notifyListeners()
适合计数器、开关 适合数据库、列表、复杂状态
自动判断值是否变化 需要自己判断是否需要通知
4.2 E-Brufen 中的实际代码
MoodStorage 是 E-Brufen 中使用 ChangeNotifier 的核心案例,完整代码已在 post-21 中详细分析。这里只关注其观察者模式相关的核心片段:
// lib/data/mood_storage.dart
import 'package:flutter/foundation.dart';
import 'package:hive_ce/hive.dart';
import '../models/mood_entry.dart';
class MoodStorage extends ChangeNotifier { // ① 继承
Box? _box;
int _nextId = 1;
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;
}
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(); // ② 删除后广播
}
List<MoodEntry> getAll() {
if (_box == null) return [];
// ... 遍历、排序、返回 ...
}
void dispose() {
_box?.close();
super.dispose(); // ③ 释放资源
}
}
UI 侧的监听模式(在 DiaryPage 中):
// 注册观察者模式三步走
void initState() {
super.initState();
widget.moodStorage.addListener(_loadMoods); // ① 订阅
_loadMoods(); // ② 首次加载
}
void _loadMoods() {
setState(() {
_allMoods = widget.moodStorage.getAll();
_weekMoods = widget.moodStorage.getByWeek(DateTime.now());
});
}
void dispose() {
widget.moodStorage.removeListener(_loadMoods); // ③ 取消订阅
super.dispose();
}
也可以使用 ListenableBuilder(Flutter 3.10+)来替代手动管理:
// 使用 ListenableBuilder —— 自动管理监听生命周期
Widget build(BuildContext context) {
return ListenableBuilder(
listenable: widget.moodStorage,
builder: (context, child) {
final moods = widget.moodStorage.getAll();
return ListView.builder(
itemCount: moods.length,
itemBuilder: (context, index) { /* ... */ },
);
},
);
}
ListenableBuilder 与 ValueListenableBuilder 的区别在于:前者接受任意 Listenable(包括 ChangeNotifier 和 ValueNotifier),后者只接受 ValueListenable<T>。如果你只是单值监听,ValueListenableBuilder 更精确;如果是多值或复杂状态,用 ListenableBuilder 或手动 addListener。
4.3 特征分析
| 维度 | 评价 |
|---|---|
| 代码量 | 中(需要手写 notifyListeners 调用点) |
| 重建粒度 | 粗(整个 listener 回调 rebuild,无法区分"哪个字段变了") |
| 内存占用 | 中(LinkedList 存储监听器) |
| 适用场景 | 多字段数据源(数据库、Repository、ViewModel) |
| 局限 | 无法传递"什么东西变了"的信息(拉模式,非推模式) |
五、实现四:Stream + StreamBuilder
Stream 是 Dart 语言内置的异步数据流机制。与 ChangeNotifier 的"拉模式"不同,Stream 是"推模式"——数据直接随着事件推送给监听者。
5.1 两个核心概念
在 Dart 中,Stream 分两类:
Dart Stream 类型
│
├── Single-subscription Stream(单订阅流)
│ │ 只能有一个监听者
│ │ 适用于:文件读取、HTTP 响应、一次性事件序列
│ │ 再次 listen() 会抛出 StateError
│ │
│ └── 示例:File.openRead()
│
└── Broadcast Stream(广播流)
│ 可以有多个监听者
│ 适用于:用户交互事件、状态变化、UI 事件
│ 通过 .asBroadcastStream() 或 StreamController.broadcast() 创建
│
└── 示例:OhosAudioPlayer.onPlaybackStateChanged
5.2 E-Brufen 中的实际代码
在 OhosAudioPlayer 中,我们使用了广播 Stream 来实现原生端到 Dart 端的播放状态通知:
// lib/data/ohos_audio.dart
import 'dart:async';
import 'package:flutter/services.dart';
class OhosAudioPlayer {
static const _channel = MethodChannel('com.ebrufen/audio_player');
// ① 创建广播 StreamController
static final _playbackController =
StreamController<PlaybackEvent>.broadcast();
// ② 暴露为只读 Stream
static final Stream<PlaybackEvent> onPlaybackStateChanged =
_playbackController.stream;
static bool _handlerRegistered = false;
// ③ 注册原生端回调
static void _ensureHandler() {
if (_handlerRegistered) return;
_handlerRegistered = true;
_channel.setMethodCallHandler((call) async {
switch (call.method) {
case 'onPlaybackStateChanged':
final state = (call.arguments as Map?)?.cast<String, dynamic>();
final stateStr = state?['state'] as String? ?? '';
// ④ 向 Stream 推送事件(观察者模式的通知步骤)
_playbackController.add(PlaybackEvent.fromString(stateStr));
break;
}
});
}
}
/// 播放状态事件
class PlaybackEvent {
final PlaybackState state;
const PlaybackEvent(this.state);
}
enum PlaybackState { playing, paused, stopped }
在 SoundscapePage 中订阅这个 Stream:
// lib/pages/soundscape/soundscape_page.dart
class _SoundscapePageState extends State<SoundscapePage> {
StreamSubscription<PlaybackEvent>? _playbackSub;
void initState() {
super.initState();
// ① 订阅广播 Stream
_playbackSub = OhosAudioPlayer.onPlaybackStateChanged.listen((event) {
if (!mounted) return;
// ② 收到事件后更新 UI 状态
switch (event.state) {
case PlaybackState.paused:
setState(() {
_isPlaying = false;
_pulse.stop();
_pulse.reset();
_countdown?.cancel();
});
break;
case PlaybackState.stopped:
setState(() {
_isPlaying = false;
_pulse.stop();
_pulse.reset();
_countdown?.cancel();
});
break;
case PlaybackState.playing:
setState(() {
_isPlaying = true;
_pulse.repeat(reverse: true);
_startCountdown();
});
break;
}
});
}
void dispose() {
_playbackSub?.cancel(); // ③ 取消订阅,释放资源
_pulse.dispose();
_countdown?.cancel();
_stopAudio();
super.dispose();
}
}
如果使用 StreamBuilder,可以用声明式的方式替代命令式的 listen:
// 使用 StreamBuilder 的声明式方式(替代手动 listen)
Widget build(BuildContext context) {
return StreamBuilder<PlaybackEvent>(
stream: OhosAudioPlayer.onPlaybackStateChanged,
initialData: const PlaybackEvent(PlaybackState.stopped),
builder: (context, snapshot) {
final isPlaying = snapshot.data?.state == PlaybackState.playing;
return FloatingActionButton.large(
onPressed: _togglePlay,
backgroundColor: isPlaying ? Colors.orange.shade300 : scene.color,
child: Icon(
isPlaying ? Icons.stop : Icons.play_arrow,
size: 36, color: Colors.white,
),
);
},
);
}
StreamBuilder 的优势是声明式——你不需要管理 StreamSubscription 的生命周期,Widget 框架帮你处理。但代价是不够灵活:如果你需要在收到事件后执行多个副作用(比如既更新 UI 又触发震动),手动 listen 更合适。
5.3 特征分析
| 维度 | 评价 |
|---|---|
| 代码量 | 中(需要 StreamController + Stream + StreamSubscription 三步) |
| 重建粒度 | 细(StreamBuilder 只重建 builder 包裹的部分) |
| 内存占用 | 中(StreamController 内部维护订阅列表和缓冲区) |
| 适用场景 | 异步事件流(网络状态、传感器数据、用户交互、原生端回调) |
| 局限 | Single-subscription Stream 只能有一个监听者;需要处理异步时序 |
六、实现五:BehaviorSubject(RxDart)
RxDart 是 Dart 的 Reactive Extensions 实现,它在原生 Stream 的基础上添加了丰富的操作符(Operators)。其中 BehaviorSubject 是最常用来替代 ValueNotifier 的组件——它是一个带有"初始值"和"最新值缓存"的广播 Stream。
6.1 与 ValueNotifier 的对比
ValueNotifier<T> BehaviorSubject<T>
───────────────── ─────────────────
value = x add(x)
读:notifier.value 读:subject.value(同步获取最新值)
watch:ValueListenableBuilder watch:StreamBuilder
本质:同步 本质:异步(Stream 体系)
无操作符 丰富的 Rx 操作符:
.map / .where / .debounceTime /
.distinct / .combineLatest / .switchMap
6.2 代码示例
import 'package:rxdart/rxdart.dart';
/// 用 BehaviorSubject 管理音频播放状态
class AudioStateManager {
// 带初始值的广播流
final _playbackState = BehaviorSubject<PlaybackState>.seeded(
PlaybackState.stopped,
);
// 只读流
Stream<PlaybackState> get playbackState$ => _playbackState.stream;
// 同步获取当前值
PlaybackState get currentState => _playbackState.value;
void updateState(PlaybackState state) {
_playbackState.add(state);
}
// 派生流:是否正在播放
Stream<bool> get isPlaying$ =>
_playbackState.stream.map((s) => s == PlaybackState.playing);
// 派生流:状态变化去抖(避免快速切换)
Stream<PlaybackState> get debouncedState$ =>
_playbackState.stream.debounceTime(const Duration(milliseconds: 300));
void dispose() {
_playbackState.close();
}
}
6.3 E-Brufen 项目中的适用性分析
在 E-Brufen 当前版本中,我们没有引入 RxDart。原因很简单:我们的异步数据流只有一个(OhosAudioPlayer.onPlaybackStateChanged),操作符需求为零。引入 RxDart 的代价(增加一个 pub 依赖、学习 Rx 操作符、增加 APK 体积)远大于收益。
但如果你面临以下场景,RxDart 的价值会迅速放大:
- 需要多个 Stream 的组合(比如:温度 + 湿度 → 体感指数)
- 需要去抖(debounce)或节流(throttle)处理用户快速输入
- 需要
combineLatest合并多个数据源
| 维度 | 评价 |
|---|---|
| 代码量 | 中-高(需要额外依赖和 Rx 操作符学习) |
| 重建粒度 | 细(声明式的 Stream 变换管道) |
| 内存占用 | 中-高(BehaviorSubject 持有最后一个值 + 订阅列表) |
| 适用场景 | 复杂的异步数据流管道、多源合并、去抖/节流 |
| 局限 | 需要额外依赖;过度使用操作符会让代码变得晦涩 |
七、实现六:InheritedWidget + dependOnInheritedWidgetOfExactType
InheritedWidget 是 Flutter 框架层级的观察者模式实现。它是 Provider 包的底层基石,也是 Theme.of(context) 和 MediaQuery.of(context) 等所有 of() 方法的幕后功臣。
7.1 基本原理
InheritedWidget 的核心机制是"向下传播 + 按需重建":
┌─────────────────────┐
│ InheritedWidget │
│ (放在 Widget 树顶部) │
│ │
│ 持有共享数据对象 │
└─────────┬───────────┘
│
┌───────────────┼───────────────┐
▼ ▼ ▼
Child A Child B Child C
(调用 of() 订阅) (调用 of() 订阅) (不订阅,不受影响)
│ │
│ 当数据变化 → A 和 B 重建,C 不受影响
关键方法有两个:
dependOnInheritedWidgetOfExactType<T>()—— 注册依赖关系。当 InheritedWidget 的updateShouldNotify返回true时,调用此方法的 Widget 会被标记为需要重建。getInheritedWidgetOfExactType<T>()—— 只获取数据,不注册依赖关系。用于事件处理器(onPressed 等)中,不需要后续自动重建。
7.2 E-Brufen 中的实际应用场景
在当前 E-Brufen 中,AppSettings 和 MoodStorage 通过构造函数一层层向下传递。如果有更多页面或更深的 Widget 树,这种"prop drilling"会变成负担。下面我们用 InheritedWidget 来改造全局主题设置。
首先,创建继承自 InheritedWidget 的响应式容器:
// lib/providers/ebrufen_scope.dart
import 'package:flutter/material.dart';
import '../data/settings.dart';
import '../data/mood_storage.dart';
/// E-Brufen 应用范围的 InheritedWidget
///
/// 替代构造函数传参,让 Widget 树中任意位置的子组件
/// 都能通过 EbrufenScope.of(context) 获取共享数据。
class EbrufenScope extends InheritedWidget {
final AppSettings settings;
final MoodStorage moodStorage;
const EbrufenScope({
super.key,
required this.settings,
required this.moodStorage,
required super.child,
});
/// 获取最近的 EbrufenScope 实例
///
/// 调用此方法会注册依赖关系 —— 当 scope 的数据变化时,
/// 调用此方法的 Widget 会自动重建。
static EbrufenScope of(BuildContext context) {
final scope = context.dependOnInheritedWidgetOfExactType<EbrufenScope>();
assert(scope != null, 'EbrufenScope 未在 Widget 树中找到');
return scope!;
}
/// 只获取数据,不注册依赖(用于事件回调中)
static EbrufenScope? maybeOf(BuildContext context) {
return context.dependOnInheritedWidgetOfExactType<EbrufenScope>();
}
/// 只获取数据,不订阅更新(用于一次性读取)
static EbrufenScope read(BuildContext context) {
final scope = context.getInheritedWidgetOfExactType<EbrufenScope>();
assert(scope != null, 'EbrufenScope 未在 Widget 树中找到');
return scope!;
}
bool updateShouldNotify(EbrufenScope oldWidget) {
// 当 settings 或 moodStorage 引用变化时通知
// 注意:这里是引用比较。如果数据在内部变化但引用不变,
// 需要配合 ChangeNotifier 使用。
return settings != oldWidget.settings
|| moodStorage != oldWidget.moodStorage;
}
}
然后,在 Widget 树顶部插入:
// lib/main.dart
void main() async {
// ...
final settings = AppSettings();
await settings.init();
final moodStorage = MoodStorage();
await moodStorage.init();
runApp(
EbrufenScope(
settings: settings,
moodStorage: moodStorage,
child: const EBrufenApp(), // EBrufenApp 内部可通过 EbrufenScope.of() 获取
),
);
}
class EBrufenApp extends StatelessWidget {
const EBrufenApp({super.key});
Widget build(BuildContext context) {
// 通过 InheritedWidget 获取数据,不再需要构造函数传参
final scope = EbrufenScope.of(context);
return MaterialApp(
title: 'E-Brufen',
home: HomePage(
moodStorage: scope.moodStorage,
settings: scope.settings,
),
);
}
}
现在,在 Widget 树的任意位置都可以获取这些共享数据,而不需要一层层地传递构造函数参数。
重要注意:仅使用 InheritedWidget 无法实现响应式更新。因为 updateShouldNotify 做的是引用比较——只有当 settings 或 moodStorage 对象本身被替换(new 一个新的实例),才会触发重建。在 E-Brufen 的实际架构中,MoodStorage 是一个单例,引用不会变化。因此,在实际项目中,InheritedWidget 通常与 ChangeNotifier 配合使用(这正是 Provider 包的原理):
// InheritedWidget + ChangeNotifier 组合(简化版 Provider 的思路)
class EbrufenScope extends InheritedNotifier<MoodStorage> {
const EbrufenScope({
super.key,
required MoodStorage notifier,
required super.child,
}) : super(notifier: notifier);
static MoodStorage of(BuildContext context) {
return context
.dependOnInheritedWidgetOfExactType<EbrufenScope>()!
.notifier!;
}
}
InheritedNotifier 是 Flutter 提供的组合类——它既是一个 InheritedWidget,又自动监听一个 ChangeNotifier。当 ChangeNotifier 的 notifyListeners() 被调用时,InheritedNotifier 会自动触发依赖它的 Widget 重建。
7.3 特征分析
| 维度 | 评价 |
|---|---|
| 代码量 | 高(需要额外创建 InheritedWidget 子类,编写 of/read 方法) |
| 重建粒度 | 中(依赖它的 Widget 全量重建,但非依赖的 Widget 不受影响) |
| 内存占用 | 低(InheritedWidget 本身很轻量) |
| 适用场景 | 全局配置(主题、语言、用户信息)、需要跨多层 Widget 访问的数据 |
| 局限 | 需要放在 Widget 树中才能使用;单独使用无法响应数据内部变化 |
八、六种实现全景对比表
经过以上逐一分析,下面是六种观察者模式实现的系统对比:
| 维度 | VoidCallback | ValueNotifier | ChangeNotifier | Stream | BehaviorSubject | InheritedWidget |
|---|---|---|---|---|---|---|
| 代码量 | 最少 | 少 | 中 | 中 | 中-高 | 高 |
| 命名 | 极简 | 值监听 | 变化通知 | 异步流 | Rx流 | 框架级 |
| 最大观察者数 | 1 | 无限 | 无限 | 单/无限 | 无限 | 无限 |
| 数据传递方式 | 推 | 拉 | 拉 | 推 | 推 | 拉 |
| 内置生命周期管理 | 无 | 自动(ValueListenableBuilder) | 手动 | 手动(listen)/自动(StreamBuilder) | 手动(listen)/自动(StreamBuilder) | 自动(dependOn) |
| 重建粒度控制 | 手动 | 细 | 粗 | 细 | 细 | 中 |
| async 支持 | 不适用 | 不适用 | 不适用 | 原生支持 | 深度支持 | 不适用 |
| Dart 类型安全 | 无泛型 | 有泛型 | 无泛型 | 有泛型 | 有泛型 | 有泛型 |
| 需要额外依赖 | 0 | 0 | 0 | 0 | rxdart | 0 |
| 学习曲线 | 零 | 低 | 低 | 中 | 高 | 中 |
| E-Brufen 使用情况 | 大量使用 | 未使用 | 核心(MoodStorage) | 核心(音频状态) | 未使用 | 未使用 |
| 适合的数据规模 | 单值事件 | 单值 | 多值集合 | 事件序列 | 复杂流管道 | 常量/配置 |
| 最佳场景 | 子→父 事件回传 | 开关/计数器 | 数据仓库 | 原生端回调 | 多流组合 | 全局配置注入 |
九、实战一:MoodStorage 的三种实现方式对比
为了更直观地展示不同实现之间的差异,让我们用三种不同的观察者模式来实现同一个 MoodStorage 的核心功能。
9.1 版本 A:ChangeNotifier(当前 E-Brufen 实现)
// 数据层 —— 完整代码
class MoodStorage extends ChangeNotifier {
Box? _box;
int _nextId = 1;
Future<int> insert(MoodEntry entry) async {
final id = _nextId++;
await _box?.put(id, jsonEncode(entry.toJson()..['id'] = id));
notifyListeners(); // 写入后广播
return id;
}
List<MoodEntry> getAll() { /* ... 遍历、解析、排序 ... */ }
}
// UI 层 —— 三步监听
class _DiaryPageState extends State<DiaryPage> {
void initState() {
super.initState();
widget.moodStorage.addListener(_loadMoods); // ① 注册
_loadMoods(); // ② 首加载
}
void _loadMoods() {
setState(() => _allMoods = widget.moodStorage.getAll());
}
void dispose() {
widget.moodStorage.removeListener(_loadMoods); // ③ 解绑
super.dispose();
}
}
9.2 版本 B:Stream
// 数据层 —— 用 StreamController 替代 ChangeNotifier
class MoodStorage {
Box? _box;
int _nextId = 1;
// 广播 Stream,支持多个观察者
final _changeController = StreamController<void>.broadcast();
Stream<void> get onChange => _changeController.stream;
Future<int> insert(MoodEntry entry) async {
final id = _nextId++;
await _box?.put(id, jsonEncode(entry.toJson()..['id'] = id));
_changeController.add(null); // 推送空事件通知
return id;
}
List<MoodEntry> getAll() { /* ... */ }
void dispose() {
_changeController.close();
_box?.close();
}
}
// UI 层 —— 用 StreamBuilder 替代手动 addListener
class DiaryPage extends StatelessWidget {
final MoodStorage moodStorage;
const DiaryPage({super.key, required this.moodStorage});
Widget build(BuildContext context) {
return StreamBuilder<void>(
stream: moodStorage.onChange,
builder: (context, snapshot) {
final moods = moodStorage.getAll();
return ListView.builder(
itemCount: moods.length,
itemBuilder: (context, index) {
return MoodCard(entry: moods[index]);
},
);
},
);
}
}
Stream 版本的优势是 StreamBuilder 自动管理生命周期,且 DiaryPage 可以是 StatelessWidget。但代价是数据层多了 StreamController 的管理代码。
9.3 版本 C:VoidCallback(最简版)
// 数据层 —— 最简实现
class MoodStorage {
Box? _box;
int _nextId = 1;
VoidCallback? onChanged; // 只有一个观察者
Future<int> insert(MoodEntry entry) async {
final id = _nextId++;
await _box?.put(id, jsonEncode(entry.toJson()..['id'] = id));
onChanged?.call();
return id;
}
List<MoodEntry> getAll() { /* ... */ }
}
// UI 层
class _DiaryPageState extends State<DiaryPage> {
void initState() {
super.initState();
widget.moodStorage.onChanged = _loadMoods; // 直接赋值
_loadMoods();
}
void _loadMoods() {
setState(() => _allMoods = widget.moodStorage.getAll());
}
void dispose() {
widget.moodStorage.onChanged = null; // 清除引用
super.dispose();
}
}
最简版的问题很明显:只能有一个观察者。如果后续需要在首页也监听心情变化,就需要改造为列表模式,最终回到 ChangeNotifier 的方案。
9.4 三版本演进分析
VoidCallback 版 Stream 版 ChangeNotifier 版
───────────────── ───────────────── ──────────────────
代码最少 代码中等 代码适中
1 个观察者 无限观察者 无限观察者
手动生命周期 StreamBuilder 自动 手动 addListener
无依赖 无依赖 无依赖
演进方向 ─────────────────────────────►
(观察者增多 → 需要广播能力)
十、实战二:AppSettings 用 ValueNotifier 重构
E-Brufen 当前的 AppSettings 是一个纯数据类,页面在 initState 中读取配置,配置变化时需要手动 setState。这种"被动读取"的模式在数据变更不频繁时足够用,但当我们需要多个 Widget 响应同一个配置变化时,ValueNotifier 会是更优雅的方案。
10.1 当前 AppSettings 的问题
// 当前实现 —— 纯数据类,无观察者
class AppSettings {
Box? _box;
String get breatheMode {
if (_box == null || !_box!.isOpen) return '盒式呼吸';
return _box!.get('breatheMode', defaultValue: '盒式呼吸');
}
set breatheMode(String v) {
if (_box != null && _box!.isOpen) _box!.put('breatheMode', v);
// ⚠️ 问题:UI 不知道数据变了!需要调用者手动 setState
}
}
10.2 用 ValueNotifier 重构单个设置
import 'package:flutter/foundation.dart';
import 'package:hive_ce_flutter/hive_flutter.dart';
/// 响应式设置容器(替代 AppSettings 的被动读取模式)
class ReactiveSetting<T> {
final ValueNotifier<T> notifier;
final String _key;
final Box _box;
final T _defaultValue;
ReactiveSetting({
required String key,
required Box box,
required T defaultValue,
}) : _key = key,
_box = box,
_defaultValue = defaultValue,
notifier = ValueNotifier(
box.get(key, defaultValue: defaultValue) as T,
);
T get value => notifier.value;
set value(T v) {
_box.put(_key, v);
notifier.value = v; // ValueNotifier 自动比较旧值,相同则不通知
}
void dispose() {
notifier.dispose();
}
}
/// 重构后的 AppSettings —— 每个设置项独立响应
class AppSettings {
Box? _box;
late ReactiveSetting<String> _breatheMode;
late ReactiveSetting<int> _breatheMinutes;
late ReactiveSetting<String> _selectedScene;
late ReactiveSetting<int> _timerDuration;
Future<void> init() async {
_box = await Hive.openBox('settings');
_breatheMode = ReactiveSetting<String>(
key: 'breatheMode', box: _box!, defaultValue: '盒式呼吸',
);
_breatheMinutes = ReactiveSetting<int>(
key: 'breatheMinutes', box: _box!, defaultValue: 3,
);
_selectedScene = ReactiveSetting<String>(
key: 'selectedScene', box: _box!, defaultValue: '雨中办公',
);
_timerDuration = ReactiveSetting<int>(
key: 'timerDuration', box: _box!, defaultValue: 30,
);
}
// 暴露 ValueNotifier 供 UI 监听
ValueNotifier<String> get breatheMode => _breatheMode.notifier;
ValueNotifier<int> get breatheMinutes => _breatheMinutes.notifier;
// 便捷存取(保持原有 API 兼容)
String get breatheModeValue => _breatheMode.value;
set breatheModeValue(String v) => _breatheMode.value = v;
int get breatheMinutesValue => _breatheMinutes.value;
set breatheMinutesValue(int v) => _breatheMinutes.value = v;
void dispose() {
_breatheMode.dispose();
_breatheMinutes.dispose();
_selectedScene.dispose();
_timerDuration.dispose();
_box?.close();
}
}
10.3 UI 侧使用 ValueListenableBuilder
// BreathePage 中 —— 局部精确重建,不触发整个页面 rebuild
Widget _buildModeSelector() {
return ValueListenableBuilder<String>(
valueListenable: widget.settings.breatheMode,
builder: (context, mode, child) {
return Column(
children: BreathePatterns.all.map((pattern) => Card(
child: RadioListTile<BreathePattern>(
value: pattern,
groupValue: BreathePatterns.all.firstWhere(
(p) => p.name == mode,
orElse: () => BreathePatterns.box,
),
title: Text(pattern.name),
onChanged: (v) {
if (v != null) {
widget.settings.breatheModeValue = v.name;
}
},
),
)).toList(),
);
},
);
}
10.4 单值 → 多值的迁移模式总结
阶段一:AppSettings(纯数据类)
│ 被动读取 + 手动 setState
│ 适合:1-2 个页面,低频变更
│
阶段二:AppSettings + ValueNotifier
│ 每个设置项独立响应
│ ValueListenableBuilder 局部重建
│ 适合:3-5 个页面需要响应同一设置
│
阶段三:AppSettings + ChangeNotifier
│ 多个字段合并为一个通知
│ ListenableBuilder 整体重建
│ 适合:多个设置项经常联动变化
十一、实战三:全局主题切换用 InheritedWidget 实现
E-Brufen 目前使用 AppTheme.materialTheme 作为全局单例主题。如果我们想支持用户在"浅色"和"深色"之间切换,用 InheritedWidget 是最高效的方式。
// lib/providers/theme_provider.dart
import 'package:flutter/material.dart';
/// 主题模式枚举
enum AppThemeMode { light, dark }
/// 全局主题 InheritedNotifier
///
/// 使用 InheritedNotifier 而不是 InheritedWidget,
/// 是因为 InheritedNotifier 自动绑定一个 ChangeNotifier,
/// 当 notifier 通知变化时,依赖它的 Widget 自动重建。
class ThemeScope extends InheritedNotifier<ValueNotifier<AppThemeMode>> {
const ThemeScope({
super.key,
required ValueNotifier<AppThemeMode> notifier,
required super.child,
}) : super(notifier: notifier);
/// 获取当前主题模式(订阅模式 —— 主题变化时自动重建)
static AppThemeMode of(BuildContext context) {
return context
.dependOnInheritedWidgetOfExactType<ThemeScope>()!
.notifier!
.value;
}
/// 获取 notifier 用于切换主题(不触发当前 Widget 重建)
static ValueNotifier<AppThemeMode> controllerOf(BuildContext context) {
return context
.getInheritedWidgetOfExactType<ThemeScope>()!
.notifier!;
}
}
/// 使用示例
// main.dart
void main() {
final themeNotifier = ValueNotifier(AppThemeMode.light);
runApp(
ThemeScope(
notifier: themeNotifier,
child: const EBrufenApp(),
),
);
}
class EBrufenApp extends StatelessWidget {
const EBrufenApp({super.key});
Widget build(BuildContext context) {
final themeMode = ThemeScope.of(context);
return MaterialApp(
title: 'E-Brufen',
themeMode: themeMode == AppThemeMode.light
? ThemeMode.light
: ThemeMode.dark,
theme: AppTheme.materialTheme,
darkTheme: AppTheme.darkTheme,
home: const HomePage(),
);
}
}
// 任意页面中切换主题
class SettingsPage extends StatelessWidget {
const SettingsPage({super.key});
Widget build(BuildContext context) {
final controller = ThemeScope.controllerOf(context); // 用 controllerOf 取,不订阅
return SwitchListTile(
title: const Text('深色模式'),
value: controller.value == AppThemeMode.dark,
onChanged: (isDark) {
controller.value = isDark
? AppThemeMode.dark
: AppThemeMode.light;
// 不需要 setState —— ValueNotifier 自动触发 InheritedNotifier 重建
},
);
}
}
核心细节在于 of() 和 controllerOf() 的区别:of() 调用 dependOnInheritedWidgetOfExactType,会注册依赖关系,主题变化时 Widget 自动重建;controllerOf() 调用 getInheritedWidgetOfExactType,只获取对象,不注册依赖,适合在事件处理器中使用。
十二、决策树:选择正确的观察者实现
面对六种实现方式,如何选择?以下是基于 E-Brufen 实际开发经验总结的决策树:
需要观察者模式?
│
┌───────────┴───────────┐
│ 否 │ 是
▼ ▼
直接用 数据是同步还是异步?
setState │
┌──────────┴──────────┐
│ 同步 │ 异步
▼ ▼
几个 Widget 需要监听? 事件流是否复杂?
│ (需要去抖/合并/变换?)
┌───────┬───┴───┬───────┐ │
│ 1个 │ 2-5个 │ 5+个 │ ┌────┴────┐
▼ ▼ ▼ │ │ 简单 │ 复杂
VoidCallback │ Inherited │ ▼ ▼
或 │ Widget │ Stream Behavior
ValueNotifier │ │ 或原生 Subject
│ │ Stream (RxDart)
▼ │
监听的数据形态? │
│ │
┌────────┴────────┐ │
│ 单值 │ 多值 │
▼ ▼ │
ValueNotifier ChangeNotifier
+ ValueListenableBuilder
速查法则:
| 条件 | 推荐方案 |
|---|---|
| 子 Widget 向父 Widget 回传事件 | VoidCallback |
| 单个开关/计数器/选项值 | ValueNotifier<T> |
| 数据库/列表/复杂业务状态 | ChangeNotifier |
| 原生端事件回调/传感器数据 | Stream |
| 多流组合/去抖/节流 | BehaviorSubject (RxDart) |
| 全局主题/语言/用户配置 | InheritedWidget + ValueNotifier |
十三、常见陷阱与避坑指南
13.1 陷阱一:忘记取消 Stream 订阅导致内存泄漏
// ❌ 错误:订阅后从未 cancel
void initState() {
super.initState();
OhosAudioPlayer.onPlaybackStateChanged.listen((event) {
setState(() { ... });
});
// 这个 StreamSubscription 永远不会被取消!
// Widget 销毁后,回调仍在触发,调用 setState 会报错
}
// ✅ 正确:保存引用,在 dispose 中取消
StreamSubscription<PlaybackEvent>? _sub;
void initState() {
super.initState();
_sub = OhosAudioPlayer.onPlaybackStateChanged.listen((event) {
if (!mounted) return;
setState(() { ... });
});
}
void dispose() {
_sub?.cancel(); // 必须取消
super.dispose();
}
// ✅ 更好:用 StreamBuilder,框架自动管理
StreamBuilder<PlaybackEvent>(
stream: OhosAudioPlayer.onPlaybackStateChanged,
builder: (context, snapshot) { ... },
)
13.2 陷阱二:Stream 的 single-subscription 限制
// ❌ 错误:对单订阅流调用两次 listen()
final stream = File('data.txt').openRead(); // 单订阅流
stream.listen((data) { /* 监听者 1 */ });
stream.listen((data) { /* 监听者 2 */ }); // StateError!
// ✅ 正确:使用 asBroadcastStream() 转换为广播流
final broadcastStream = stream.asBroadcastStream();
broadcastStream.listen((data) { /* 监听者 1 */ });
broadcastStream.listen((data) { /* 监听者 2 */ });
// ✅ 更好:创建时就使用广播模式
final controller = StreamController.broadcast(); // 明确指定广播
13.3 陷阱三:ChangeNotifier 在 build 中注册监听
// ❌ 错误:在 build() 中 addListener
Widget build(BuildContext context) {
widget.moodStorage.addListener(_loadMoods); // 每次 rebuild 都注册一次!
// 第一次 build:监听器数 = 1
// 第一次 notifyListeners 触发 rebuild → 第二次 build:监听器数 = 2
// ... 指数增长 ...
}
// ✅ 正确:只在 initState 中注册一次
void initState() {
super.initState();
widget.moodStorage.addListener(_loadMoods);
}
13.4 陷阱四:notifyListeners 在异步操作之前调用
// ❌ 错误:先通知,后写入 —— UI 刷新时数据还没落地
Future<void> insert(MoodEntry entry) async {
notifyListeners(); // ← 时机太早!
final id = _nextId++;
await _box?.put(id, jsonEncode(data));
}
// ✅ 正确:写入完成后再通知
Future<void> insert(MoodEntry entry) async {
final id = _nextId++;
await _box?.put(id, jsonEncode(data));
notifyListeners(); // ← 数据已持久化,UI 可以安全读取
}
13.5 陷阱五:InheritedWidget 的 updateShouldNotify 过于宽松
// ❌ 错误:始终返回 true —— 每次父 Widget rebuild 都触发所有子 Widget 重建
bool updateShouldNotify(covariant EbrufenScope oldWidget) {
return true; // 性能杀手
}
// ✅ 正确:仅在数据真正变化时返回 true
bool updateShouldNotify(covariant EbrufenScope oldWidget) {
return settings != oldWidget.settings
|| moodStorage != oldWidget.moodStorage;
}
13.6 陷阱六:ValueNotifier 的 value setter 不触发通知
// ⚠️ 注意:ValueNotifier 内部做了相等性检查
final notifier = ValueNotifier<int>(5);
notifier.value = 5; // 不触发通知(新值 == 旧值)
notifier.value = 6; // 触发通知(6 != 5)
// 如果你需要"强制通知"(即使值相同),使用:
// 方案 A:直接调用 notifyListeners()
notifier.notifyListeners(); // 绕过值比较
// 方案 B:使用 ChangeNotifier 而非 ValueNotifier
// ChangeNotifier 没有值比较逻辑,每次调用都会通知
陷阱速查表:
| 陷阱 | 症状 | 根因 | 修复 |
|---|---|---|---|
| 未取消 Stream 订阅 | dispose 后 setState 报错 | StreamSubscription 未 cancel | dispose 中 .cancel() |
| Single-subscription 冲突 | StateError: Stream already listened | 对单订阅流多次 listen | 使用 asBroadcastStream() |
| build 中 addListener | 监听器指数增长,内存泄漏 | 每次 rebuild 重复注册 | 移到 initState |
| 通知在写入前 | UI 读到旧数据 | notifyListeners 调用时机错误 | await 之后调用 |
| updateShouldNotify=true | 不必要的 Widget 重建 | 未做数据比较 | 返回数据是否变化的布尔值 |
| ValueNotifier 值不变 | 调用 setter 但 UI 不更新 | 内部相等性检查拦截 | 确认值确实变了,或手动 notifyListeners |
十四、总结
本文从 E-Brufen 项目的实际代码出发,系统地梳理了 Flutter 中观察者模式的六种实现方式。它们从简到繁,覆盖了从"一对一回调"到"框架级广播"的全部场景。
核心观点:
- 没有"最好"的实现,只有"最合适"的实现。 VoidCallback 在单一事件回传场景下比 ChangeNotifier 更简洁;Stream 在处理异步事件时比 ValueNotifier 更自然。选择的关键不是"谁更强大",而是"谁更匹配当前需求"。
- 从简单开始,等复杂度自然增长后再升级。 E-Brufen 的演进路径完美诠释了这一点:最初只用 VoidCallback 处理子组件事件 → 发现需要跨页面刷新心情数据,引入 ChangeNotifier → 原生音频控制需要异步事件流,引入 Stream → 未来主题切换可能引入 InheritedWidget。每一步升级都是需求驱动的,而非预先设计。
- 生命周期管理是观察者模式的第一纪律。 无论哪种实现,忘记解绑都是最常见的内存泄漏源。Stream 的
cancel()、ChangeNotifier 的removeListener()、ValueListenableBuilder 和 StreamBuilder 的自动管理——它们都是为了防止"幽灵监听者"在 Widget 销毁后继续触发回调。 - InheritedWidget + ChangeNotifier 是 Provider 的底层原理。 如果你理解了本文中的
EbrufenScope和ThemeScope实现,你就理解了 Provider 的核心机制。Provider 只是在 InheritedNotifier 的基础上包装了一层更友好的 API ——context.watch()、context.read()、context.select()—— 本质上就是我们在实战三中写的代码。
一句话总结:
观察者模式是 Flutter 状态管理的地基。理解 VoidCallback → ValueNotifier → ChangeNotifier → Stream → InheritedWidget 这一条链路,你就理解了从零搭建一个可扩展的 Flutter 应用需要掌握的所有观察者实现。
作者简介
E-Brufen Dev,Flutter 和鸿蒙(HarmonyOS)开发者。专注于跨平台移动应用开发,致力于将 Flutter 生态引入鸿蒙平台。E-Brufen 情绪健康应用作者,AtomGit Flutter 鸿蒙客户端项目维护者。
- AtomGit 项目主页:https://atomgit.com/e-brufen/firstproject
- CSDN 博客:关注鸿蒙 Flutter 实战系列,每周更新深度技术文章
更多推荐


所有评论(0)