mcpField
P
project-structure-and-packages

project-structure-and-packages

github.com/zakariaf开发63151 次安装5 周前更新
代码仓库

关于 project-structure-and-packages

面向 Flutter 与 Dart 项目的结构设计技能,帮助开发者规划目录、依赖层和测试布局。它默认单 Flutter 包,并以依赖清单和导入边界落实分层。

主要功能

  1. 01默认采用单 Flutter 包
  2. 02按功能组织 lib/features/
  3. 03限制依赖向下流动
  4. 04用 pubspec.yaml 审核依赖
  5. 05镜像源码布局组织测试
  6. 06提供结构与导入检查脚本

适用场景

  1. 01规划新 Flutter 应用目录
  2. 02隔离纯 Dart 领域逻辑
  3. 03检查功能间导入边界
  4. 04判断何时拆分 Dart 包

安装

复制下面的命令,在终端中运行。

$npx skills add https://github.com/zakariaf/flutter-skills --skill project-structure-and-packages

安装后在 Claude Code 中输入/project-structure-and-packages确认已加载。

包含的文件

除 SKILL.md 外,安装时一起下载的参考资料和脚本。

examples/
placement_by_failure.dart
public_api_surface.dart
references/
layering-and-dependencies.md
single-package-layout.md
workspace-and-packages.md
scripts/
check_import_boundaries.sh
check_structure.sh

详细介绍

项目结构与软件包

项目布局是一个反馈闭环,而非装饰:结构的存在,是为了让“什么导入了 Flutter?”、“什么没有测试?”以及“这个功能是否伸手到了另一个功能里?”都能通过 grep 和 ls 找到答案,而不是靠信任。默认使用一个 Flutter 软件包(应用),在 lib/features/ 下按功能优先组织,而非围绕共享基础层组织。只有当一组逻辑确实需要隔离时,才引入第二个纯 Dart 软件包。pubspec.yaml 是审计凭据——软件包无法导入未声明的内容,因此依赖列表就是分层边界。

此技能负责管理物理目录树(D1)。其他技能应引用此目录树,而不是另行绘制自己的目录树。

根据当前任务阅读相应参考文档:

  • references/single-package-layout.md — 默认的单软件包、功能优先目录树,共享基础层文件夹、按故障类型确定放置位置,以及刻意不包含的内容列表。
  • references/layering-and-dependencies.md — 仅向下的 DAG、功能隔离、将 pubspec 作为审计依据,以及编译防火墙的理念。
  • references/workspace-and-packages.md — 何时(以及如何)提取纯 Dart 软件包:通过 barrel 导出而非 src、使用 resolution: workspace,以及为核心软件包命名时采用通用名称。

提交 PR 前运行 scripts/check_structure.sh 和 scripts/check_import_boundaries.sh。纯度检查只会自动检测名称为 *_core 的纯代码根目录;名称普通且通用的纯软件包(如 money、scheduling)必须显式传入,例如:PURE_DIR=packages/money scripts/check_import_boundaries.sh。

