AtomGit Flutter 鸿蒙客户端:Clean Architecture 落地 Flutter
将 Clean Architecture 的抽象理念转化为具体的 Flutter 代码结构
目录
- 从一段"改不动"的代码说起
- Clean Architecture 的三层模型概览
- E-Brufen 当前架构诊断:问题出在哪里
- 三层架构的目录结构与文件命名规范
- 依赖方向的控制:Domain 层零依赖的工程化手段
- 跨层边界的数据转换:Entity vs Model vs DTO
- 实战:将情绪记录功能重构为 Clean Architecture
- Domain 层:Mood Entity 与 MoodRepository 接口
- Data 层:MoodRepositoryImpl 与 Hive DataSource
- Presentation 层:MoodBloc 与 MoodPage
- 依赖注入:把三层串联起来
- Clean Architecture 的真实成本:文件数量增加 3 倍
- 收益分析:可测试性、可维护性与团队协作
- 鸿蒙平台的兼容性说明
- 总结:什么时候该用,什么时候不该用
一、从一段"改不动"的代码说起

在 E-Brufen 项目的早期迭代中,情绪记录功能写在 DiaryPage 里,数据访问直接调用 MoodStorage。那是一段典型的"能用但不敢改"的代码。让我展示一个真实的问题场景。
某天,我想把数据存储从 Hive 迁移到鸿蒙原生的 RDB(关系型数据库)。这个需求听起来简单——换掉底层存储实现而已。但当我打开 DiaryPage 的代码时,发现这样的调用散布在 300 行 Widget 代码中:
// lib/pages/diary/diary_page.dart —— 重构前
void _saveMood() {
if (_selectedMood == null) return;
final now = DateTime.now();
final timestamp = DateTime(
_selectedDate.year, _selectedDate.month, _selectedDate.day,
now.hour, now.minute, now.second,
);
if (_editingId != null) {
final existing = widget.moodStorage.getAll().firstWhere(
(e) => e.id == _editingId,
orElse: () => MoodEntry(
moodType: _selectedMood!,
createdAt: timestamp,
updatedAt: timestamp,
),
);
widget.moodStorage.update(_editingId!, existing.copyWith(
moodType: _selectedMood!,
note: _noteController.text.isEmpty ? null : _noteController.text,
updatedAt: timestamp,
));
} else {
widget.moodStorage.insert(MoodEntry(
moodType: _selectedMood!,
note: _noteController.text.isEmpty ? null : _noteController.text,
createdAt: timestamp,
updatedAt: timestamp,
));
}
// ... UI 更新代码
}
问题的本质很清楚:UI 代码和数据访问代码纠缠在一起。DiaryPage 不仅需要知道"如何显示一个情绪选择器",还需要知道"MoodStorage 是通过 Hive 的 Box 来存数据的"。当你试图替换底层存储时,你必须同时理解 UI 逻辑和数据逻辑——这在工程上叫做高耦合。
更糟糕的是,这种耦合不是孤例。同一个文件中的 _loadMoods() 直接调用 widget.moodStorage.getAll(),_deleteMood() 直接调用 widget.moodStorage.delete(id),甚至 HomePage 也在直接调用 moodStorage.insert()。如果换掉 MoodStorage,至少 3 个文件需要修改。
这就是 Clean Architecture 要解决的核心问题:让业务逻辑独立于框架、独立于 UI、独立于数据库。
二、Clean Architecture 的三层模型概览
Robert C. Martin 在 2012 年提出的 Clean Architecture 原本包含 4 个同心圆:Entities、Use Cases、Interface Adapters、Frameworks & Drivers。但在 Flutter 移动应用的实际落地中,绝大多数项目使用一个简化的三层模型,每一层的职责非常明确:
┌─────────────────────────────────────────────────────────────────┐
│ Clean Architecture 三层模型 │
├─────────────────────────────────────────────────────────────────┤
│ │
│ ┌───────────────────────────────────────────────────────┐ │
│ │ Presentation 层 │ │
│ │ ┌──────────┐ ┌──────────┐ ┌──────────────────┐ │ │
│ │ │ Pages │ │ Widgets │ │ State Management │ │ │
│ │ │ (UI) │ │ (组件) │ │ (Bloc/Provider) │ │ │
│ │ └──────────┘ └──────────┘ └──────────────────┘ │ │
│ │ │ │
│ │ 职责:展示数据、响应用户输入、管理 UI 状态 │ │
│ │ 依赖:只依赖 Domain 层 │ │
│ └───────────────────────────┬───────────────────────────┘ │
│ │ 依赖方向 ↑ │
│ ┌───────────────────────────┴───────────────────────────┐ │
│ │ Domain 层 │ │
│ │ ┌──────────┐ ┌──────────┐ ┌──────────────────┐ │ │
│ │ │ Entities │ │ Use Cases│ │ Repositories │ │ │
│ │ │ (实体) │ │ (用例) │ │ (接口/抽象) │ │ │
│ │ └──────────┘ └──────────┘ └──────────────────┘ │ │
│ │ │ │
│ │ 职责:定义业务实体、封装业务规则、声明数据契约 │ │
│ │ 依赖:零依赖!不依赖任何层 │ │
│ └───────────────────────────┬───────────────────────────┘ │
│ │ 依赖方向 ↑ │
│ ┌───────────────────────────┴───────────────────────────┐ │
│ │ Data 层 │ │
│ │ ┌──────────┐ ┌──────────┐ ┌──────────────────┐ │ │
│ │ │ Repo Impl│ │DataSources│ │ DTOs │ │ │
│ │ │ (实现) │ │(本地/远程) │ │ (传输对象) │ │ │
│ │ └──────────┘ └──────────┘ └──────────────────┘ │ │
│ │ │ │
│ │ 职责:实现 Domain 层定义的接口、管理数据持久化 │ │
│ │ 依赖:只依赖 Domain 层 + 外部框架(Hive/HTTP) │ │
│ └───────────────────────────────────────────────────────┘ │
│ │
│ 核心规则:依赖方向永远由外向内,Domain 层是圆心 │
│ │
└─────────────────────────────────────────────────────────────────┘
这三层形成了严格的依赖倒置关系。用一句话概括:
Domain 层定义"做什么",Data 层负责"怎么做",Presentation 层负责"怎么展示"。Domain 层不依赖任何东西,Data 层和 Presentation 层都依赖 Domain 层,但它们彼此之间互不依赖。
这个架构在 Flutter 中的文件组织如下表所示:
| 层级 | 目录 | 典型文件 | 依赖的包 |
|---|---|---|---|
| Domain | lib/domain/ |
entities/, repositories/, usecases/ |
无(纯 Dart) |
| Data | lib/data/ |
datasources/, repositories/, models/ |
hive_ce, http 等 |
| Presentation | lib/presentation/ |
pages/, widgets/, blocs/ |
flutter, flutter_bloc |
三、E-Brufen 当前架构诊断:问题出在哪里
在动手重构之前,我们需要先对 E-Brufen 当前的代码结构做一个诚实的诊断。当前的目录结构是这样的:
lib/
├── main.dart # 初始化 + 依赖创建 + runApp
├── data/
│ ├── mood_storage.dart # 情绪数据 CRUD(Hive 实现)
│ ├── settings.dart # 应用设置(Hive 实现)
│ └── ohos_audio.dart # 鸿蒙原生音频桥接
├── models/
│ └── mood_entry.dart # 情绪数据模型(含序列化逻辑)
├── pages/
│ ├── home_page.dart # 首页(直接持有 MoodStorage)
│ ├── diary/
│ │ └── diary_page.dart # 日记页(直接调用 MoodStorage CRUD)
│ ├── breathe/
│ │ └── breathe_page.dart
│ └── soundscape/
│ └── soundscape_page.dart
├── widgets/
│ ├── mood_picker.dart # 情绪选择器
│ ├── mood_chart.dart # 周情绪图表
│ ├── breathing_circle.dart
│ └── home_card.dart
└── theme/
└── app_theme.dart
这个结构能不能用?能用。目前在 50 多篇系列文章构建下来的应用,功能正常运转。但从架构层面审视,存在四个明确的改进空间:
问题一:Data 层和 Presentation 层直接耦合。 HomePage 和 DiaryPage 都直接引用了 MoodStorage——一个 Data 层的具体实现类。当你想把 MoodStorage 从 Hive 换到鸿蒙 RDB 时,所有引用了它的文件都得改。下面的图展示了这种耦合:
┌──────────────┐ ┌──────────────────┐
│ HomePage │────────►│ MoodStorage │
│ (UI) │ 直接依赖 │ (Hive 实现) │
└──────────────┘ └──────────────────┘
▲
┌──────────────┐ │
│ DiaryPage │───────────────────┘
│ (UI) │ 直接依赖
└──────────────┘
问题二:没有 Domain 层。 业务规则内嵌在 UI 代码中。比如"同一分钟内不允许重复记录情绪"这样的规则,现在不得不写在 Page 的 State 里——这使得规则既不能被单独测试,也不能被其他页面复用。
问题三:Model 承担了多重职责。 MoodEntry 类同时包含了业务字段(moodType、note)、JSON 序列化逻辑(toJson()、fromJson())、以及 UI 展示用的 Emoji。它在不同层之间被直接传递,没有任何边界。
问题四:测试困难。 要给 DiaryPage 写 Widget 测试,你必须先初始化一个真实的 Hive Box。测试变成了集成测试,而不是单元测试。
下面是一张诊断对比表,展示了当前架构与 Clean Architecture 的核心差距:
| 维度 | 当前架构(E-Brufen) | Clean Architecture 目标 |
|---|---|---|
| 分层数量 | 2 层(Models + Pages/Data 混合) | 3 层(Data → Domain → Presentation) |
| 依赖方向 | 无约束,任意文件可引用任意文件 | 严格由外向内 |
| Domain 层 | 不存在 | 纯 Dart,零外部依赖 |
| 数据层可替换性 | 低(UI 直接依赖具体实现) | 高(UI 只依赖接口) |
| 数据对象 | 1 种(MoodEntry 通用于所有层) | 3 种(Entity / Model / DTO) |
| 单元测试 | 困难(Hive 必须初始化) | 简单(Mock Repository 接口即可) |
| 文件数量 | 约 10 个 | 约 25-30 个(3 倍增长) |
四、三层架构的目录结构与文件命名规范
在 Clean Architecture 中,目录结构就是架构的第一份文档。一个新人打开项目,应该能从目录树中直接读懂架构分层。以下是 E-Brufen 重构后的目标目录结构:
lib/
├── main.dart # 入口:依赖注入 + runApp
│
├── core/ # 跨层共享的通用工具
│ ├── error/
│ │ └── failures.dart # 统一的错误类型定义
│ └── usecase.dart # UseCase 基类
│
├── domain/ # ── Domain 层(零依赖)──
│ ├── entities/
│ │ └── mood.dart # Mood 业务实体
│ ├── repositories/
│ │ └── mood_repository.dart # MoodRepository 抽象接口
│ └── usecases/
│ ├── record_mood.dart # 记录情绪用例
│ ├── get_weekly_moods.dart # 获取周情绪用例
│ └── delete_mood.dart # 删除情绪记录用例
│
├── data/ # ── Data 层 ──
│ ├── datasources/
│ │ ├── mood_local_datasource.dart # 本地数据源接口
│ │ └── mood_local_datasource_impl.dart # Hive 实现
│ ├── models/
│ │ └── mood_model.dart # 数据模型(含序列化逻辑)
│ └── repositories/
│ └── mood_repository_impl.dart # MoodRepository 接口的实现
│
└── presentation/ # ── Presentation 层 ──
├── pages/
│ ├── home/
│ │ └── home_page.dart
│ └── diary/
│ └── diary_page.dart
├── widgets/
│ ├── mood_picker.dart
│ └── mood_chart.dart
└── blocs/
└── mood/
├── mood_bloc.dart # 情绪页面的状态管理
├── mood_event.dart # 事件定义
└── mood_state.dart # 状态定义
文件命名规范的核心原则:
- Domain 层的实体用业务语言命名,不加前缀后缀:
mood.dart、user.dart、breathing_session.dart - Data 层的实现类加
Impl后缀:mood_repository_impl.dart、mood_local_datasource_impl.dart - Data 层的 DTO/Model 加
_model后缀:mood_model.dart,区分于 Domain 层的mood.dart - Repository 接口与实现分属不同层:接口在
domain/repositories/,实现在data/repositories/ - UseCase 一个文件一个类:每个用例一个 Dart 文件,单一职责
这个目录结构初看比原来的 10 个文件多了很多——大约 25 个文件。但当你需要定位某个功能的代码时,路径非常清晰:想看业务规则去 domain/,想看存储实现去 data/datasources/,想看 UI 去 presentation/pages/。不需要在"这个逻辑到底该放哪里"上耗费心智。
五、依赖方向的控制:Domain 层零依赖的工程化手段
Clean Architecture 最核心的规则是依赖规则。在 Flutter 项目中,这条规则的工程化落地有三个手段。
5.1 手段一:pubspec.yaml 的依赖隔离
Domain 层的代码不引用任何 Flutter 或第三方包。它只使用 Dart SDK 自带的功能。这个约束不是靠口头约定,而是靠 pubspec.yaml 的依赖设计来保证的——Domain 层的代码文件不 import 任何 package:flutter/ 或 package:hive_ce/ 的内容:
// ✅ domain/entities/mood.dart —— 合法的 Domain 层代码
// 没有任何 import,只用 Dart 核心库
enum MoodType { angry, sad, tired, calm, happy }
class Mood {
final int? id;
final MoodType moodType;
final String? note;
final DateTime createdAt;
const Mood({
this.id,
required this.moodType,
this.note,
required this.createdAt,
});
}
// ❌ domain/entities/mood.dart —— 不合法的 Domain 层代码
// import 'package:hive_ce/hive.dart'; // 违反零依赖原则!
// import 'package:flutter/material.dart'; // 违反零依赖原则!
5.2 手段二:Repository 接口定义在 Domain 层
这是依赖倒置的精髓。Domain 层声明"我需要一个能存取情绪数据的东西",但不关心这个"东西"是谁实现的。Hive 也好、鸿蒙 RDB 也好、甚至一个 Mock 实现也好——对 Domain 层来说都一样:
// lib/domain/repositories/mood_repository.dart
// 这个文件在 Domain 层,只定义接口,不引用任何外部依赖
import '../entities/mood.dart';
abstract class MoodRepository {
Future<int> record(Mood mood);
Future<void> delete(int id);
Future<void> update(Mood mood);
Future<List<Mood>> getAll();
Future<List<Mood>> getByWeek(DateTime anyDay);
}
Data 层实现这个接口时,需要 import Domain 层的文件:
// lib/data/repositories/mood_repository_impl.dart
// 这个文件在 Data 层,import Domain 层的接口
import 'package:hive_ce/hive.dart'; // ← 外部依赖 OK(在 Data 层)
import '../../domain/entities/mood.dart'; // ← 依赖 Domain 层(向内依赖 OK)
import '../../domain/repositories/mood_repository.dart';
class MoodRepositoryImpl implements MoodRepository {
final MoodLocalDataSource _localDataSource;
const MoodRepositoryImpl({required MoodLocalDataSource localDataSource})
: _localDataSource = localDataSource;
Future<int> record(Mood mood) async {
return _localDataSource.insert(mood.toModel());
}
// ...
}
依赖方向:mood_repository_impl.dart → mood_repository.dart,由外向内,正确。
5.3 手段三:UseCase 封装业务规则
这是很多 Clean Architecture 初学者困惑的点:UseCase 到底该干什么?简单来说,UseCase 封装的是单个用户操作所涉及的业务规则。比如"记录一条情绪"这个操作,表面上看只是调用 repository.record(),但实际业务规则可能包括:
- 同一分钟内不允许重复记录
- 每天最多记录 20 条(防止 API 滥用——即使本地存储也要防御)
- 记录后更新本周统计快照
这些规则应该独立于 UI 实现:
// lib/domain/usecases/record_mood.dart
import '../entities/mood.dart';
import '../repositories/mood_repository.dart';
class RecordMood {
final MoodRepository repository;
const RecordMood(this.repository);
/// 记录一条情绪。返回 null 表示成功,否则返回错误信息。
Future<String?> call(Mood mood) async {
// 业务规则 1:同一分钟内不允许重复记录
final recentMoods = await repository.getAll();
final now = DateTime.now();
final duplicate = recentMoods.any((m) =>
m.moodType == mood.moodType &&
m.createdAt.difference(now).inMinutes.abs() < 1);
if (duplicate) {
return '一分钟内已记录相同情绪,请稍后再试';
}
// 业务规则 2:每天最多 20 条
final todayMoods = recentMoods.where((m) =>
m.createdAt.year == now.year &&
m.createdAt.month == now.month &&
m.createdAt.day == now.day);
if (todayMoods.length >= 20) {
return '今日记录已达上限(20条)';
}
// 通过规则检查,执行记录
try {
await repository.record(mood);
return null; // 成功
} catch (e) {
return '保存失败: $e';
}
}
}
请注意:RecordMood 类没有 import 任何 Flutter 或 Hive 相关的包。它是一个纯 Dart 类,只依赖 Domain 层自己的 Mood 实体和 MoodRepository 接口。这意味着你可以用 100% 的单元测试来验证"一分钟内重复记录"这个业务规则,不需要任何 Flutter 框架的初始化。
六、跨层边界的数据转换:Entity vs Model vs DTO
在 Clean Architecture 中,同一个"情绪数据"在不同层有不同的表示形式。这是很多开发者觉得"啰嗦"的地方,但正是这种啰嗦换来了边界清晰。
6.1 三种数据对象的定义
┌─────────────────────────────────────────────────────────────────┐
│ 跨层数据转换流程 │
├─────────────────────────────────────────────────────────────────┤
│ │
│ Presentation 层 │
│ │ 使用:MoodState (UI 需要的格式) │
│ │ MoodState { List<Mood> moods, bool isLoading, ... } │
│ ▼ │
│ Domain 层 │
│ │ 使用:Mood Entity (业务实体,纯数据) │
│ │ Mood { int? id, MoodType type, String? note, ... } │
│ ▼ │
│ Data 层 │
│ │ 使用:MoodModel (序列化模型,知道如何转 JSON) │
│ │ MoodModel extends Mood { toJson(), fromJson() } │
│ ▼ │
│ Hive Box(持久化存储) │
│ │ 存储:JSON String { "id":1, "mood_type":5, ... } │
│ │
└─────────────────────────────────────────────────────────────────┘
下面是具体的代码实现。
Domain 层的 Mood Entity:
// lib/domain/entities/mood.dart
enum MoodType {
angry(1, '😡', '生气'),
sad(2, '😢', '难过'),
tired(3, '😴', '疲惫'),
calm(4, '😐', '平静'),
happy(5, '😊', '开心');
final int value;
final String emoji;
final String label;
const MoodType(this.value, this.emoji, this.label);
static MoodType fromValue(int v) => MoodType.values.firstWhere(
(m) => m.value == v,
orElse: () => MoodType.calm,
);
}
class Mood {
final int? id;
final MoodType moodType;
final String? note;
final DateTime createdAt;
final DateTime updatedAt;
const Mood({
this.id,
required this.moodType,
this.note,
required this.createdAt,
required this.updatedAt,
});
}
注意:Domain 层的 Mood 类不包含 toJson() 和 fromJson()。序列化逻辑属于 Data 层的职责。
Data 层的 MoodModel(DTO):
// lib/data/models/mood_model.dart
import '../../domain/entities/mood.dart';
class MoodModel extends Mood {
const MoodModel({
super.id,
required super.moodType,
super.note,
required super.createdAt,
required super.updatedAt,
});
/// 从 Domain Entity 构造 Model
factory MoodModel.fromEntity(Mood mood) => MoodModel(
id: mood.id,
moodType: mood.moodType,
note: mood.note,
createdAt: mood.createdAt,
updatedAt: mood.updatedAt,
);
Map<String, dynamic> toJson() => {
'id': id,
'mood_type': moodType.value,
'note': note,
'created_at': createdAt.toIso8601String(),
'updated_at': updatedAt.toIso8601String(),
};
factory MoodModel.fromJson(Map<String, dynamic> json) => MoodModel(
id: json['id'] as int?,
moodType: MoodType.fromValue(json['mood_type'] as int),
note: json['note'] as String?,
createdAt: DateTime.parse(json['created_at'] as String),
updatedAt: DateTime.parse(json['updated_at'] as String),
);
}
关键设计:MoodModel extends Mood。这意味着在 Data 层操作 MoodModel 的代码,可以将它直接返回给 Domain 层(因为 MoodModel 就是 Mood 的子类),Domain 层不需要感知任何序列化逻辑。
Presentation 层的 MoodState:
// lib/presentation/blocs/mood/mood_state.dart
import '../../../domain/entities/mood.dart';
class MoodState {
final List<Mood> moods;
final List<Mood> weekMoods;
final bool isLoading;
final String? errorMessage;
const MoodState({
this.moods = const [],
this.weekMoods = const [],
this.isLoading = false,
this.errorMessage,
});
MoodState copyWith({
List<Mood>? moods,
List<Mood>? weekMoods,
bool? isLoading,
String? errorMessage,
}) {
return MoodState(
moods: moods ?? this.moods,
weekMoods: weekMoods ?? this.weekMoods,
isLoading: isLoading ?? this.isLoading,
errorMessage: errorMessage,
);
}
}
6.2 三种对象的对比表
| 对象类型 | 所在层 | 职责 | 依赖 | 是否可序列化 |
|---|---|---|---|---|
| Mood (Entity) | Domain | 定义业务数据的结构 | 无 | 否 |
| MoodModel (DTO) | Data | 负责 JSON ↔ 对象转换 | 依赖 Mood + Hive | 是 |
| MoodState | Presentation | 封装 UI 需要的显示状态 | 依赖 Mood | 否 |
6.3 转换的边界在哪里
一个常见的问题是:转换逻辑应该放在哪里?答案是在 Data 层的 Repository 实现中。每当数据跨越 Data ↔ Domain 的边界时,就执行一次转换:
// lib/data/repositories/mood_repository_impl.dart
class MoodRepositoryImpl implements MoodRepository {
final MoodLocalDataSource _dataSource;
const MoodRepositoryImpl(this._dataSource);
Future<List<Mood>> getAll() async {
// DataSource 返回的是 MoodModel 列表(带序列化能力)
final models = await _dataSource.getAll();
// 在边界处转换:MoodModel → Mood(去掉序列化能力)
// 因为 MoodModel extends Mood,实际上可以直接返回
return models;
}
Future<int> record(Mood mood) async {
// 入口转换:Mood → MoodModel(加上序列化能力)
final model = MoodModel.fromEntity(mood);
return _dataSource.insert(model);
}
}
这个转换看起来有点"多余"——毕竟 MoodModel 就是 Mood 的子类。但我刻意保留这个转换,因为当底层存储从 Hive 迁移到鸿蒙 RDB 时,MoodModel 的 toJson() 可能会变成 toRow(),那时你就知道这个边界的重要性了。
七、实战:将情绪记录功能重构为 Clean Architecture
现在我们进入真正的实战环节。以 E-Brufen 的情绪记录功能(从"今日速记"点击 Emoji 到数据持久化完成)为主线,完整展示从现有代码迁移到 Clean Architecture 的全过程。
7.1 重构前的代码流
用户点击 Emoji
│
▼
HomePage._quickCheckIn(context, mood)
│
├── moodStorage.insert(MoodEntry(...)) ← 直接操作 Hive
└── ScaffoldMessenger.showSnackBar(...) ← 直接操作 UI
重构前的流程只有一步:UI 事件直接触发数据操作。没有中间层,没有业务验证。
7.2 重构后的代码流
用户点击 Emoji
│
▼
MoodBloc.add(RecordMoodEvent(moodType: mood)) ← 发送事件
│
▼
MoodBloc._onRecordMood(event) ← Bloc 处理事件
│
├── RecordMood(repository).call(mood) ← 调用 UseCase(含业务规则)
│ │
│ └── MoodRepository.record(mood) ← 调用 Repository 接口
│ │
│ └── MoodRepositoryImpl.record(mood) ← Data 层实现
│ │
│ ├── MoodModel.fromEntity(mood) ← Entity → Model 转换
│ └── MoodLocalDataSource.insert(model) ← Hive 写入
│
└── emit(MoodState(..., successMessage: '已记录 ✅')) ← 更新 UI 状态
从一步变成七步并不是无意义的迂回。这七步中的每一步都有独立的职责和可测试边界。下面我们分三层详细展开代码。
八、Domain 层:Mood Entity 与 MoodRepository 接口
Domain 层是重构的起点。我们从最核心的业务语言开始,定义"情绪是什么"和"情绪数据该怎么存取"。
8.1 Mood Entity
// lib/domain/entities/mood.dart
// 零外部依赖——这个文件不 import 任何 package:xxx
/// 五种情绪类型
enum MoodType {
angry(1, '😡', '生气'),
sad(2, '😢', '难过'),
tired(3, '😴', '疲惫'),
calm(4, '😐', '平静'),
happy(5, '😊', '开心');
final int value;
final String emoji;
final String label;
const MoodType(this.value, this.emoji, this.label);
static MoodType fromValue(int v) => MoodType.values.firstWhere(
(m) => m.value == v,
orElse: () => MoodType.calm,
);
}
/// 情绪记录实体——业务层唯一的数据表示
class Mood {
final int? id;
final MoodType moodType;
final String? note;
final DateTime createdAt;
final DateTime updatedAt;
const Mood({
this.id,
required this.moodType,
this.note,
required this.createdAt,
required this.updatedAt,
});
String toString() =>
'Mood(id:$id, type:${moodType.label}, note:$note)';
}
8.2 统一的错误模型
在 Domain 层定义一个跨层通用的 Failure 类型,这样 UseCase 和 Bloc 都可以用它来传递错误信息,而不是直接抛 Exception:
// lib/core/error/failures.dart
abstract class Failure {
final String message;
const Failure(this.message);
}
class StorageFailure extends Failure {
const StorageFailure(super.message);
}
class ValidationFailure extends Failure {
const ValidationFailure(super.message);
}
8.3 MoodRepository 接口
// lib/domain/repositories/mood_repository.dart
import '../entities/mood.dart';
/// 情绪数据仓库的抽象契约
/// 任何存储实现(Hive、RDB、云同步)都必须满足这个接口
abstract class MoodRepository {
/// 记录一条情绪,返回记录 ID
Future<int> record(Mood mood);
/// 删除指定记录
Future<void> delete(int id);
/// 更新指定记录
Future<void> update(Mood mood);
/// 获取所有记录,按时间倒序
Future<List<Mood>> getAll();
/// 获取指定周(周一~周日)的记录
Future<List<Mood>> getByWeek(DateTime anyDay);
}
8.4 RecordMood UseCase
// lib/domain/usecases/record_mood.dart
import '../entities/mood.dart';
import '../repositories/mood_repository.dart';
import '../../core/error/failures.dart';
class RecordMood {
final MoodRepository repository;
const RecordMood(this.repository);
/// 记录一条情绪。
/// 返回 null 表示成功,返回 Failure 表示失败。
Future<Failure?> call(Mood mood) async {
final now = DateTime.now();
// 业务规则 1:prevent duplicate within 1 minute
final allMoods = await repository.getAll();
final duplicate = allMoods.any((m) =>
m.moodType == mood.moodType &&
m.createdAt.difference(now).inMinutes.abs() < 1);
if (duplicate) {
return const ValidationFailure('一分钟内已记录相同情绪,请稍后再试');
}
// 业务规则 2:daily cap at 20
final todayCount = allMoods.where((m) =>
m.createdAt.year == now.year &&
m.createdAt.month == now.month &&
m.createdAt.day == now.day).length;
if (todayCount >= 20) {
return const ValidationFailure('今日记录已达上限(20条)');
}
// 执行持久化
try {
await repository.record(mood);
return null;
} catch (e) {
return StorageFailure('保存失败: $e');
}
}
}
Domain 层的代码加起来约 120 行。但这 120 行定义了整个情绪记录功能的业务语言和业务规则——而且是可独立测试的。你可以用 Mock 的 MoodRepository 来验证"一分钟内重复记录会被拒绝"这条规则,完全不需要启动 Hive 或 Flutter。
九、Data 层:MoodRepositoryImpl 与 Hive DataSource
Data 层的职责是实现 Domain 层的契约。它需要知道 Hive 的 Box 怎么打开、JSON 怎么序列化、ID 怎么自增——但这些细节永远不会泄露到 Domain 层或 Presentation 层。
9.1 本地数据源接口
为了进一步提升 Data 层内部的可替换性,我们把原始的存储操作也抽象为一个接口:
// lib/data/datasources/mood_local_datasource.dart
import '../models/mood_model.dart';
abstract class MoodLocalDataSource {
Future<void> init();
Future<int> insert(MoodModel model);
Future<void> update(int id, MoodModel model);
Future<void> delete(int id);
List<MoodModel> getAll();
bool get isReady;
}
9.2 Hive 实现
// lib/data/datasources/mood_local_datasource_impl.dart
import 'dart:convert';
import 'package:hive_ce/hive.dart';
import '../models/mood_model.dart';
import 'mood_local_datasource.dart';
class MoodLocalDataSourceImpl implements MoodLocalDataSource {
static const _boxName = 'moods';
Box? _box;
int _nextId = 1;
bool get isReady => _box != null && _box!.isOpen;
Future<void> init() async {
_box = await Hive.openBox(_boxName);
if (_box!.isNotEmpty) {
_nextId = _box!.keys
.fold<int>(0, (max, k) => k is int ? (k > max ? k : max) : max) + 1;
}
}
Future<int> insert(MoodModel model) async {
final id = _nextId++;
final data = model.toJson();
data['id'] = id;
await _box?.put(id, jsonEncode(data));
return id;
}
Future<void> update(int id, MoodModel model) async {
final data = model.toJson();
data['id'] = id;
await _box?.put(id, jsonEncode(data));
}
Future<void> delete(int id) async {
await _box?.delete(id);
}
List<MoodModel> getAll() {
if (_box == null) return [];
final entries = <MoodModel>[];
for (final key in _box!.keys) {
if (key is int) {
final raw = _box!.get(key);
if (raw is String) {
entries.add(MoodModel.fromJson(
(jsonDecode(raw) as Map).cast<String, dynamic>()));
}
}
}
entries.sort((a, b) => b.createdAt.compareTo(a.createdAt));
return entries;
}
}
9.3 Repository 实现
// lib/data/repositories/mood_repository_impl.dart
import '../../domain/entities/mood.dart';
import '../../domain/repositories/mood_repository.dart';
import '../datasources/mood_local_datasource.dart';
import '../models/mood_model.dart';
class MoodRepositoryImpl implements MoodRepository {
final MoodLocalDataSource _dataSource;
const MoodRepositoryImpl({required MoodLocalDataSource dataSource})
: _dataSource = dataSource;
Future<int> record(Mood mood) async {
final model = MoodModel.fromEntity(mood);
return _dataSource.insert(model);
}
Future<void> delete(int id) async {
await _dataSource.delete(id);
}
Future<void> update(Mood mood) async {
if (mood.id == null) {
throw ArgumentError('Cannot update a Mood without an id');
}
final model = MoodModel.fromEntity(mood);
await _dataSource.update(mood.id!, model);
}
Future<List<Mood>> getAll() async {
final models = _dataSource.getAll();
return models; // MoodModel extends Mood,直接返回
}
Future<List<Mood>> getByWeek(DateTime anyDay) async {
final monday = _mondayOf(anyDay);
final sunday = monday.add(const Duration(days: 7));
final all = await getAll();
return all.where((m) =>
m.createdAt.isAfter(monday.subtract(const Duration(seconds: 1))) &&
m.createdAt.isBefore(sunday)).toList();
}
DateTime _mondayOf(DateTime d) {
return DateTime(d.year, d.month, d.day - (d.weekday - 1));
}
}
关键观察:MoodRepositoryImpl 的 getAll() 返回类型是 List<Mood>(Domain 层的类型),但实际返回的是 List<MoodModel>(Data 层的类型)。因为 MoodModel extends Mood,这在 Dart 的类型系统中完全合法,而且 Presentation 层拿到的是 List<Mood>,永远接触不到 MoodModel 的 toJson() 方法。
十、Presentation 层:MoodBloc 与 MoodPage
Presentation 层只做三件事:接收用户事件、调用 Domain 层的 UseCase、根据结果更新 UI 状态。使用 BLoC 模式(你也可以用 Provider、Riverpod 或任何状态管理方案,架构分层与状态管理方案的选择是正交的)。
10.1 MoodEvent
// lib/presentation/blocs/mood/mood_event.dart
import '../../../domain/entities/mood.dart';
sealed class MoodEvent {
const MoodEvent();
}
class RecordMoodEvent extends MoodEvent {
final MoodType moodType;
const RecordMoodEvent(this.moodType);
}
class DeleteMoodEvent extends MoodEvent {
final int id;
const DeleteMoodEvent(this.id);
}
class LoadMoodsEvent extends MoodEvent {
const LoadMoodsEvent();
}
class LoadWeekMoodsEvent extends MoodEvent {
const LoadWeekMoodsEvent();
}
10.2 MoodBloc
// lib/presentation/blocs/mood/mood_bloc.dart
import 'dart:async';
import '../../../domain/entities/mood.dart';
import '../../../domain/usecases/record_mood.dart';
import '../../../domain/usecases/get_weekly_moods.dart';
import '../../../domain/usecases/delete_mood.dart';
import 'mood_event.dart';
import 'mood_state.dart';
class MoodBloc {
final RecordMood _recordMood;
final DeleteMood _deleteMood;
final GetWeeklyMoods _getWeeklyMoods;
MoodState _state = const MoodState();
MoodState get state => _state;
final _stateController = StreamController<MoodState>.broadcast();
Stream<MoodState> get stateStream => _stateController.stream;
MoodBloc({
required RecordMood recordMood,
required DeleteMood deleteMood,
required GetWeeklyMoods getWeeklyMoods,
}) : _recordMood = recordMood,
_deleteMood = deleteMood,
_getWeeklyMoods = getWeeklyMoods;
void _emit(MoodState newState) {
_state = newState;
_stateController.add(_state);
}
Future<void> add(MoodEvent event) async {
switch (event) {
case RecordMoodEvent(:final moodType):
await _onRecordMood(moodType);
case DeleteMoodEvent(:final id):
await _onDeleteMood(id);
case LoadMoodsEvent():
await _onLoadMoods();
case LoadWeekMoodsEvent():
await _onLoadWeekMoods();
}
}
Future<void> _onRecordMood(MoodType moodType) async {
_emit(_state.copyWith(isLoading: true));
final now = DateTime.now();
final mood = Mood(
moodType: moodType,
createdAt: now,
updatedAt: now,
);
final failure = await _recordMood(mood);
if (failure != null) {
_emit(_state.copyWith(
isLoading: false,
errorMessage: failure.message,
));
} else {
// 记录成功后刷新列表
final allMoods = await _getWeeklyMoods.repository.getAll();
final weekMoods = await _getWeeklyMoods(DateTime.now());
_emit(_state.copyWith(
isLoading: false,
moods: allMoods,
weekMoods: weekMoods,
errorMessage: null,
));
}
}
Future<void> _onDeleteMood(int id) async {
await _deleteMood(id);
// 删除后刷新
await _onLoadMoods();
await _onLoadWeekMoods();
}
Future<void> _onLoadMoods() async {
final moods = await _recordMood.repository.getAll();
_emit(_state.copyWith(moods: moods));
}
Future<void> _onLoadWeekMoods() async {
final weekMoods = await _getWeeklyMoods(DateTime.now());
_emit(_state.copyWith(weekMoods: weekMoods));
}
void dispose() {
_stateController.close();
}
}
10.3 DiaryPage 重构后
// lib/presentation/pages/diary/diary_page.dart —— 重构后
class DiaryPage extends StatelessWidget {
final MoodBloc moodBloc;
const DiaryPage({super.key, required this.moodBloc});
Future<void> _quickRecord(BuildContext context, MoodType mood) async {
await moodBloc.add(RecordMoodEvent(mood));
final state = moodBloc.state;
if (context.mounted) {
ScaffoldMessenger.of(context).showSnackBar(SnackBar(
content: Text(state.errorMessage ?? '已记录 ✅'),
duration: const Duration(seconds: 1),
backgroundColor: state.errorMessage != null
? Colors.red.shade400
: null,
));
}
}
Widget build(BuildContext context) {
return StreamBuilder<MoodState>(
stream: moodBloc.stateStream,
initialData: moodBloc.state,
builder: (context, snapshot) {
final state = snapshot.data ?? moodBloc.state;
return Scaffold(
appBar: AppBar(title: const Text('情绪日记')),
body: state.isLoading
? const Center(child: CircularProgressIndicator())
: _buildContent(context, state),
);
},
);
}
Widget _buildContent(BuildContext context, MoodState state) {
return SingleChildScrollView(
child: Column(
children: [
// 今日速记区域
Row(
mainAxisAlignment: MainAxisAlignment.spaceEvenly,
children: MoodType.values.map((mood) => GestureDetector(
onTap: () => _quickRecord(context, mood),
child: Text(mood.emoji, style: const TextStyle(fontSize: 32)),
)).toList(),
),
// 周统计图表
if (state.weekMoods.isNotEmpty)
MoodChart(weekMoods: state.weekMoods),
// 时间线
...state.moods.map((m) => ListTile(
leading: Text(m.moodType.emoji),
title: Text(m.moodType.label),
subtitle: m.note != null ? Text(m.note!) : null,
)),
],
),
);
}
}
注意重构后的 DiaryPage 不再 import MoodStorage。它对底层存储一无所知——它只知道 MoodBloc。如果将来把 Hive 替换成鸿蒙 RDB,DiaryPage 一行代码都不需要改动。
十一、依赖注入:把三层串联起来
Clean Architecture 分出了三层,但最终需要一个地方把三层组装起来。在 Flutter 中,这个组装通常发生在 main.dart 或一个专门的 injection_container.dart 中:
// lib/main.dart —— 重构后
void main() async {
WidgetsFlutterBinding.ensureInitialized();
// 1. Data 层初始化
await Hive.initFlutter();
final dataSource = MoodLocalDataSourceImpl();
await dataSource.init();
// 2. Repository 初始化(Data 层实现 Domain 层接口)
final repository = MoodRepositoryImpl(dataSource: dataSource);
// 3. UseCase 初始化(Domain 层)
final recordMood = RecordMood(repository);
final deleteMood = DeleteMood(repository);
final getWeeklyMoods = GetWeeklyMoods(repository);
// 4. Bloc 初始化(Presentation 层持有 UseCase)
final moodBloc = MoodBloc(
recordMood: recordMood,
deleteMood: deleteMood,
getWeeklyMoods: getWeeklyMoods,
);
// 5. 启动应用
runApp(EBrufenApp(moodBloc: moodBloc));
}
依赖注入的顺序严格遵循依赖方向:Data → Domain → Presentation。你可以清晰地看到每一层的对象是如何被创建的,以及它们引用了谁:
main.dart 的依赖组装顺序:
Hive (外部框架)
│
▼
MoodLocalDataSourceImpl ← Data 层的最内层
│
▼
MoodRepositoryImpl ← Data 层,实现 Domain 接口
│
▼
RecordMood / DeleteMood ← Domain 层,使用 Repository 接口
│
▼
MoodBloc ← Presentation 层,使用 UseCase
│
▼
EBrufenApp / DiaryPage ← UI,使用 Bloc
十二、Clean Architecture 的真实成本:文件数量增加 3 倍
Clean Architecture 不是免费的。它最大的成本是文件数量和间接层的增加。让我用 E-Brufen 情绪记录功能的实际数据来说明。
12.1 文件数量对比
| 功能模块 | 重构前(文件数) | 重构后(文件数) | 倍数 |
|---|---|---|---|
| 情绪数据模型 | 1(mood_entry.dart) | 3(mood.dart, mood_model.dart, mood_state.dart) | 3x |
| 数据存储 | 1(mood_storage.dart) | 3(datasource 接口, Hive 实现, Repository 实现) | 3x |
| 业务规则 | 0(混在 UI 中) | 3(record_mood.dart, delete_mood.dart, get_weekly_moods.dart) | N/A |
| UI 页面 | 1(diary_page.dart) | 4(diary_page.dart, mood_bloc.dart, mood_event.dart, mood_state.dart) | 4x |
| 情绪模块合计 | 3 个文件 | 13 个文件 | 4.3x |
| 全局合计 | 约 10 个文件 | 约 25-30 个文件 | 约 3x |
12.2 代码行数对比
| 指标 | 重构前 | 重构后 | 差值 |
|---|---|---|---|
| DiaryPage 代码行数 | 317 行 | 约 100 行 | -217 行(减少 68%) |
| 情绪存储代码行数 | 115 行(mood_storage.dart) | 约 200 行(分散在 3 个文件) | +85 行 |
| 新增业务逻辑文件 | 0 行 | 约 80 行(3 个 UseCase) | +80 行 |
| 新增 Bloc 文件 | 0 行 | 约 120 行(Bloc + Event + State) | +120 行 |
| 新增接口/抽象文件 | 0 行 | 约 40 行(Repository + DataSource 接口) | +40 行 |
| 跨层转换逻辑 | 0 行 | 约 30 行(MoodModel 转换) | +30 行 |
| 依赖注入配置 | 0 行 | 约 25 行(main.dart 中新增) | +25 行 |
| 合计 | 约 430 行 | 约 595 行 | +38% 代码量 |
12.3 成本收益的量化分析
| 维度 | 重构前的评分 | 重构后的评分 | 变化 |
|---|---|---|---|
| 代码行数 | 430 行 | 595 行 | +38% |
| 文件数量 | 3 个 | 13 个 | +333% |
| 单元测试覆盖率可达性 | 约 20%(只能测纯函数) | 约 80%(每层可独立 Mock) | +300% |
| 新人理解代码结构的时间 | 约 1 小时 | 约 2 小时(但后续修改快) | 初次成本 +100% |
| 修改存储实现的时间 | 约 3 小时(需改 3+ 个文件) | 约 0.5 小时(只改 Data 层) | -83% |
| 新增一条业务规则的时间 | 约 1 小时(需理解 UI 代码) | 约 15 分钟(只改 UseCase) | -75% |
真实数据表明:Clean Architecture 的初次写作成本约为原来的 1.4 倍,但后续维护成本显著降低。如果你的项目预计会迭代 6 个月以上,或者需要频繁替换底层实现(比如先在本地 Hive 跑起来,后续接入云端同步),那么初期多写的 165 行代码将在第一次修改时就连本带利赚回来。
十三、收益分析:可测试性、可维护性与团队协作
13.1 可测试性:从集成测试到单元测试
重构前,验证"一分钟内重复记录被拒绝"这个逻辑需要启动 Hive,写入一条数据,再写入第二条,检查是否被拒绝——这是一个集成测试:
// 重构前的"集成测试"——需要启动 Hive
test('should reject duplicate mood within 1 minute', () async {
await Hive.initFlutter();
final storage = MoodStorage();
await storage.init();
final mood1 = MoodEntry(
moodType: MoodType.happy,
createdAt: DateTime.now(),
updatedAt: DateTime.now(),
);
await storage.insert(mood1);
// 测试重复——但这实际上在测 Hive + MoodStorage + 硬编码规则
final allMoods = storage.getAll();
final duplicate = allMoods.any((m) =>
m.moodType == MoodType.happy &&
m.createdAt.difference(DateTime.now()).inMinutes.abs() < 1);
expect(duplicate, true);
});
重构后,同样的逻辑可以在 Domain 层用纯单元测试完成:
// 重构后的"单元测试"——不需要 Hive,不需要 Flutter
class MockMoodRepository implements MoodRepository {
final List<Mood> _moods = [];
Future<int> record(Mood mood) async { _moods.add(mood); return _moods.length; }
Future<List<Mood>> getAll() async => _moods;
Future<void> delete(int id) async {}
Future<void> update(Mood mood) async {}
Future<List<Mood>> getByWeek(DateTime d) async => _moods;
}
void main() {
test('should reject duplicate mood within 1 minute', () async {
final repository = MockMoodRepository();
final recordMood = RecordMood(repository);
final mood = Mood(
moodType: MoodType.happy,
createdAt: DateTime.now(),
updatedAt: DateTime.now(),
);
// 第一次记录成功
final result1 = await recordMood(mood);
expect(result1, isNull); // null 表示成功
// 第二次记录被拒绝
final result2 = await recordMood(mood);
expect(result2, isA<ValidationFailure>());
expect(result2!.message, contains('一分钟内已记录'));
});
}
运行速度对比:重构前的集成测试需要约 2.3 秒(Hive 初始化 1.8 秒 + 数据操作 0.5 秒),重构后的单元测试只需要约 0.05 秒——快了约 46 倍。
13.2 可维护性:模块化降低认知负载
当一个开发者需要修改"情绪记录的每日上限从 20 条改为 50 条"时,流程是这样的:
| 操作 | 重构前 | 重构后 |
|---|---|---|
| 定位代码 | 在 DiaryPage 的 317 行中搜索 20 |
打开 record_mood.dart(约 40 行) |
| 修改代码 | 修改 DiaryPage 中硬编码的数值 | 修改 UseCase 中的常量 |
| 影响范围评估 | 不确定(20 这个数字可能还在别处出现) | 确定(只有这一个 UseCase 使用该规则) |
| 修改后验证 | 手动在应用中快速记录 21 条情绪 | 运行 RecordMood 的单元测试 |
13.3 团队协作:并行开发成为可能
在 Clean Architecture 的分层结构下,三个开发者可以同时工作:
开发者 A ──► Domain 层 ── 定义 Mood Entity, MoodRepository 接口, UseCase
│
┌─────────┼─────────┐
▼ ▼ ▼
开发者 B 开发者 C 开发者 A (接口定好后)
Data 层 Presentation层 开始写 UseCase 单元测试
(Hive实现) (UI + Bloc)
三个人依赖的是同一份接口契约(Domain 层),不需要等待对方完成。这对于接手项目的实习生尤其友好——他们可以先看 Domain 层的代码理解业务规则,然后再深入具体实现。
十四、鸿蒙平台的兼容性说明
E-Brufen 是一个同时运行在 Android 和 HarmonyOS 上的 Flutter 应用。Clean Architecture 的引入对鸿蒙平台的兼容性没有任何负面影响,反而提升了跨平台的一致性。
14.1 使用 Hive CE 而非 Hive 原版
由于鸿蒙 Flutter 的底层文件系统实现与标准 Android 不同,E-Brufen 从项目第一天就使用了 hive_ce(Community Edition)而不是官方的 hive。在 Clean Architecture 重构中,这一点被很好地隔离在 Data 层内部:
// Data 层的 import —— 只在 datasource 实现文件中出现
import 'package:hive_ce/hive.dart'; // 只有这一个文件需要知道 Hive CE 的存在
// Domain 层 —— 完全不知道 Hive 的存在
// Presentation 层 —— 完全不知道 Hive 的存在
如果将来鸿蒙 Flutter 生态成熟,出现了更适合的存储方案(比如鸿蒙官方的分布式数据库插件),我们只需要替换 MoodLocalDataSourceImpl,其他 12 个文件完全不需要改动。
14.2 鸿蒙平台的异步初始化
鸿蒙的 Stage 模型要求应用在 onCreate 中完成初始化。E-Brufen 的 main() 函数中使用了分步初始化(第 1 步 Hive、第 2 步 Settings、第 3 步 MoodStorage),重构后这套初始化流程依然适用,只是初始化的对象从 MoodStorage 变成了 MoodLocalDataSourceImpl + MoodRepositoryImpl:
// main.dart —— 鸿蒙兼容的分步初始化
void main() async {
WidgetsFlutterBinding.ensureInitialized();
String? errorStep;
try {
errorStep = 'Hive init';
await Hive.initFlutter();
errorStep = 'DataSource init';
final dataSource = MoodLocalDataSourceImpl();
await dataSource.init();
errorStep = 'Repository init';
final repository = MoodRepositoryImpl(dataSource: dataSource);
// ... 继续初始化 UseCase 和 Bloc
errorStep = null;
runApp(EBrufenApp(moodBloc: moodBloc));
} catch (e) {
runApp(_ErrorApp(errorStep ?? 'unknown', e.toString()));
}
}
14.3 鸿蒙原生插件桥接的处理
E-Brufen 的白噪音功能通过 OhosAudioPlayer 调用鸿蒙原生 AVPlayer。这个桥接代码位于 lib/data/ohos_audio.dart,重构后它也应该被归于 Data 层的 datasources/ 目录下,例如 lib/data/datasources/audio_player_datasource.dart。同时,Domain 层应该定义一个 AudioPlayerRepository 接口来抽象"播放音频"的概念,使得白噪音页面不需要直接依赖鸿蒙的 MethodChannel。
十五、总结:什么时候该用,什么时候不该用
Clean Architecture 不是银弹。它是一套有成本的工程规范。在 15 个章节的深入分析之后,让我们做一个诚实的总结。
15.1 Clean Architecture 适合的场景
- 项目预计迭代 6 个月以上。如果是一个长期维护的产品,初期的架构投入会在后续 10 次以上的迭代中回本。
- 底层存储或网络层可能会被替换。Hive → 鸿蒙 RDB、REST API → GraphQL、本地存储 → 云同步——这些替换在有 Clean Architecture 保护时成本极低。
- 团队人数 ≥ 3 人。多人协作时代码边界清晰是刚需,否则每次 merge 都是一场灾难。
- 需要高单元测试覆盖率。如果你的团队有测试覆盖率指标(比如 ≥ 80%),Clean Architecture 的分层是实现这个目标的必要条件。
15.2 Clean Architecture 不适合的场景
- MVP / 原型验证项目。当你的目标是 2 周内做出一个可演示的原型时,Clean Architecture 的文件和间接层会增加开发摩擦。E-Brufen 的早期版本(前 50 篇文章构建的版本)刻意选择了简单的结构,因为那时需求还在验证阶段。
- 单人开发 + 需求稳定。如果你一个人维护一个功能不再大变的工具类应用,简单的 MVC 结构就够了。
- 团队对架构模式不熟悉。Clean Architecture 的学习曲线是真实的。如果团队中大多数成员不理解依赖倒置原则,那么先学会走路再跑步。
15.3 E-Brufen 的架构演进路线
E-Brufen 的架构不是一步到位的。它经历了三个阶段:
第一阶段(文章 1-20):
简单 MVC:models/ + pages/ + data/
目标:快速验证产品概念
第二阶段(文章 21-50):
引入 ChangeNotifier + 抽取 Widgets
目标:提升状态管理的清晰度
第三阶段(本文及后续):
全面迁移 Clean Architecture
目标:为长期维护和开源协作建立工程规范
这个渐进式的演进路径是刻意设计的。我们从不认为 Clean Architecture 应该从项目第一天就引入——就像你不会在车库里只有一辆自行车的时候就建一个 4S 店。但随着 E-Brufen 功能越来越多、代码越来越复杂,建筑升级是必要的。
15.4 核心原则回顾
如果用三句话总结 Clean Architecture 在 Flutter 中的落地:
- Domain 层定义"做什么":实体、接口、用例——纯 Dart,零依赖,这是你业务的源代码。
- Data 层负责"怎么做":实现 Domain 层的接口,管理 Hive、数据库、网络请求——这些是实现细节,可以被替换。
- Presentation 层负责"怎么展示":Bloc + Widget + Page——它只看 Domain 层的类型,不知道 Data 层的存在。
这三条规则看似简单,但真正落地到 Flutter 项目中需要大量的工程实践和细节决策。希望这篇 15 章节的文章能为你提供一个可操作的参考。
项目源码:https://gitcode.com/PengXiansheng/E-Brufen
全系列索引:README.md
作者简介:E-Brufen Dev,Flutter & 鸿蒙开发者,AtomGit Flutter 鸿蒙客户端 E-Brufen 的作者。专注于跨平台移动应用开发与软件架构设计。全系列 100 篇技术博客文章已在 CSDN、AtomGit 同步发布。
更多推荐




所有评论(0)