关于 project-structure-and-packages
面向 Flutter 与 Dart 项目的结构设计技能,帮助开发者规划目录、依赖层和测试布局。它默认单 Flutter 包,并以依赖清单和导入边界落实分层。
主要功能
- 01默认采用单 Flutter 包
- 02按功能组织 lib/features/
- 03限制依赖向下流动
- 04用 pubspec.yaml 审核依赖
- 05镜像源码布局组织测试
- 06提供结构与导入检查脚本
适用场景
- 01规划新 Flutter 应用目录
- 02隔离纯 Dart 领域逻辑
- 03检查功能间导入边界
- 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.dartpublic_api_surface.dartreferences/layering-and-dependencies.mdsingle-package-layout.mdworkspace-and-packages.mdscripts/check_import_boundaries.shcheck_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。
不容妥协的规则
- 默认使用单个包。 中小型应用是一个 Flutter 包;提交其
pubspec.lock(它是应用,不是库)。在纯逻辑确实值得隔离之前,不要添加packages/、Melos 或 Dart 工作区(参见references/workspace-and-packages.md)。在没有可拆分内容时增加包毫无收益,只会浪费数天。 - 在
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。 - 不要设置杂物筐,也不要使用无名嵌套。 没有固定的最大深度——按功能组织自然会形成嵌套(
features/orders/presentation/)。原则是每个文件夹都有明确的职责:不设utils/、helpers/、common/、misc/,也不设杂物筐式的shared/。core/是获准的纯基础层目录,不是杂物抽屉。 - 将类放在由其故障负责的层。 与 DB 通信 →
data/。与平台通道通信 →services/native/。纯值类型/计算器/Clock 接缝 →core/(如果确实仅用于某个功能,则放在该功能的domain/)。其他所有内容 → 渲染它的功能。这是确定性规则,而非个人偏好。 pubspec.yaml是审计凭据——声明审计所需的最小依赖集。 未使用的依赖会导致审核不通过:它会模糊清单所作的声明。尽可能精简依赖列表,让人只需阅读一个文件,就能验证每一层的边界。- 依赖只能单向向下;功能之间绝不互相导入。
features → services/data → core。低层不得导入高层;共享代码不得导入组合根。功能文件夹绝不导入其他功能文件夹——通过core/data/services共享,或通过路由导航(参见navigation-and-routing)。循环依赖是设计缺陷:分析器会在单一上下文中解析整个依赖图,因此向上或跨功能导入会导致错误,而非带来运行时意外。 - 纯 Dart 核心只依赖
meta——缺少 Flutter 导入就是编译防火墙。 在应用内,lib/core/仅存放纯基础内容(值对象、Result/Failure、Clock 接缝、计算器),不得读取系统时钟。时间来自package:clock的Clock(环境中的clock,或Clock参数),绝不使用DateTime.now(),也绝不自定义ClockService——参见service-boundary-and-native。将此类逻辑提取到包后,不含 Flutter 的清单会让误加的Widget或DateTime.now()成为构建错误。 - 仅限包:通过私有的
lib/src/提供一个公开的 barrel。 共享包只公开一个库lib/<package>.dart,用于导出稳定接口;其他内容都放在lib/src/下,其他包不得导入该目录。应用包既不使用 barrel,也不使用lib/src/:它没有外部导入方,而 barrel 会增加分析器开销和循环导入风险,却毫无益处。 - 不要设置杂物筐文件夹。 不设
utils/、helpers/、common/、misc/,也不设杂物筐式的shared/。辅助内容要么放在其服务对象旁边(同文件扩展),要么作为有明确归属的命名服务/值类型。core/明确不是杂物抽屉——它是具名的纯基础层。 - 测试目录与
lib/一一对应。lib/data/order_repository.dart对应test/data/order_repository_test.dart。这样,“哪些内容没有测试?”就只需对比两份ls输出。无法归属于单个文件的横切断言放在专属文件夹中(test/policy/);共享伪对象放在test/support/。 - 每个文件只放一个主要声明;文件名与声明对应,使用
snake_case。order_repository.dart存放OrderRepository。唯一例外:sealed层级放在同一个文件中,因为该文件就是编译器强制执行的封闭集合。 - 始终使用包导入;绝不与相对路径混用。
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.dartNotifier + 作用域内的 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 的规范归属。
参考资料
- Dart package 布局:https://dart.dev/tools/pub/package-layout
- Dart 和 Flutter 工作区(
resolution: workspace):https://dart.dev/tools/pub/workspaces - Effective Dart — 库与导入:https://dart.dev/effective-dart/usage#libraries
- pubspec 格式:https://dart.dev/tools/pub/pubspec
- Flutter 架构建议:https://docs.flutter.dev/app-architecture