不容妥协的规则

  1. 默认使用单个包。 中小型应用是一个 Flutter 包;提交其 pubspec.lock(它是应用,不是库)。在纯逻辑确实值得隔离之前,不要添加 packages/、Melos 或 Dart 工作区(参见 references/workspace-and-packages.md)。在没有可拆分内容时增加包毫无收益,只会浪费数天。
  2. 在 lib/features/ 内按功能组织,并建立共享基础层。 每个屏幕/界面都是 lib/features/<feature>/ 下的一个文件夹(presentation/ + <feature>_notifier.dart;可选的 application/、domain/,极少使用 data/)。横切基础层在顶层按技术角色拆分为 core/(纯逻辑)、data/(仓库 + DB)、services/(副作用端口)、routing/、theme/、l10n/,因为这些文件服务于所有功能。main.dart 是调用 bootstrap() 的精简入口;bootstrap.dart 是组合根;app.dart 用于构建 MaterialApp.router。
  3. 不要设置杂物筐,也不要使用无名嵌套。 没有固定的最大深度——按功能组织自然会形成嵌套(features/orders/presentation/)。原则是每个文件夹都有明确的职责:不设 utils/、helpers/、common/、misc/,也不设杂物筐式的 shared/。core/ 是获准的纯基础层目录,不是杂物抽屉。
  4. 将类放在由其故障负责的层。 与 DB 通信 → data/。与平台通道通信 → services/native/。纯值类型/计算器/Clock 接缝 → core/(如果确实仅用于某个功能,则放在该功能的 domain/)。其他所有内容 → 渲染它的功能。这是确定性规则,而非个人偏好。
  5. pubspec.yaml 是审计凭据——声明审计所需的最小依赖集。 未使用的依赖会导致审核不通过:它会模糊清单所作的声明。尽可能精简依赖列表,让人只需阅读一个文件,就能验证每一层的边界。
  6. 依赖只能单向向下;功能之间绝不互相导入。 features → services/data → core。低层不得导入高层;共享代码不得导入组合根。功能文件夹绝不导入其他功能文件夹——通过 core/data/services 共享,或通过路由导航(参见 navigation-and-routing)。循环依赖是设计缺陷:分析器会在单一上下文中解析整个依赖图,因此向上或跨功能导入会导致错误,而非带来运行时意外。
  7. 纯 Dart 核心只依赖 meta——缺少 Flutter 导入就是编译防火墙。 在应用内,lib/core/ 仅存放纯基础内容(值对象、Result/Failure、Clock 接缝、计算器),不得读取系统时钟。时间来自 package:clock 的 Clock(环境中的 clock,或 Clock 参数),绝不使用 DateTime.now(),也绝不自定义 ClockService——参见 service-boundary-and-native。将此类逻辑提取到包后,不含 Flutter 的清单会让误加的 Widget 或 DateTime.now() 成为构建错误。
  8. 仅限包:通过私有的 lib/src/ 提供一个公开的 barrel。 共享包只公开一个库 lib/<package>.dart,用于导出稳定接口;其他内容都放在 lib/src/ 下,其他包不得导入该目录。应用包既不使用 barrel,也不使用 lib/src/:它没有外部导入方,而 barrel 会增加分析器开销和循环导入风险,却毫无益处。
  9. 不要设置杂物筐文件夹。 不设 utils/、helpers/、common/、misc/,也不设杂物筐式的 shared/。辅助内容要么放在其服务对象旁边(同文件扩展),要么作为有明确归属的命名服务/值类型。core/ 明确不是杂物抽屉——它是具名的纯基础层。
  10. 测试目录与 lib/ 一一对应。 lib/data/order_repository.dart 对应 test/data/order_repository_test.dart。这样,“哪些内容没有测试?”就只需对比两份 ls 输出。无法归属于单个文件的横切断言放在专属文件夹中(test/policy/);共享伪对象放在 test/support/。
  11. 每个文件只放一个主要声明;文件名与声明对应,使用 snake_case。 order_repository.dart 存放 OrderRepository。唯一例外:sealed 层级放在同一个文件中,因为该文件就是编译器强制执行的封闭集合。
  12. 始终使用包导入;绝不与相对路径混用。 always_use_package_imports。混用 package: 和相对路径会让同一成员以两种方式解析,从而产生两个不同的运行时类型。包导入在文件移动后仍然有效,也便于使用 grep 搜索。

默认的单包目录树

├── analysis_options.yaml   # the lint safety net (see lint-and-style-config)
├── pubspec.yaml            # pubspec.lock is COMMITTED — this is an app
├── lib/
│   ├── main.dart           # thin entry: runs bootstrap(). Nothing else.
│   ├── bootstrap.dart      # composition root: build infra, override providers, error net, runApp (see app-startup-and-bootstrap)
│   ├── app.dart            # MaterialApp.router, theme wiring, ProviderScope child
│   ├── core/               # PURE foundation, NO Flutter: value objects, Result/Failure, Clock seam, calculators
│   ├── data/               # shared data layer: Drift db, DAOs, repositories, row→model mappers
│   ├── services/           # injectable side-effect ports + impls; native/ quarantines MethodChannel
│   │   └── native/
│   ├── routing/            # the single go_router config, typed routes, guards (see navigation-and-routing)
│   ├── theme/              # ThemeExtension token sets, ColorScheme (see design-system-structure)
│   ├── l10n/               # generated AppLocalizations + ARB (see i18n-rtl-l10n)
│   └── features/           # BY FEATURE — one folder per surface
│       └── orders/
│           ├── presentation/
│           │   ├── orders_screen.dart
│           │   ├── orders_notifier.dart   # the ViewModel: a Riverpod Notifier (see state-management-riverpod)
│           │   └── widgets/
│           │       └── order_tile.dart
│           ├── application/               # OPTIONAL: use-cases spanning repositories
│           └── domain/                    # OPTIONAL: feature-local models
├── test/                   # mirrors lib/ 1:1
└── integration_test/       # only what unit/widget tests cannot reach

