目标
把 响应式编程 与 Clean Architecture 、MVU(Model--View--Update) 三者打通,构建一条:
事件 → 用例(业务)→ 状态变更 → UI 响应 的单向数据流,做到可维护、可测试、可扩展。
一、核心思想一图流
┌──────────────┐
│ View(UI) │ 用户操作/系统事件
└──────┬───────┘
│ Intent / Event
▼
┌──────────────┐
│ Update │ 纯函数:State × Event → State(+ SideEffects)
└──────┬───────┘
│ 触发 UseCase / Repo(副作用)
▼
┌──────────────┐
│ UseCases │ 业务用例(领域规则)
└──────┬───────┘
│ 调用
▼
┌──────────────┐
│ Repositories │ I/O(API、DB、MQTT、蓝牙...)
└──────┬───────┘
│ 结果
▼
┌──────────────┐
│ State │ 不可变/可比较,驱动 UI
└──────────────┘
MVU 保证"状态 = UI 的唯一来源";
Clean Architecture 把副作用与领域规则分层解耦。
二、目录分层(按 Clean Architecture)
lib/
├─ app/ # app壳:路由/主题/依赖注入
├─ core/ # 通用:错误、结果、日志、网络配置
├─ features/
│ └─ robot/ # 功能域示例:机器人
│ ├─ domain/ # 领域层:实体/仓库接口/用例
│ │ ├─ entities/
│ │ ├─ repositories/
│ │ └─ usecases/
│ ├─ data/ # 数据层:repo实现/remote/local
│ └─ presentation/ # 表现层:MVU(State/Action/Update/View)
└─ main.dart
三、状态模型(MVU 的 "M")
State 要求 :不可变(freezed 推荐)、可比较(便于 diff)、易测试。
Dart
// features/robot/presentation/robot_state.dart
import 'package:freezed_annotation/freezed_annotation.dart';
part 'robot_state.freezed.dart';
@freezed
class RobotState with _$RobotState {
const factory RobotState({
@Default(false) bool connecting,
@Default(false) bool moving,
@Default(0) int progress,
String? error,
@Default([]) List<Waypoint> waypoints,
}) = _RobotState;
const RobotState._();
bool get idle => !connecting && !moving;
}
四、动作与意图(MVU 的 "V→U")
将 UI 操作与系统事件统一成 Action/Event:
Dart
// features/robot/presentation/robot_action.dart
sealed class RobotAction {}
class ConnectRequested extends RobotAction {}
class MoveToRequested extends RobotAction { final Waypoint dest; MoveToRequested(this.dest); }
class CancelMoveRequested extends RobotAction {}
class WaypointsLoaded extends RobotAction { final List<Waypoint> list; WaypointsLoaded(this.list); }
class ProgressUpdated extends RobotAction { final int percent; ProgressUpdated(this.percent); }
class Failed extends RobotAction { final String msg; Failed(this.msg); }
五、Update:纯函数(MVU 的 "U" 核心)
update(state, action) => newState + sideEffects
Dart
typedef SideEffect = Future<void> Function(Dispatch dispatch);
RobotState update(RobotState s, RobotAction a, List<SideEffect> effects) {
switch (a) {
case ConnectRequested():
effects.add((dispatch) async {
final ok = await usecases.connect(); // 副作用: 调用用例
if (!ok) dispatch(Failed('连接失败'));
});
return s.copyWith(connecting: true, error: null);
case MoveToRequested(:final dest):
effects.add((dispatch) async {
final res = await usecases.moveTo(dest);
res.fold(
(err) => dispatch(Failed(err.message)),
(_) {},
);
});
return s.copyWith(moving: true, progress: 0, error: null);
case CancelMoveRequested():
effects.add((_) => usecases.cancelMove());
return s.copyWith(moving: false);
case ProgressUpdated(:final percent):
return s.copyWith(progress: percent, moving: percent < 100);
case WaypointsLoaded(:final list):
return s.copyWith(waypoints: list);
case Failed(:final msg):
return s.copyWith(error: msg, connecting: false, moving: false);
}
}
要点 :Update 只产出新状态 与副作用列表 ;副作用执行完成后继续
dispatch(...)回流,形成单向循环。
六、用例层(Clean 的 "UseCases")
Dart
// domain/usecases/move_to.dart
class MoveTo {
final RobotRepository repo;
MoveTo(this.repo);
Future<Either<Failure, Unit>> call(Waypoint dest) async {
if (!await repo.connected) return left(Failure('未连接'));
return repo.moveTo(dest.id);
}
}
仓库接口 在 domain 层定义,实现在 data 层完成(HTTP/MQTT/DB)。
七、响应式接入(Stream:进度、MQTT、蓝牙...)
将外部流接入 Action,以保持 MVU 的单向性。
Dart
class RobotEffects {
final MqttBus mqtt;
final MoveTo moveTo;
final CancelMove cancelMove;
final Connect connect;
RobotEffects(this.mqtt, this.moveTo, this.cancelMove, this.connect);
// 订阅外部事件流,转为 Action
Stream<RobotAction> bindExternal() => mqtt.messages.map((m) {
if (m.type == 'progress') return ProgressUpdated(m.percent);
if (m.type == 'waypoints') return WaypointsLoaded(m.list);
return Failed('未知消息');
});
}
八、落地实现 1:Riverpod 版 MVU
Dart
// Provider 壳
final effectsProvider = Provider<RobotEffects>((ref) => RobotEffects(
ref.read(mqttBusProvider),
ref.read(moveToProvider),
ref.read(cancelMoveProvider),
ref.read(connectProvider),
));
// MVU Notifier
class RobotStore extends StateNotifier<RobotState> {
RobotStore(this._effects): super(const RobotState()) {
_external = _effects.bindExternal().listen(dispatch);
}
final RobotEffects _effects;
late final StreamSubscription _external;
void dispatch(RobotAction action) async {
final effects = <SideEffect>[];
state = update(state, action, effects);
for (final fx in effects) { await fx(dispatch); }
}
@override
void dispose() { _external.cancel(); super.dispose(); }
}
final robotStoreProvider =
StateNotifierProvider<RobotStore, RobotState>((ref) => RobotStore(ref.read(effectsProvider)));
View:
Dart
class RobotPage extends ConsumerWidget {
const RobotPage({super.key});
@override
Widget build(BuildContext _, WidgetRef ref) {
final s = ref.watch(robotStoreProvider);
final dispatch = ref.read(robotStoreProvider.notifier).dispatch;
return Column(
children: [
if (s.connecting) const LinearProgressIndicator(),
if (s.error != null) Text('❌ ${s.error}'),
Text('进度:${s.progress}%'),
Wrap(children: [
ElevatedButton(onPressed: () => dispatch(ConnectRequested()), child: const Text('连接')),
ElevatedButton(
onPressed: s.idle ? null : () => dispatch(CancelMoveRequested()),
child: const Text('取消移动'),
),
]),
for (final w in s.waypoints)
ListTile(title: Text(w.name), onTap: () => dispatch(MoveToRequested(w))),
],
);
}
}
九、落地实现 2:Bloc 版 MVU(事件即 Action)
Dart
class RobotBloc extends Bloc<RobotAction, RobotState> {
RobotBloc(this.effects): super(const RobotState()) {
on<RobotAction>((action, emit) async {
final fx = <SideEffect>[];
emit(update(state, action, fx));
for (final f in fx) { await f(add); } // add 即 dispatch
});
// 外部流接入
_sub = effects.bindExternal().listen(add);
}
final RobotEffects effects;
late final StreamSubscription _sub;
@override Future<void> close() { await _sub.cancel(); return super.close(); }
}
View:
Dart
BlocProvider(
create: (_) => RobotBloc(context.read<RobotEffects>()),
child: BlocBuilder<RobotBloc, RobotState>(
builder: (_, s) => /* 同上渲染 */,
),
);
十、测试策略(纯 Dart、可回归)
Update 纯函数单测:
Dart
test('MoveToRequested sets moving and resets progress', () {
final s0 = const RobotState();
final effects = <SideEffect>[];
final s1 = update(s0, MoveToRequested(Waypoint('A', 'idA')), effects);
expect(s1.moving, true);
expect(s1.progress, 0);
expect(effects, isNotEmpty); // 会触发用例副作用
});
UseCase 单测 :mock Repository,断言返回 Either。
Store/Bloc 测试 :Riverpod 用 ProviderContainer + override;Bloc 用 blocTest.
十一、性能与工程化要点
-
不可变状态 + 拆分 Widget:只重建必要节点。
-
Stream 频繁 → debounce/throttle(如进度上报)。
-
副作用隔离:Update 不写 I/O,所有外设/API 走 UseCase/Repo。
-
错误映射 :统一
Failure → 用户可读消息。 -
代码生成 :
freezed/json_serializable提升稳定性。 -
日志链路 :为每个副作用加
traceId,便于追踪事件→状态。
十二、迁移指北(从 setState/Provider 到 MVU)
-
先把状态 抽成不可变
State+Action枚举; -
写出
update(state, action)纯函数,页面仅dispatch(Action); -
把异步散落逻辑挪到 UseCase;
-
选 Riverpod 的
StateNotifier或 Bloc 的Bloc做 Store; -
外部流改成 Action 流 ,通过
dispatch/add回流; -
加单测,锁住 Update 与 UseCase 行为。
总结
-
响应式:UI = f(State);
-
MVU:事件 → 纯函数 Update → 新状态(副作用再回流为事件);
-
Clean Architecture:副作用入 UseCase/Repo,领域规则与 I/O 解耦;
-
Riverpod/Bloc:作为 Store 容器承载 MVU 循环。
最终得到一套可测试、可演进、可定位问题的 Flutter 架构。