Flutter 整洁架构:可扩展应用的实践指南

6 阅读8分钟

引言

本文旨在为采用 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 序列化相关的注解, 如 @JsonKeyfromJson/toJson工厂构造函数。
    • Use Cases (Interactors): 代表一个单一的业务操作流程(例如 LoginUseCase, GetProductDetailsUseCase)。它们编排对 Repositories 的调用,是业务逻辑的执行者。
    • Repository Abstractions: 数据操作的契约(抽象类/接口)。例如 abstract class AuthRepository,它定义了 login 方法,但不关心数据来源。
  • 依赖规则: 无依赖。只包含纯 Dart 代码,不依赖任何其他层或第三方框架。

2. Data Layer (数据层) - 数据的协调者

  • 职责: 实现 Domain 层定义的 Repository 接口。它作为 Domain 层和 Infrastructure 层之间的桥梁,负责协调数据源,实现如缓存策略、数据合并等逻辑。
  • 内容:
    • Repository Implementations: 例如 AuthRepositoryImpl。它实现了 AuthRepository 接口,并决定是从网络还是本地获取数据。
  • 依赖规则: 依赖 Domain Layer (为了实现接口) 和 Infrastructure Layer (为了调用具体的数据源)。

3. Infrastructure Layer (基础设施层) - 连接外部世界

  • 职责: 提供与外部世界交互的所有技术细节实现。这是所有“脏活累活”的隔离区。
  • 内容:
    • Data Sources: 与具体数据终端交互的类。
      • RemoteDataSource (例如 UserApiDataSource): 使用 diohttp 进行网络请求。
      • LocalDataSource (例如 UserDbDataSource): 使用 hive, drift, shared_preferences 操作本地存储。
    • Models (DTOs): 数据传输对象。他们是外部数据源(如 JSON API)的直接映射。这里是 @JsonKey, fromJsontoJson 等序列化/反序列化逻辑的唯一归属地。 Model 的主要职责之一就是将不稳定的, 可能不规范的外部数据,转换为稳定,干净的内部业务实体(Entity)。即: 使用 freezedjson_serializable 创建,负责序列化/反序列化。它们必须有一个 toEntity() 方法,将数据转换为 Domain 层的 Entity。
    • Service Wrappers: 对第三方 SDK 的封装(适配器),例如 FirebaseAnalyticsService, GoogleSignInService
    • Platform Channel Handlers: 与原生 iOS/Android 代码交互的实现。
  • 依赖规则: 依赖外部库(dio, firebase_core 等),但不依赖项目中的任何其他层。

4. Presentation Layer (表现层) - 用户界面

  • 职责: 展示 UI、响应用户输入、管理 UI 状态。
  • 内容:
    • Pages/Screens & Widgets: Flutter 的 UI 组件。
    • State Management: 使用 RiverpodNotifier / AsyncNotifier。它们调用 Domain 层的 Use Cases,并根据结果更新 UI 状态。
    • Routing: 使用 GoRouter 定义和管理页面导航。
  • 依赖规则: 依赖 Domain Layer (为了调用 Use Cases)。

依赖关系总结: PresentationDomainDataInfrastructure


第二部分:项目结构与模块组织

我们采用“按功能划分 (Feature-first)”的目录结构,以实现高内聚、低耦合。

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 - 接口隔离原则: 使用小而专一的接口。AuthRepositoryProductRepository 是分离的,认证模块无需知道产品模块的任何数据细节。
  • D - 依赖倒置原则: 高层模块不依赖低层模块,二者都依赖于抽象。Riverpod 是实现此原则的完美工具LoginStateNotifier (Presentation) 依赖 LoginUseCase (Domain),LoginUseCase 依赖 AuthRepository (Domain 抽象),而具体的实现 AuthRepositoryImpl (Data) 是在 Provider 中被注入的。

第四部分:功能开发工作流 (A-to-Z)