功能的 data/ 文件夹通常不存在——功能会读取 lib/data/ 中的共享存储库。完整目录树和刻意省略的目录列表,请参阅 references/single-package-layout.md。

放置决策流程

// Q: where does OrderRepository go?  A: it owns DB failure → lib/data/
class OrderRepository {
  OrderRepository(this._db);
  final AppDatabase _db;

  // Rows never escape this class; the UI is owed a value type, not a raw row.
  Future<List<Order>> loadAll() async {
    final rows = await _db.selectOrders();          // package:drift confined here
    return rows.map(Order.fromRow).toList();
  }
}
// Q: where does PaymentChannel go?  A: it constructs a MethodChannel → lib/services/native/
// One `grep -rn MethodChannel lib/` outside services/native/ is a review failure.
class PaymentChannel {
  static const _channel = MethodChannel('app/payment');
  Future<bool> canPay() async => await _channel.invokeMethod('canPay') ?? false;
}
  • 被 2 个或更多功能复用的小部件或模型,应下移到 lib/core/(纯代码)或 lib/data/,或移至一个命名明确的共享小部件目录——绝不能放进某个功能的文件夹,再由另一个功能导入。
  • 即使只有一个文件,新界面也应在 lib/features/ 下拥有自己的文件夹——保持一致比省一行代码更重要。将其路由注册到 routing/(参见 scaffold-feature-module、navigation-and-routing)。
  • 在小部件中于构建时读取平台/无障碍状态(MediaQuery.textScalerOf),不要通过辅助文件或 provider 读取——MediaQuery 已经具备响应性。应用状态使用 Riverpod;平台状态使用 BuildContext。参见 accessibility-as-code。

何时提取为包(多包 / 工作区)

只要一段逻辑同时满足以下条件,就应立即将其提取为纯 Dart包:它确实与 UI 无关;值得在毫秒级的无头环境中测试;并且确保“不得导入 Flutter”这一约束能通过构建错误强制执行。典型示例:金额/单位运算、实体、确定性算法。未达到这些条件时,它应留在 lib/core/ 中。

# packages/domain_core/pubspec.yaml — the manifest IS the purity audit
name: domain_core          # a GENERIC name — never the brand, so a rename never churns the core
publish_to: none
resolution: workspace       # member of the root workspace (modern successor to path:/overrides)
environment:
  sdk: ^3.6.0
dependencies:
  meta: ^1.15.0            # the ONLY dependency. No flutter, no dart:io, no dart:ui, no clock.
dev_dependencies:
  test: ^1.25.0            # plain `dart test` — NOT flutter_test

根目录的 pubspec.yaml 会在 workspace: 下列出成员;提交的 pubspec.lock 只有一份,位于根目录。公共导出文件 lib/domain_core.dart 导出稳定接口;内部实现保留在 lib/src/ 下。完整流程、依赖关系矩阵和关卡要求见 references/workspace-and-packages.md。

仅适用于多包(工作区)项目。 本节中的所有内容——packages/、resolution: workspace、导出文件、lib/src/——只适用于你已决定拆分之后。单包应用无需执行其中任何操作。

