Flutter 响应式 + Clean Architecture / MVU 模式 实战指南
目标
把 响应式编程 与 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)、易测试。
// 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:
// 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
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”)
// 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 的单向性。
class RobotEffects {final MqttBus mqtt;final MoveTo moveTo;final CancelMove cancelMove;final Connect connect;RobotEffects(this.mqtt, this.moveTo, this.cancelMove, this.connect);// 订阅外部事件流,转为 ActionStream<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
// 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); }}@overridevoid dispose() { _external.cancel(); super.dispose(); }
}
final robotStoreProvider =StateNotifierProvider<RobotStore, RobotState>((ref) => RobotStore(ref.read(effectsProvider)));
View:
class RobotPage extends ConsumerWidget {const RobotPage({super.key});@overrideWidget 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)
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:
BlocProvider(create: (_) => RobotBloc(context.read<RobotEffects>()),child: BlocBuilder<RobotBloc, RobotState>(builder: (_, s) => /* 同上渲染 */,),
);
十、测试策略(纯 Dart、可回归)
Update 纯函数单测:
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 架构。
