Flutter 响应式 + Clean Architecture / MVU 模式 实战指南

目标

响应式编程Clean ArchitectureMVU(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 架构。

相关推荐
Java烘焙师4 小时前
架构师必备:限流方案选型(使用篇)
redis·架构·限流
你听得到114 小时前
卷不动了?我写了一个 Flutter 全链路监控 SDK,从卡顿、崩溃到性能,一次性搞定!
前端·flutter·性能优化
404未精通的狗4 小时前
(数据结构)栈和队列
android·数据结构
YUFENGSHI.LJ4 小时前
Flutter 高性能 Tab 导航:懒加载与状态保持的最佳实践
开发语言·flutter·1024程序员节
恋猫de小郭4 小时前
今年各大厂都在跟进的智能眼镜是什么?为什么它突然就成为热点之一?它是否是机会?
android·前端·人工智能
朱嘉鼎4 小时前
寄存器编写LED程序
stm32·单片机·架构·keilmdk
游戏开发爱好者86 小时前
iOS 混淆工具链实战 多工具组合完成 IPA 混淆与加固 无源码混淆
android·ios·小程序·https·uni-app·iphone·webview
Layer11 小时前
CommonMark 解析策略与 cmark 工程核心代码解析
架构·markdown·设计
豆豆豆大王11 小时前
Android 数据持久化(SharedPreferences)
android