引言
本文旨在为采用 Flutter 技术栈(Riverpod, GoRouter, Freezed)开发的应用程序,提供一套完整、健壮且可扩展的 Clean Architecture 实施方案。作为一名资深的 iOS 开发者,我深知从传统 MVC 模式演进到分层架构(如 VIPER, MVVM-C)所带来的巨大收益------即可测试性、可维护性和团队协作效率的提升。Clean Architecture 正是这些架构思想的精髓提炼,其与平台无关的核心原则使其在 Flutter 生态中同样表现卓越。
本文档将作为项目开发的"单一事实来源",确保所有团队成员对架构有统一的理解,并遵循一致的最佳实践。
第一部分:架构蓝图:四层模型详解
我们采用以"依赖倒置原则"为核心的洋葱架构,并将其具体化为以下四个层次。核心规则:依赖关系必须由外层指向内层。
1. Domain Layer (领域层) - 应用的心脏
- 职责: 定义应用程序的核心业务规则和业务对象。此层是完全独立的,不包含任何关于 UI、数据库或网络的知识。
- 内容 :
- Entities : 业务对象的核心模型(例如
User
,Product
)。他们代表了应用程序的业务概念, 使用freezed
创建的纯净、不可变的 Dart 类。Entities
本身不应关心数据是如何进行序列化和反序列化的, 因此它不包含任何与 JSON 序列化相关的注解, 如@JsonKey
或fromJson/toJson
工厂构造函数。 - Use Cases (Interactors) : 代表一个单一的业务操作流程(例如
LoginUseCase
,GetProductDetailsUseCase
)。它们编排对 Repositories 的调用,是业务逻辑的执行者。 - Repository Abstractions : 数据操作的契约(抽象类/接口)。例如
abstract class AuthRepository
,它定义了login
方法,但不关心数据来源。
- Entities : 业务对象的核心模型(例如
- 依赖规则 : 无依赖。只包含纯 Dart 代码,不依赖任何其他层或第三方框架。
2. Data Layer (数据层) - 数据的协调者
- 职责: 实现 Domain 层定义的 Repository 接口。它作为 Domain 层和 Infrastructure 层之间的桥梁,负责协调数据源,实现如缓存策略、数据合并等逻辑。
- 内容 :
- Repository Implementations : 例如
AuthRepositoryImpl
。它实现了AuthRepository
接口,并决定是从网络还是本地获取数据。
- Repository Implementations : 例如
- 依赖规则 : 依赖 Domain Layer (为了实现接口) 和 Infrastructure Layer (为了调用具体的数据源)。
3. Infrastructure Layer (基础设施层) - 连接外部世界
- 职责: 提供与外部世界交互的所有技术细节实现。这是所有"脏活累活"的隔离区。
- 内容 :
- Data Sources : 与具体数据终端交互的类。
RemoteDataSource
(例如UserApiDataSource
): 使用dio
或http
进行网络请求。LocalDataSource
(例如UserDbDataSource
): 使用hive
,drift
,shared_preferences
操作本地存储。
- Models (DTOs) : 数据传输对象。他们是外部数据源(如 JSON API)的直接映射。这里是
@JsonKey
,fromJson
和toJson
等序列化/反序列化逻辑的唯一归属地。 Model 的主要职责之一就是将不稳定的, 可能不规范的外部数据,转换为稳定,干净的内部业务实体(Entity
)。即: 使用freezed
和json_serializable
创建,负责序列化/反序列化。它们必须有一个toEntity()
方法,将数据转换为 Domain 层的 Entity。 - Service Wrappers : 对第三方 SDK 的封装(适配器),例如
FirebaseAnalyticsService
,GoogleSignInService
。 - Platform Channel Handlers: 与原生 iOS/Android 代码交互的实现。
- Data Sources : 与具体数据终端交互的类。
- 依赖规则 : 依赖外部库(
dio
,firebase_core
等),但不依赖项目中的任何其他层。
4. Presentation Layer (表现层) - 用户界面
- 职责: 展示 UI、响应用户输入、管理 UI 状态。
- 内容 :
- Pages/Screens & Widgets: Flutter 的 UI 组件。
- State Management : 使用 Riverpod 的
Notifier
/AsyncNotifier
。它们调用 Domain 层的 Use Cases,并根据结果更新 UI 状态。 - Routing : 使用 GoRouter 定义和管理页面导航。
- 依赖规则 : 依赖 Domain Layer (为了调用 Use Cases)。
依赖关系总结 : Presentation
→ Domain
← Data
→ Infrastructure
第二部分:项目结构与模块组织
我们采用"按功能划分 (Feature-first)"的目录结构,以实现高内聚、低耦合。
csharp
lib/
├── core/ # 应用核心,跨功能共享的基础设施和服务
│ ├── di/ # 全局依赖注入 (Providers for Infrastructure Clients)
│ ├── error/ # 自定义 Exceptions 和 Failures (e.g., ServerFailure, CacheFailure)
│ ├── infrastructure/ # 共享的基础设施层
│ │ ├── api/ # Dio client, interceptors, base response models
│ │ ├── db/ # Hive/Drift 数据库初始化和配置
│ │ ├── native/ # 平台通道的通用封装
│ │ └── services/ # 第三方服务的通用封装 (Analytics, Crashlytics)
│ ├── routes/ # GoRouter 配置 (app_router.dart)
│ └── theme/ # App 主题 (app_theme.dart)
│
├── features/ # 按功能划分的模块
│ └── auth/ # 认证功能模块
│ ├── domain/
│ │ ├── entities/ user_entity.dart
│ │ ├── repositories/ auth_repository.dart (Interface)
│ │ └── usecases/ login_usecase.dart
│ │
│ ├── data/
│ │ └── repositories/ auth_repository_impl.dart
│ │
│ ├── infrastructure/
│ │ ├── datasources/ auth_remote_data_source.dart, auth_local_data_source.dart
│ │ └── models/ user_model.dart (@freezed with json_serializable)
│ │
│ └── presentation/
│ ├── providers/ auth_providers.dart, login_state_notifier.dart
│ ├── screens/ login_screen.dart
│ └── widgets/ login_form.dart
│
├── shared/ # 项目内部共享的非核心代码
│ ├── components/ # 共享的自定义UI组件 (e.g., PrimaryButton, EmptyStateWidget)
│ └── utils/ # 共享的工具类 (e.g., Formatters, Validators)
│
└── main.dart # App 入口,初始化核心依赖并运行 App
第三部分:应用 SOLID 原则
- S - 单一职责: 每个类/模块只做一件事。UseCase 只封装一个业务流程,Repository 只管理一类数据,DataSource 只对接一个数据源。
- O - 开闭原则: 对扩展开放,对修改关闭。当需要支持新的登录方式(如 Apple Sign-In)时,我们只需添加一个新的 DataSource 和在 Repository 中增加逻辑,而不用修改现有的 UseCase 或 Presentation 层。
- L - 里氏替换原则: Repository 实现类必须能完全替代其抽象接口。Dart 的类型系统为我们提供了保障。
- I - 接口隔离原则 : 使用小而专一的接口。
AuthRepository
和ProductRepository
是分离的,认证模块无需知道产品模块的任何数据细节。 - D - 依赖倒置原则 : 高层模块不依赖低层模块,二者都依赖于抽象。Riverpod 是实现此原则的完美工具 。
LoginStateNotifier
(Presentation) 依赖LoginUseCase
(Domain),LoginUseCase
依赖AuthRepository
(Domain 抽象),而具体的实现AuthRepositoryImpl
(Data) 是在 Provider 中被注入的。
第四部分:功能开发工作流 (A-to-Z)
以开发"登录"功能为例,遵循由内向外的顺序:
-
Domain Layer:
auth/domain/entities/user_entity.dart
: 用freezed
定义UserEntity
。auth/domain/repositories/auth_repository.dart
: 定义abstract class AuthRepository
及其login
方法,返回Future<Either<Failure, UserEntity>>
。auth/domain/usecases/login_usecase.dart
: 创建LoginUseCase
,构造函数接收AuthRepository
,并实现call
方法。
-
Infrastructure Layer:
auth/infrastructure/models/user_model.dart
: 用freezed
和json_serializable
创建UserModel
,包含fromJson/toJson
和toEntity()
方法。auth/infrastructure/datasources/auth_remote_data_source.dart
: 实现具体的 API 调用,返回Future<UserModel>
。
-
Data Layer:
auth/data/repositories/auth_repository_impl.dart
: 实现AuthRepository
。它调用DataSource
,try-catch
捕获异常并转换为自定义的Failure
,将UserModel
映射为UserEntity
,最后返回Either
。
-
Presentation Layer:
auth/presentation/providers/auth_providers.dart
:- 创建
authRemoteDataSourceProvider
。 - 创建
authRepositoryProvider
,在其中ref.watch
数据源 Provider 并注入。 - 创建
loginUseCaseProvider
,在其中ref.watch
仓库 Provider 并注入。 - 创建
loginStateProvider = StateNotifierProvider.autoDispose<...>
,注入 UseCase。
- 创建
auth/presentation/notifiers/login_state_notifier.dart
: 创建StateNotifier
,管理 UI 状态(如AsyncValue
),并提供login
方法来执行 UseCase。auth/presentation/screens/login_screen.dart
: 使用ConsumerWidget
,通过ref.watch(loginStateProvider)
来构建 UI,通过ref.read(loginStateProvider.notifier).login()
来触发动作。
-
Routing:
- 在
core/routes/app_router.dart
中,为登录页面添加GoRoute
。
- 在
第五部分:处理高级与真实世界场景
5.1 数据持久化与缓存 (Local vs. Remote)
RepositoryImpl
是缓存策略的决策中心。它会注入 RemoteDataSource
和 LocalDataSource
两个数据源。在实现方法时,它将按需决定是先读缓存、失败后读网络、成功后写回缓存,还是其他更复杂的策略。
5.2 集成第三方服务 (Adapter Pattern)
严禁业务逻辑直接依赖第三方 SDK。所有第三方服务都必须通过基础设施层 的 Wrapper/Adapter 进行封装。
- 步骤 :
- 在 Domain 或 Data 层定义一个业务所需的抽象接口(例如
abstract class AnalyticsService { trackEvent(...) }
)。 - 在 Infrastructure 层创建该接口的实现,实现内部调用具体的 SDK(例如
FirebaseAnalyticsServiceImpl
)。 - 通过 Riverpod 将此实现注入到需要它的地方(通常是 Repository 或 UseCase)。
- 在 Domain 或 Data 层定义一个业务所需的抽象接口(例如
- 收益: 易于替换、易于测试(可以 Mock 接口)。
5.3 共享组件库
项目内跨功能复用的 UI 组件(如 PrimaryButton
, LoadingIndicator
)应放置在顶层的 shared/components
目录中。这些组件应保持纯粹的展示性,不包含任何业务逻辑,也不得依赖任何 features
目录下的内容。
5.4 与原生代码交互 (Platform Channels)
平台通道的交互被严格限制在基础设施层。
- DataSource (Infrastructure) : 创建一个
DataSource
类,在其中定义和调用MethodChannel
。 - Repository (Data) :
RepositoryImpl
调用此DataSource
,并将可能抛出的PlatformException
捕获,转换为 Domain 层的Failure
对象。 - UseCase (Domain): UseCase 调用 Repository 的抽象方法,它对平台交互一无所知,只关心成功或失败的结果。
第六部分:痛点、陷阱与最佳实践
-
挑战:模板代码过多
- 解决方案 : 这是为可维护性付出的前期成本。使用 IDE 文件模板或
mason
等 CLI 工具可以一键生成整个 feature 模块的骨架,大幅提升效率。
- 解决方案 : 这是为可维护性付出的前期成本。使用 IDE 文件模板或
-
陷阱:Entity vs. Model 混淆
- 最佳实践 :
Model
(Infrastructure) 是 API 的直接反映,负责序列化。Entity
(Domain) 是业务的核心表达,纯净且稳定。转换发生在RepositoryImpl
中,确保 Domain 层和 Presentation 层永远不会接触到 API 的具体实现细节。
- 最佳实践 :
-
陷阱:层级泄露
- 纪律 : 严禁跨层级的非法
import
。Domain 层绝不能import 'package:flutter/...'
或任何与数据/UI 相关的包。在 Code Review 中对此进行严格审查。使用analysis_options.yaml
配置 lint 规则来强制执行。
- 纪律 : 严禁跨层级的非法
-
挑战:Riverpod Provider 管理
- 最佳实践 :
- 按功能组织 : 将 Provider 放在其所属
feature
的providers
目录下。 - 使用
.autoDispose
: 对与页面生命周期绑定的 Provider(特别是StateNotifierProvider
)使用.autoDispose
,以防止内存泄漏。 - 善用
AsyncValue
: 在 UI 层使用asyncValue.when()
来优雅地处理加载、数据和错误三种状态,保证 UI 的完备性。
- 按功能组织 : 将 Provider 放在其所属
- 最佳实践 :
-
挑战:过度工程化
- 原则 : 架构服务于项目。对于极简单、几乎不变的功能(如"关于我们"页面),可以酌情简化,例如让
StateNotifier
直接调用Repository
(省略 UseCase)。但团队需就何时可以破例达成共识。
- 原则 : 架构服务于项目。对于极简单、几乎不变的功能(如"关于我们"页面),可以酌情简化,例如让