Flutter 中实现观察者模式的全景图——选择最适合你场景的方案


目录

  1. 观察者模式的本质:一对多依赖关系
  2. 实现一:VoidCallback 回调函数
  3. 实现二:ValueNotifier + ValueListenableBuilder
  4. 实现三:ChangeNotifier + ListenableBuilder
  5. 实现四:Stream + StreamBuilder
  6. 实现五:BehaviorSubject(RxDart)
  7. 实现六:InheritedWidget + dependOnInheritedWidgetOfExactType
  8. 六种实现全景对比表
  9. 实战一:MoodStorage 的三种实现方式对比
  10. 实战二:AppSettings 用 ValueNotifier 重构
  11. 实战三:全局主题切换用 InheritedWidget 实现
  12. 决策树:选择正确的观察者实现
  13. 常见陷阱与避坑指南
  14. 总结

一、观察者模式的本质:一对多依赖关系

观察者模式(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 基本原理

ValueNotifierChangeNotifier 的一个特化子类,源码极其精简:

// 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) { /* ... */ },
      );
    },
  );
}

ListenableBuilderValueListenableBuilder 的区别在于:前者接受任意 Listenable(包括 ChangeNotifierValueNotifier),后者只接受 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 中,AppSettingsMoodStorage 通过构造函数一层层向下传递。如果有更多页面或更深的 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 做的是引用比较——只有当 settingsmoodStorage 对象本身被替换(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 中观察者模式的六种实现方式。它们从简到繁,覆盖了从"一对一回调"到"框架级广播"的全部场景。

核心观点

  1. 没有"最好"的实现,只有"最合适"的实现。 VoidCallback 在单一事件回传场景下比 ChangeNotifier 更简洁;Stream 在处理异步事件时比 ValueNotifier 更自然。选择的关键不是"谁更强大",而是"谁更匹配当前需求"。
  2. 从简单开始,等复杂度自然增长后再升级。 E-Brufen 的演进路径完美诠释了这一点:最初只用 VoidCallback 处理子组件事件 → 发现需要跨页面刷新心情数据,引入 ChangeNotifier → 原生音频控制需要异步事件流,引入 Stream → 未来主题切换可能引入 InheritedWidget。每一步升级都是需求驱动的,而非预先设计。
  3. 生命周期管理是观察者模式的第一纪律。 无论哪种实现,忘记解绑都是最常见的内存泄漏源。Stream 的 cancel()、ChangeNotifier 的 removeListener()、ValueListenableBuilder 和 StreamBuilder 的自动管理——它们都是为了防止"幽灵监听者"在 Widget 销毁后继续触发回调。
  4. InheritedWidget + ChangeNotifier 是 Provider 的底层原理。 如果你理解了本文中的 EbrufenScopeThemeScope 实现,你就理解了 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 鸿蒙客户端项目维护者。


Logo

作为“人工智能6S店”的官方数字引擎,为AI开发者与企业提供一个覆盖软硬件全栈、一站式门户。

更多推荐