关于 layered-architecture
面向 Flutter 应用开发者的分层单仓库架构指南,将数据、仓储、业务逻辑和展示划分为独立层,并规定依赖方向与包边界。内容涵盖数据转换、依赖注入、应用启动配置及常见工作流。
主要功能
- 01四层职责与依赖方向
- 02数据与仓储独立 Dart 包
- 03构造函数注入外部数据源
- 04响应模型转换为领域模型
- 05单仓库路径依赖与导出规范
适用场景
- 01为 Flutter 单仓库规划功能结构
- 02新增 API 客户端或数据源包
- 03实现仓储及数据模型转换
- 04在应用启动时组装各层依赖
MCP 相关
指南描述了使用 Very Good CLI MCP 服务的 `create dart_package` 和 `test` 工具来创建及测试包;也提供未连接该 MCP 服务时的命令行替代方式。
安装
复制下面的命令,在终端中运行。
npx skills add https:/ / github.com/ verygoodopensource/ vgv-ai-flutter-plugin --skill layered-architecture安装后在 Claude Code 中输入/layered-architecture确认已加载。
包含的文件
除 SKILL.md 外,安装时一起下载的参考资料和脚本。
agents/openai.yamlreferences/data-flow.mdmodel-transformation.mdpubspec.mdtesting.mdworked-example.md详细介绍
分层架构
适用于 Flutter 应用的分层单仓库架构——四个层作为独立的 Dart 包组织,依赖关系严格保持单向。
核心规范
所有分层架构工作都应遵循以下规范:
- 四个层——Data、Repository、Business Logic、Presentation——只要某个功能的 repository 读取外部数据源,该功能就应涵盖全部四层
- 依赖单向流动——Presentation → Business Logic → Repository → Data——不得跳过或反转任何一层
- Data 层和 Repository 层位于
packages/——每一层都是独立的 Dart 包,并有自己的pubspec.yaml - Business Logic 层和 Presentation 层位于
lib/——在应用中按功能组织 - Data 层包不包含任何领域/业务逻辑——它们必须能在无关项目中复用
- Repository 之间不得相互依赖——repository 永远不能导入其他 repository
- Data 或 Repository 包中不得使用 Flutter SDK——使用
very_good_cliMCP 服务的create dart_package工具搭建 - 每个领域对应一个 repository——
user_repository、weather_repository、auth_repository - 本地包使用路径依赖——同一仓库中的包不得使用
git:或 pub 版本引用 - 每个包边界都要提供 Barrel 导出——使用者不得直接导入
src/ - Repository 通过构造函数接收所有外部数据源——数据客户端或
FirebaseAuth.instance之类的 SDK 对象应在 bootstrap 中创建并传入,不得在 repository 内部创建或设置默认值 - 应用 bootstrap 负责接通所有层——
main_<flavor>.dart创建客户端和 repositories,并通过RepositoryProvider提供它们 - 只有应用入口点和 bootstrap 可以导入 Data 包——blocs、widgets 和应用测试都通过 repository 访问 Data
- Dart 3.13 主构造函数——在 Dart 3.13+ 基线上,将模型和 widget 字段声明为主构造函数声明参数(
class const User(final String id, final String name) extends Equatable),而不是使用this.field;仅在低于 3.13 的版本中保留经典形式
跨运行环境回退方案。 此技能通过 Very Good CLI MCP 服务搭建和测试包。如果宿主环境没有此插件的 Bash hooks,也未连接该 MCP 服务,请直接运行对应的
very_good create dart_package …、very_good packages get和very_good test命令。
架构概览
| 层 | 职责 | 位置 | 依赖项 | 示例 |
|---|---|---|---|---|
| Data | 外部通信——API 调用、本地存储、平台插件 | packages/<name>_api_client/ | 仅外部包 | user_api_client、local_storage_client |
| Repository | 数据编排——组合数据源、转换模型、缓存 | packages/<name>_repository/ | 零个或多个 Data 层包 | user_repository、weather_repository |
| Business Logic | 状态管理——处理用户操作、发出状态变更 | lib/<feature>/bloc/ 或 lib/<feature>/cubit/ | Repository 层 | LoginBloc、ProfileCubit |
| Presentation | UI——widgets、页面、视图、布局 | lib/<feature>/view/ | Business Logic 层 | LoginPage、ProfileView |
┌─────────────────────────────────────────────┐
│ Presentation │
│ (lib/<feature>/view/) │
└──────────────────┬──────────────────────────┘
│ reads state / dispatches events
┌──────────────────▼──────────────────────────┐
│ Business Logic │
│ (lib/<feature>/bloc/) │
└──────────────────┬──────────────────────────┘
│ calls repository methods
┌──────────────────▼──────────────────────────┐
│ Repository │
│ (packages/<name>_repository/) │
└──────────────────┬──────────────────────────┘
│ calls data clients
┌──────────────────▼──────────────────────────┐
│ Data │
│ (packages/<name>_api_client/) │
└─────────────────────────────────────────────┘
单仓库结构
功能位于 lib/,每个功能各有一个目录,并拆分为 bloc|cubit/ 和 view/,同时提供一个 barrel 文件。各层位于 packages/,每个数据源对应一个包,每个 repository 也对应一个包。第二个数据包和第二个 repository 沿用第一个的结构。
my_app/
├── lib/
│ ├── app/
│ │ ├── app.dart # Barrel file
│ │ └── view/
│ │ └── app.dart # App widget with MultiRepositoryProvider
│ ├── login/ # Feature: login
│ │ ├── login.dart # Barrel file
│ │ ├── bloc/
│ │ │ ├── login_bloc.dart
│ │ │ ├── login_event.dart
│ │ │ └── login_state.dart
│ │ └── view/
│ │ ├── login_page.dart # Page provides Bloc
│ │ └── login_view.dart # View consumes state
│ ├── profile/ # Feature: profile
│ ├── main_development.dart # Flavor entrypoint
│ ├── main_staging.dart
│ └── main_production.dart
├── packages/
│ ├── auth_api_client/ # Data layer: auth API
│ │ ├── lib/
│ │ │ ├── auth_api_client.dart # Barrel file
│ │ │ └── src/
│ │ │ ├── auth_api_client.dart
│ │ │ └── models/
│ │ │ ├── models.dart
│ │ │ └── auth_response.dart
│ │ └── pubspec.yaml
│ ├── local_storage_client/ # Data layer: local storage
│ ├── auth_repository/ # Repository layer: auth
│ │ ├── lib/
│ │ │ ├── auth_repository.dart # Barrel file
│ │ │ └── src/
│ │ │ ├── auth_repository.dart
│ │ │ └── models/
│ │ │ ├── models.dart
│ │ │ └── user.dart # Domain model
│ │ └── pubspec.yaml
│ └── user_repository/ # Repository layer: user
├── test/
│ └── ... # Mirrors lib/ structure
└── pubspec.yaml # Root app pubspec
数据层
数据层负责处理所有外部通信。每个数据包封装一个外部数据源(REST API、本地数据库、平台插件),并公开类型化方法和响应模型。
规则:
- 模型表示外部数据的结构,必须与 API/存储架构完全一致
- 不导入 Flutter — 使用
very_good_cliMCP 服务的create dart_package工具 - 通过构造函数注入 HTTP 客户端,以便测试
- 响应模型使用
fromJson/toJson工厂 - 通过统一导出文件导出所有内容 — 绝不公开
src/
模式:数据客户端类
通过构造函数注入 HTTP 客户端,以便测试。返回类型化响应模型,绝不返回原始 JSON。
/// HTTP client for the User API.
class UserApiClient {
// http.Client injected — tests pass a mock, production gets a real client
UserApiClient({
required String baseUrl,
http.Client? httpClient,
}) : _baseUrl = baseUrl,
_httpClient = httpClient ?? http.Client();
final String _baseUrl;
final http.Client _httpClient;
/// Every method returns a typed response model.
Future<UserResponse> getUser(String userId) async {
final response = await _httpClient.get(
Uri.parse('$_baseUrl/users/$userId'),
);
if (response.statusCode != 200) {
throw UserApiException(response.statusCode, response.body);
}
return UserResponse.fromJson(
json.decode(response.body) as Map<String, dynamic>,
);
}
}
完整的 user_api_client 软件包示例(包含 pubspec、统一导出文件、响应模型和异常类),请参阅 worked-example.md。
仓储层
仓储层负责协调数据源并公开领域模型。每个仓储会组合所需的数据客户端,将响应模型转换为领域模型,并为业务逻辑层提供简洁的 API。
规则:
- 仓储之间不得相互依赖 — 各仓储彼此隔离
- 不依赖 Flutter SDK — 使用
very_good_cliMCP 服务的create dart_package工具 - 领域模型位于仓储软件包中,而非数据软件包中
- 将数据模型转换为领域模型 — 绝不向上游泄露 API 响应结构
- 没有外部数据源的仓储(例如内存中的会话状态)不接收任何构造函数参数
模式:领域模型 + 仓储转换
领域模型扩展 Equatable,表示应用内部的数据结构,与 API 响应结构不同。仓储方法负责在两者之间进行转换。
/// Domain model — lives in the repository package, NOT the data package.
/// Fields match the app's needs, not the API schema.
class const User({
required final String id,
required final String email,
required final String displayName,
final String? avatarUrl,
}) extends Equatable {
@override
List<Object?> get props => [id, email, displayName, avatarUrl];
}
/// Repository accepts data client via constructor — never creates its own.
class UserRepository {
const UserRepository({
required UserApiClient userApiClient,
}) : _userApiClient = userApiClient;
final UserApiClient _userApiClient;
/// Transforms UserResponse (API shape) → User (domain shape).
Future<User> getUser(String userId) async {
final response = await _userApiClient.getUser(userId);
return User(
id: response.id,
email: response.email,
displayName: response.displayName,
avatarUrl: response.avatarUrl,
);
}
}
完整的 user_repository 软件包示例(包含 pubspec、统一导出文件和错误处理),请参阅 worked-example.md。有关数据模型与领域模型之间转换模式的详细说明,请参阅 model-transformation.md。
依赖关系图
每个 pubspec.yaml 中的路径依赖都指向同一个方向。数据软件包仅声明外部软件包。仓储软件包声明对其数据软件包的路径依赖。根应用声明其仓储软件包,以及启动过程会构造的每个数据软件包。main_<flavor>.dart 会导入每个客户端以便注入,而 package:very_good_analysis 中的 depend_on_referenced_packages 要求声明所有导入的软件包。
导入规则决定了分层边界。 应用一旦声明了某个数据软件包,lint 就不会再阻止 bloc 导入它。边界由核心规范中的导入规则界定:只有应用的入口点和启动代码可以导入数据软件包。
# packages/user_api_client/pubspec.yaml — external packages only
dependencies:
http: ^1.4.0
# packages/user_repository/pubspec.yaml — path dependency on its data package
dependencies:
user_api_client:
path: ../user_api_client
# pubspec.yaml: repositories, plus the data packages bootstrap constructs
dependencies:
user_api_client:
path: packages/user_api_client
user_repository:
path: packages/user_repository
有关这三个文件的完整内容,以及如何检查导入边界(包括哪些文件算作入口点和启动代码),请参阅 references/pubspec.md。
数据流
Presentation 分发事件 → Bloc 调用仓储 → 仓储调用数据客户端并返回领域模型 → BlocBuilder 在新状态下重建。
各层代码请参阅 references/data-flow.md。
应用启动
main_<flavor>.dart 导入并构造每个数据客户端和仓储,然后将仓储传递给 App 组件,由其通过 MultiRepositoryProvider 提供这些仓储。不同风味只改变配置(基本 URL、API 密钥),不改变依赖装配方式。
// lib/main_development.dart
import 'package:auth_api_client/auth_api_client.dart';
import 'package:auth_repository/auth_repository.dart';
import 'package:flutter/material.dart';
import 'package:my_app/app/app.dart';
import 'package:user_api_client/user_api_client.dart';
import 'package:user_repository/user_repository.dart';
void main() {
WidgetsFlutterBinding.ensureInitialized();
const baseUrl = 'https://api.dev.example.com';
// Data layer
final authApiClient = AuthApiClient(baseUrl: baseUrl);
final userApiClient = UserApiClient(baseUrl: baseUrl);
// Repository layer
final authRepository = AuthRepository(authApiClient: authApiClient);
final userRepository = UserRepository(userApiClient: userApiClient);
runApp(
App(
authRepository: authRepository,
userRepository: userRepository,
),
);
}
完整的 main() 以及带有 MultiRepositoryProvider 的 App 组件,请参阅 references/worked-example.md。
反模式
| 反模式 | 问题 | 正确做法 |
|---|---|---|
| Widget 直接调用 API 客户端 | 绕过 Repository 和业务逻辑层——没有数据转换,也没有状态管理 | Widget 分发事件 → Bloc 调用 Repository → Repository 调用 API 客户端 |
| Repository 导入另一个 Repository | 形成循环或错综复杂的依赖关系图——破坏独立测试能力 | 每个 Repository 都独立自包含;如有需要,在 Bloc 层组合数据 |
| 在数据层中放置领域模型 | 将外部 API 结构与内部领域耦合——API 变更会影响整个应用 | 数据层使用响应模型;Repository 层使用经过转换的领域模型 |
| 在 Repository 中编写业务逻辑 | Repository 会变成难以测试的单体,同时混杂流程协调与业务规则 | Repository 转换数据;Bloc/Cubit 包含所有业务规则 |
对本地 package 使用 git: 或 pub 版本 | 破坏 monorepo——变更需要经历发布/推送流程,而非即时本地修改 | monorepo 中的所有 package 都使用 path: 依赖 |
| 在数据层/Repository package 中导入 Flutter | 导致 package 无法用于纯 Dart 环境(CLI 工具、服务) | 使用 very_good_cli MCP 服务的 create dart_package 工具搭建脚手架——无需依赖 Flutter SDK |
| 用一个庞大的 Repository 处理所有事务 | 变成职责过多的上帝对象——无法单独测试 | 每个领域边界对应一个 Repository(user_repository、settings_repository) |
直接导入 src/ | 破坏封装——使用方依赖内部结构 | 通过 barrel 文件导出公共 API;导入 package,绝不导入 src/ 路径 |
常见工作流
添加新的数据源
- 使用
very_good_cliMCP 服务的create dart_package工具搭建 package 脚手架:<name>_api_client --output-directory packages - 在
pubspec.yaml中添加外部依赖(例如http、json_annotation) - 在
lib/src/models/中创建响应模型,并实现fromJson/toJson - 创建 barrel 文件
lib/src/models/models.dart,导出所有模型 - 在
lib/src/<name>_api_client.dart中实现客户端类 - 创建 package 的 barrel 文件
lib/<name>_api_client.dart,导出src/内容 - 在
test/中编写与lib/结构对应的单元测试——参见 testing 技能 - 使用
very_good_cliMCP 服务的test工具对 package 目录运行测试——传入directory: 'packages/<name>_api_client'以限定运行范围
添加新的 Repository
- 使用
very_good_cliMCP 服务的create dart_package工具搭建 package 脚手架:<name>_repository --output-directory packages - 在
pubspec.yaml中为数据层 package 添加路径依赖 - 在依赖项中添加
equatable,供领域模型使用 - 在
lib/src/models/中创建继承自Equatable的领域模型 - 创建 barrel 文件
lib/src/models/models.dart - 实现 Repository 类,并通过构造函数注入数据客户端
- 添加从响应模型到领域模型的转换逻辑
- 创建 package 的 barrel 文件
lib/<name>_repository.dart - 使用模拟的数据客户端编写单元测试——参见 testing 技能
将 Repository 连接到功能
- 在根目录的
pubspec.yaml中添加对 Repository package 及其依赖的每个数据 package 的路径依赖 - 在
main_<flavor>.dart中创建数据客户端和 Repository,并将 Repository 传递给App - 在
App的MultiRepositoryProvider中添加RepositoryProvider.value - 注入 Repository,创建 Bloc/Cubit——参见 bloc 技能
- 使用
BlocProvider和BlocBuilder构建 Page/View——参见 bloc 技能 - 在
lib/和test/中搜索package:<data_package>/导入。根据 pubspec 引用的 Import Boundary 部分定义,只有入口点或引导文件中可以存在匹配项;全部符合时,即表示工作完成
附加资源
- 完整的示例和 pubspec 参考
- 数据流演练 — 各层的请求路径及代码
- 模型转换模式 — 数据模型与领域模型的转换
- 包级测试 — 独立测试数据客户端和仓库
- 有关 Bloc/Cubit 模式和 Page/View 分离,请参阅 bloc 技能
- 要搭建项目脚手架,请使用
very_good_cliMCP 服务的create dart_package工具 - 有关数据客户端、仓库和 Blocs 的测试,请参阅 testing 技能