反模式

  • 按功能优先拆分共享基础层(orders/、settings/ 各自拥有数据库和存储库)。实体关系图是共享的;存储库/数据库应放在 lib/data/,纯类型应放在 lib/core/。功能优先仅适用于 lib/features/ 内部,不适用于数据关系图。
  • 一个功能文件夹导入另一个功能文件夹。 这会耦合本应只通过 core/data/services 或路由交互的两个界面。将共享部分下移,或通过导航跳转。
  • 使用 utils/、helpers/、common/、misc/ 或杂物堆式的 shared/。 这会变成杂物抽屉;每个候选项都应放在其所属对象旁边,或作为命名明确的类型。(core/ 没问题——它是命名明确的纯基础层。)
  • 在单体应用包中使用导出文件或 lib/src/。 没有外部导入者需要它们;它们会增加分析器开销和循环导入风险。lib/src/ 仅适用于包。
  • 在小部件或存储库中构造 MethodChannel。 它无法伪造,也无法在代码审查中发现。应将其隔离在 services/native/ 中。
  • 向 lib/core/ 或纯核心包添加 flutter、dart:io 或 DateTime.now()。 这会破坏编译防火墙和无头测试层级;应注入 Clock(package:clock)并提供种子值。
  • 使用 path: + dependency_overrides,而不是 resolution: workspace。 这会破坏单上下文解析,并隐藏工作区旨在暴露的版本冲突。
  • 以品牌名命名核心包(myapp_core)。将来更名会导致所有导入都要改动。应根据包的内容命名(domain_core、money)。
  • 添加“以后再用”的推测性依赖。 未使用的依赖边会模糊审查重点,审查时会被拒绝。
  • 混用 package: 导入和相对路径导入。 两种导入路径会导致同一个类出现两种运行时类型。

完成定义

  • 默认采用单个 Flutter package;提交 pubspec.lock;除非纯逻辑主体值得单独拆分,否则不使用 packages//Melos/工作区。
  • 顶层是共享基础层(core/ data/ services/ routing/ theme/ l10n/);功能位于 lib/features/<feature>/ 下;main.dart 只运行 bootstrap()。
  • 功能文件夹之间不得相互导入;通过 core/data/services 或路由实现共享。
  • 每个类都放在负责其故障的层中;MethodChannel 只位于 services/native/;数据行不得离开 data/;纯基础层和 Clock 接缝位于 core/。
  • 任何位置都不得存在 utils//helpers//common//misc//杂物堆式的 shared/ 文件夹;core/ 作为具名的纯基础层存在,而非杂物收纳处。
  • 应用 package 中不得有 lib/src/ 或 barrel(它们仅是 PACKAGE 约定)。
  • test/ 与 lib/ 逐项对应;共享 fake 位于 test/support/,横切检查位于 test/policy/。
  • 每个文件仅有一个主要声明,文件名与声明同名并使用 snake_case(sealed 层级是唯一例外);启用 always_use_package_imports。
  • 如果拆分了 package:它是 resolution: workspace 成员,名称通用,并在 lib/src/ 上提供一个公共 barrel;纯 core 仅声明 meta(不使用 flutter/dart:io/dart:ui/clock);根目录只有一个 pubspec.lock。
  • 依赖图只向下;不得跨 package 导入 lib/src/;依赖应精简且无未使用的依赖边。
  • scripts/check_structure.sh 和 scripts/check_import_boundaries.sh 检查通过。

相关技能

  • flutter-architecture — 此文件夹结构所实现的分层 MVVM(按文件夹划分功能、按 package 划分基础层、仓库采用单一写入路径);它明确各层语义并引用此目录树。
  • scaffold-feature-module — 一个 lib/features/<feature>/ 文件夹的固定结构(View + 一一对应的 <feature>_notifier.dart Notifier + 作用域内的 providers),以及如何注册路由。
  • app-startup-and-bootstrap — 负责 main.dart/bootstrap.dart、组合根的顺序以及全局错误处理网。
  • navigation-and-routing — 负责 lib/routing/(go_router 配置、类型化路由、守卫),供各功能注册路由。
  • naming-conventions — 上文提及的角色后缀(Screen/Notifier/Repository/Service/Gateway)以及文件名与主要声明一致的规则。
  • dependency-hygiene — pubspec/lock 机制、插入符范围,以及添加依赖前审计传递依赖树。
  • codegen-and-toolchain — 每个 package 的 build.yaml 作用域,以及生成代码应提交还是加入 gitignore 的决策。
  • lint-and-style-config — analysis_options.yaml,用于将 always_use_package_imports 和静默失败相关 lint 提升为错误。
  • service-boundary-and-native — services/native/ 中对 MethodChannel 的隔离、可注入的 Service/Gateway 端口,以及通过 clockProvider 实现的 Clock 时间接缝。
  • value-objects-money-and-units — lib/core/ 或拆分出的纯 Dart core 的规范归属。

参考资料

评论

支持 Markdown