project-structure-and-packages について
Flutter/Dart開発者がディレクトリ、依存レイヤー、テスト配置を設計するためのガイドです。単一のFlutterパッケージを基本とし、マニフェストとインポート検査で境界を明確にします。
主な機能
- 01単一のFlutterパッケージを基本とする
- 02lib/features/を機能別に構成
- 03依存を下位方向に限定
- 04pubspec.yamlで依存を監査
- 05テストをソース構成に対応
- 06構成・インポート検査を提供
活用シーン
- 01新しいFlutterアプリの構成設計
- 02純粋なDartドメインロジックの分離
- 03機能間の境界チェック
- 04Dartパッケージの分割時期の判断
インストール
コマンドをコピーしてターミナルで実行します。
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 で確認できるように存在します。基本は、lib/features/ 配下に機能優先で整理した 1 つの Flutter パッケージ(アプリ)です。ロジックのまとまりが分離に値する場合に限り、2 つ目の純粋な Dart パッケージを導入してください。pubspec.yaml は監査用の成果物です。パッケージは宣言していないものをインポートできないため、依存関係リストそのものがレイヤーの境界になります。
このスキルは、物理ディレクトリツリー(D1)を所有します。他のスキルは独自のツリーを定義せず、これを参照します。
作業内容に応じて、次のリファレンスを確認してください。
references/single-package-layout.md— デフォルトの 1 パッケージ・機能優先ツリー、共有基盤フォルダー、失敗の種類に応じた配置、意図的に設けていない項目の一覧。references/layering-and-dependencies.md— 下方向のみの DAG、機能の分離、監査としての pubspec、コンパイルファイアウォールの考え方。references/workspace-and-packages.md— 純粋な Dart パッケージを切り出すタイミングと方法(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を参照)。分けるものがないうちにパッケージを増やしても何も得られず、数日を費やすことになります。 - 共有基盤の上に、
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は監査の根拠となるファイルです。監査に必要な最小限の依存関係を宣言します。 未使用の依存関係はレビューで却下されます。マニフェストの主張が曖昧になるためです。依存関係を可能な限り絞ることで、各レイヤーの境界を1つのファイルを読むだけで検証できます。- 依存関係は一方向のみ、下位レイヤーに向けます。機能同士のインポートは禁止です。
features → services/data → coreの方向にします。下位レイヤーから上位レイヤーをインポートしてはならず、共有コードからコンポジションルートをインポートしてもいけません。ある機能のフォルダーから別の機能のフォルダーをインポートすることは絶対に禁止です。core/data/services経由で共有するか、ルートで遷移してください(navigation-and-routingを参照)。循環依存は設計上の欠陥です。アナライザーはグラフ全体を1つのコンテキストで解決するため、上位レイヤーや別の機能をインポートすると、実行時に驚くのではなく、エラーになります。 - 純粋な 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/を1つの公開バレルで公開します。 共有パッケージが公開するライブラリは、安定した公開 API をエクスポートするlib/<package>.dart1つだけにします。それ以外はlib/src/以下に置き、他のパッケージからインポートできないようにします。アプリパッケージでは、バレルもlib/src/も使いません。外部からインポートされることはなく、バレルを使ってもメリットがない一方で、アナライザーの負荷と循環インポートのリスクが増えるためです。 - 何でも入れるフォルダーは作りません。
utils/、helpers/、common/、misc/、寄せ集めのshared/は作りません。ヘルパーは、それが補助する型の隣に置く(同じファイル内の拡張にする)か、適切な置き場所を持つ名前付きサービス/値型にします。core/は明確に、何でも入れるフォルダーではありません。名前のとおり、純粋な基盤のレイヤーです。 - テストは
lib/と1対1で対応させます。lib/data/order_repository.dartにはtest/data/order_repository_test.dartを用意します。「テストのないものは何か?」という問いは、2つのlsの出力の差分を見るだけで答えられます。1つのファイルに属さない横断的なアサーションには専用のフォルダー(test/policy/)を、共有フェイクにはtest/support/を使います。 - 1ファイルにつき主要な宣言は1つとし、ファイル名はその宣言名を
snake_caseにしたものにします。order_repository.dartにはOrderRepositoryを定義します。例外はsealed階層です。コンパイラーが強制する閉じた集合そのものがそのファイルを構成するため、1つのファイルにまとめます。 - どこでもパッケージインポートを使い、相対インポートと混在させません。
always_use_package_importsを使います。package:と相対パスを混在させると、同じメンバーが2通りの方法で解決され、実行時に異なる型として扱われることがあります。パッケージインポートはファイルの移動後も維持され、検索も容易です。
デフォルトの単一パッケージのツリー
├── 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/に下ろすか、名前付きの共有ウィジェット用フォルダーに移します。別の機能がインポートすることになる 1 つの機能のフォルダーには入れません。 - 新しい画面は、ファイルが 1 つだけでも
lib/features/の下に独自のフォルダーを作成します。1 行節約することより一貫性を優先します。そのルートをrouting/に登録します(scaffold-feature-module、navigation-and-routingを参照)。 - プラットフォーム/アクセシビリティの状態(
MediaQuery.textScalerOf)は、ヘルパーファイルやプロバイダー経由ではなく、ビルド時にウィジェット内で読み取ります。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 はルートに 1 つだけです。公開バレル lib/domain_core.dart が安定した API をエクスポートし、内部実装は lib/src/ に置きます。手順全体、依存関係マトリクス、ゲートについては references/workspace-and-packages.md を参照してください。
マルチパッケージ(ワークスペース)の場合のみ。 このセクションの
packages/、resolution: workspace、バレル、lib/src/はすべて、分割すると決めた場合にのみ適用されます。単一パッケージのアプリでは、いずれも使いません。
アンチパターン
- 共有基盤を機能ごとに分割する(
orders/、settings/がそれぞれ独自の DB とリポジトリを持つ)。エンティティグラフは共有されるため、リポジトリ/DB はlib/data/に、純粋な型はlib/core/に置きます。機能優先の構成を適用するのはlib/features/内であり、データグラフではありません。 - ある機能フォルダーから別の機能フォルダーをインポートする。 本来
core/data/servicesまたはルートを通じてのみ連携すべき 2 つの画面が密結合します。共有部分を下層に移すか、画面遷移を使います。 utils/、helpers/、common/、misc/、または寄せ集めのshared/。 何でも入れる場所になってしまいます。各項目は所有者の隣に置くか、名前付きの型にします(core/は名前の定まった純粋な基盤レイヤーなので問題ありません)。- 単一アプリのパッケージでバレルや
lib/src/を使う。 それらを正当化する外部インポーターがなく、アナライザーの負荷と循環インポートのリスクが増えます。lib/src/はパッケージ専用の規約です。 - ウィジェットやリポジトリ内で
MethodChannelを構築する。 フェイク化できず、レビューでも見つけられません。services/native/に隔離します。 lib/core/または純粋なコアパッケージにflutter、dart:io、DateTime.now()を追加する。 コンパイル時の境界とヘッドレステスト層が損なわれます。代わりにClock(package:clock)とシードを注入します。resolution: workspaceの代わりにpath:とdependency_overridesを使う。 単一コンテキストでの依存解決を損ない、ワークスペースが明らかにするためにあるバージョン競合を隠してしまいます。- ブランド名をコアパッケージ名にする(
myapp_core)。将来の名称変更で、すべてのインポートを修正することになります。含まれるものを表す名前(domain_core、money)にします。 - 「後で使うかもしれない」という理由で依存関係を追加する。 未使用の依存関係は監査を曖昧にし、レビューで却下されます。
package:インポートと相対インポートを混在させる。 インポートパスが 2 つあると、同じクラスに対して実行時型が 2 つ生じます。
完了の定義
- デフォルトは単一の Flutter パッケージです。
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/は名前付きの純粋な基盤レイヤーとして設け、何でも入れる場所にはしません。 - アプリパッケージに
lib/src/やバレルを設けません(これらはパッケージの慣例に限られます)。 -
test/はlib/の構成を1対1で反映します。共有フェイクはtest/support/に、横断的なチェックはtest/policy/に配置します。 - 1ファイルにつき主要な宣言は1つとし、ファイル名は宣言名に合わせて
snake_caseにします(唯一の例外は sealed 階層です)。always_use_package_importsを使用します。 - パッケージを切り出した場合は、
resolution: workspaceのメンバーとし、汎用的な名前を付け、lib/src/を公開するパブリックバレルを1つ設けます。純粋な core が宣言する依存関係はmetaのみとし(flutter/dart:io/dart:ui/clock は不可)、ルートのpubspec.lockは1つだけにします。 - 依存関係グラフは下方向にのみつなげます。パッケージをまたぐ
lib/src/のインポートは禁止し、依存関係は監査に必要な最小限に抑え、未使用の依存関係を残しません。 -
scripts/check_structure.shとscripts/check_import_boundaries.shが成功します。
関連スキル
flutter-architecture— このフォルダー構成が実現するレイヤード MVVM(フォルダー単位の機能、パッケージ単位の基盤、書き込み経路を一本化したリポジトリー)を扱います。レイヤーの意味を保ち、このツリーを参照します。scaffold-feature-module— 1つのlib/features/<feature>/フォルダーの固定的な構成(View + 1対1で対応する<feature>_notifier.dartNotifier +スコープ付きプロバイダー)と、ルートへの登録方法を扱います。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— パッケージ単位のbuild.yamlのスコープ設定と、生成コードをコミットするか gitignore に含めるかの判断を扱います。lint-and-style-config—always_use_package_importsとサイレントな失敗に関する lint をエラーに昇格するanalysis_options.yamlを扱います。service-boundary-and-native—services/native/での MethodChannel の隔離、注入可能な Service/Gateway ポート、clockProviderを介したClockの時間境界を扱います。value-objects-money-and-units—lib/core/または切り出した純粋な Dart core に置く正準的な実装を扱います。
参考資料
- Dart パッケージのレイアウト: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