mcpField
L
layered-architecture

layered-architecture

github.com/verygoodopensource开发17571 次安装今天更新
代码仓库

关于 layered-architecture

面向 Flutter 应用开发者的分层单仓库架构指南,将数据、仓储、业务逻辑和展示划分为独立层,并规定依赖方向与包边界。内容涵盖数据转换、依赖注入、应用启动配置及常见工作流。

主要功能

  1. 01四层职责与依赖方向
  2. 02数据与仓储独立 Dart 包
  3. 03构造函数注入外部数据源
  4. 04响应模型转换为领域模型
  5. 05单仓库路径依赖与导出规范

适用场景

  1. 01为 Flutter 单仓库规划功能结构
  2. 02新增 API 客户端或数据源包
  3. 03实现仓储及数据模型转换
  4. 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.yaml
references/
data-flow.md
model-transformation.md
pubspec.md
testing.md
worked-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_cli MCP 服务的 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
PresentationUI——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_cli MCP 服务的 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_cli MCP 服务的 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/ 路径

常见工作流

添加新的数据源
  1. 使用 very_good_cli MCP 服务的 create dart_package 工具搭建 package 脚手架:<name>_api_client --output-directory packages
  2. 在 pubspec.yaml 中添加外部依赖(例如 http、json_annotation)
  3. 在 lib/src/models/ 中创建响应模型,并实现 fromJson/toJson
  4. 创建 barrel 文件 lib/src/models/models.dart,导出所有模型
  5. 在 lib/src/<name>_api_client.dart 中实现客户端类
  6. 创建 package 的 barrel 文件 lib/<name>_api_client.dart,导出 src/ 内容
  7. 在 test/ 中编写与 lib/ 结构对应的单元测试——参见 testing 技能
  8. 使用 very_good_cli MCP 服务的 test 工具对 package 目录运行测试——传入 directory: 'packages/<name>_api_client' 以限定运行范围
添加新的 Repository
  1. 使用 very_good_cli MCP 服务的 create dart_package 工具搭建 package 脚手架:<name>_repository --output-directory packages
  2. 在 pubspec.yaml 中为数据层 package 添加路径依赖
  3. 在依赖项中添加 equatable,供领域模型使用
  4. 在 lib/src/models/ 中创建继承自 Equatable 的领域模型
  5. 创建 barrel 文件 lib/src/models/models.dart
  6. 实现 Repository 类,并通过构造函数注入数据客户端
  7. 添加从响应模型到领域模型的转换逻辑
  8. 创建 package 的 barrel 文件 lib/<name>_repository.dart
  9. 使用模拟的数据客户端编写单元测试——参见 testing 技能
将 Repository 连接到功能
  1. 在根目录的 pubspec.yaml 中添加对 Repository package 及其依赖的每个数据 package 的路径依赖
  2. 在 main_<flavor>.dart 中创建数据客户端和 Repository,并将 Repository 传递给 App
  3. 在 App 的 MultiRepositoryProvider 中添加 RepositoryProvider.value
  4. 注入 Repository,创建 Bloc/Cubit——参见 bloc 技能
  5. 使用 BlocProvider 和 BlocBuilder 构建 Page/View——参见 bloc 技能
  6. 在 lib/ 和 test/ 中搜索 package:<data_package>/ 导入。根据 pubspec 引用的 Import Boundary 部分定义,只有入口点或引导文件中可以存在匹配项;全部符合时,即表示工作完成

附加资源

  • 完整的示例和 pubspec 参考
  • 数据流演练 — 各层的请求路径及代码
  • 模型转换模式 — 数据模型与领域模型的转换
  • 包级测试 — 独立测试数据客户端和仓库
  • 有关 Bloc/Cubit 模式和 Page/View 分离,请参阅 bloc 技能
  • 要搭建项目脚手架,请使用 very_good_cli MCP 服务的 create dart_package 工具
  • 有关数据客户端、仓库和 Blocs 的测试,请参阅 testing 技能

评论

支持 Markdown