当前位置: 首页 > news >正文

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)、易测试。

// 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 架构。

http://www.dtcms.com/a/532428.html

相关文章:

  • 免费注册二级域名的网站网站制作哪些公司好
  • 【Go】--time包的使用
  • VR 工业组装案例
  • 网络运维管理
  • 使用STM32H723VGT6芯片驱动达妙电机
  • 【计算机通识】进程、线程、协程对比讲解--特点、优点、缺点
  • 专业做俄语网站建设上海建设公司注册
  • 南京营销型网站制作建设一个网站需要什么手续
  • POPAI全球启动仪式成功举办|AI×Web3全球算力革命启航
  • PCB笔记
  • C++ 类的学习(六) 虚函数
  • leetcode 2043 简易银行系统
  • 网站插件代码怎么用哪个网站上做自媒体最好
  • 【LeetCode100】--- 97.多数元素【思维导图+复习回顾】
  • Wasserstein 距离简介
  • 南宁网站建设外包vs做的网站如何
  • 【C++】前缀和算法习题
  • GitHub等平台形成的开源文化正在重塑加特
  • 基于单片机的家庭防盗防火智能门窗报警系统设计
  • 响应式网站建设的未来发展网络规划与设计就业前景
  • 【图像处理】图像错切变换
  • Docker环境离线安装-linux服务器
  • 软件设计师知识点总结:结构化开发
  • 持续改变源于团队学习
  • Unity安装newtonsoft
  • Spring Boot3零基础教程,整合 Redis,笔记69
  • 凡科网站官网登录入口wordpress 列表模板
  • 读论文AI prompt
  • 《Ionic 平台:全面解析与深入探讨》
  • 网站做淘宝客有什么要求微网站的优势