以开发“登录”功能为例,遵循由内向外的顺序:

  1. 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 方法。
  2. Infrastructure Layer:

    • auth/infrastructure/models/user_model.dart: 用 freezedjson_serializable 创建 UserModel,包含 fromJson/toJsontoEntity() 方法。
    • auth/infrastructure/datasources/auth_remote_data_source.dart: 实现具体的 API 调用,返回 Future<UserModel>
  3. Data Layer:

    • auth/data/repositories/auth_repository_impl.dart: 实现 AuthRepository。它调用 DataSourcetry-catch 捕获异常并转换为自定义的 Failure,将 UserModel 映射为 UserEntity,最后返回 Either
  4. 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() 来触发动作。
  5. Routing:

    • core/routes/app_router.dart 中,为登录页面添加 GoRoute

第五部分:处理高级与真实世界场景

5.1 数据持久化与缓存 (Local vs. Remote)

RepositoryImpl 是缓存策略的决策中心。它会注入 RemoteDataSourceLocalDataSource 两个数据源。在实现方法时,它将按需决定是先读缓存、失败后读网络、成功后写回缓存,还是其他更复杂的策略。

5.2 集成第三方服务 (Adapter Pattern)

严禁业务逻辑直接依赖第三方 SDK。所有第三方服务都必须通过基础设施层Wrapper/Adapter 进行封装。

  • 步骤:
    1. DomainData 层定义一个业务所需的抽象接口(例如 abstract class AnalyticsService { trackEvent(...) })。
    2. Infrastructure 层创建该接口的实现,实现内部调用具体的 SDK(例如 FirebaseAnalyticsServiceImpl)。
    3. 通过 Riverpod 将此实现注入到需要它的地方(通常是 Repository 或 UseCase)。
  • 收益: 易于替换、易于测试(可以 Mock 接口)。

5.3 共享组件库

项目内跨功能复用的 UI 组件(如 PrimaryButton, LoadingIndicator)应放置在顶层的 shared/components 目录中。这些组件应保持纯粹的展示性,不包含任何业务逻辑,也不得依赖任何 features 目录下的内容。

5.4 与原生代码交互 (Platform Channels)

平台通道的交互被严格限制在基础设施层

  1. DataSource (Infrastructure): 创建一个 DataSource 类,在其中定义和调用 MethodChannel
  2. Repository (Data): RepositoryImpl 调用此 DataSource,并将可能抛出的 PlatformException 捕获,转换为 Domain 层的 Failure 对象。
  3. UseCase (Domain): UseCase 调用 Repository 的抽象方法,它对平台交互一无所知,只关心成功或失败的结果。

第六部分:痛点、陷阱与最佳实践

  1. 挑战:模板代码过多

    • 解决方案: 这是为可维护性付出的前期成本。使用 IDE 文件模板或 mason 等 CLI 工具可以一键生成整个 feature 模块的骨架,大幅提升效率。
  2. 陷阱:Entity vs. Model 混淆

    • 最佳实践: Model (Infrastructure) 是 API 的直接反映,负责序列化。Entity (Domain) 是业务的核心表达,纯净且稳定。转换发生在 RepositoryImpl 中,确保 Domain 层和 Presentation 层永远不会接触到 API 的具体实现细节。
  3. 陷阱:层级泄露

    • 纪律: 严禁跨层级的非法 import。Domain 层绝不能 import 'package:flutter/...' 或任何与数据/UI 相关的包。在 Code Review 中对此进行严格审查。使用 analysis_options.yaml 配置 lint 规则来强制执行。
  4. 挑战:Riverpod Provider 管理

    • 最佳实践:
      • 按功能组织: 将 Provider 放在其所属 featureproviders 目录下。
      • 使用 .autoDispose: 对与页面生命周期绑定的 Provider(特别是 StateNotifierProvider)使用 .autoDispose,以防止内存泄漏。
      • 善用 AsyncValue: 在 UI 层使用 asyncValue.when() 来优雅地处理加载、数据和错误三种状态,保证 UI 的完备性。
  5. 挑战:过度工程化

    • 原则: 架构服务于项目。对于极简单、几乎不变的功能(如“关于我们”页面),可以酌情简化,例如让 StateNotifier 直接调用 Repository(省略 UseCase)。但团队需就何时可以破例达成共识。