layered-architecture について
Flutter 開発者向けのレイヤードモノレポ設計ガイドです。データ、リポジトリ、ビジネスロジック、プレゼンテーションを分離し、依存方向を定めます。モデル変換、依存性注入、アプリの初期化、パッケージ運用も扱います。
主な機能
- 014 層構造と依存方向
- 02データ層とリポジトリ層の Dart パッケージ化
- 03外部データソースのコンストラクター注入
- 04レスポンスモデルからドメインモデルへの変換
- 05モノレポのパス依存とエクスポート規則
活用シーン
- 01Flutter モノレポの機能構成の設計
- 02API クライアントやデータソースパッケージの追加
- 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 アプリ向けのレイヤー構造を持つモノレポアーキテクチャです。4 つのレイヤーを、厳格な一方向依存を持つ独立した Dart パッケージとして構成します。
コア標準
レイヤーアーキテクチャに関するすべての作業に、以下の標準を適用してください。
- 4 つのレイヤー — Data、Repository、Business Logic、Presentation。機能のリポジトリが外部ソースを読み取る場合、その機能は必ず 4 つすべてのレイヤーにまたがります
- 一方向の依存関係 — Presentation → Business Logic → Repository → Data。レイヤーを飛ばしたり、依存の向きを逆にしたりしないでください
- Data レイヤーと Repository レイヤーは
packages/に配置 — それぞれ独立した Dart パッケージで、独自のpubspec.yamlを持ちます - Business Logic レイヤーと Presentation レイヤーは
lib/に配置 — アプリ内で機能ごとに構成します - Data レイヤーのパッケージにドメインロジックやビジネスロジックを含めない — 無関係なプロジェクトでも再利用できる必要があります
- リポジトリ間の依存関係を作らない — リポジトリが別のリポジトリをインポートすることはありません
- data パッケージと repository パッケージでは Flutter SDK を使用しない —
very_good_cliMCP サーバーのcreate dart_packageツールでスキャフォールドを作成します - ドメインごとにリポジトリを 1 つ作成 —
user_repository、weather_repository、auth_repository - ローカルパッケージにはパス依存を使用 — 同じリポジトリ内のパッケージに
git:や pub のバージョン参照を使用しないでください - すべてのパッケージ境界にバレルエクスポートを用意 — 利用側から
src/を直接インポートすることはありません - リポジトリはすべての外部ソースをコンストラクター経由で受け取る — データクライアントや
FirebaseAuth.instanceなどの SDK オブジェクトはブートストラップで作成して渡します。リポジトリ内で作成したり、デフォルト値として設定したりしないでください - アプリのブートストラップですべてのレイヤーを接続 —
main_<flavor>.dartでクライアントとリポジトリを作成し、RepositoryProvider経由で提供します - データパッケージをインポートできるのはアプリのエントリーポイントとブートストラップのみ — bloc、ウィジェット、アプリのテストはリポジトリを通じてデータにアクセスします
- Dart 3.13 のプライマリーコンストラクター — Dart 3.13 以降を基準とする場合、
this.fieldではなく、モデルとウィジェットのフィールドをプライマリーコンストラクターの宣言パラメーターとして宣言します(class const User(final String id, final String name) extends Equatable)。3.13 より前の環境では、従来の形式を使用してください
ハーネス間のフォールバック。 このスキルは Very Good CLI MCP サーバーを介してパッケージをスキャフォールドし、テストします。このプラグインの Bash フックがなく、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/ | 0 個以上の Data レイヤーパッケージ | user_repository、weather_repository |
| Business Logic | 状態管理 — ユーザー操作の処理、状態変化の通知 | lib/<feature>/bloc/ または lib/<feature>/cubit/ | Repository レイヤー | LoginBloc、ProfileCubit |
| Presentation | UI — ウィジェット、ページ、ビュー、レイアウト | 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/ に分割して、バレルファイルを用意します。レイヤーは packages/ に配置し、データソースごとに 1 パッケージ、リポジトリごとに 1 パッケージを作成します。2 つ目のデータパッケージと 2 つ目のリポジトリも、1 つ目と同じ構成にします。
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>,
);
}
}
pubspec、バレルファイル、レスポンスモデル、例外クラスを含む完全な user_api_client パッケージについては、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,
);
}
}
pubspec、バレルファイル、エラー処理を含む完全な user_repository パッケージについては、worked-example.md を参照してください。データモデルとドメインモデル間の詳細な変換パターンについては、model-transformation.md を参照してください。
依存関係グラフ
各 pubspec.yaml のパス依存関係は一方向を向きます。データパッケージが宣言するのは外部パッケージのみです。リポジトリパッケージは、データパッケージへのパス依存関係を宣言します。ルートアプリは、リポジトリパッケージと、ブートストラップで構築するすべてのデータパッケージを宣言します。main_<flavor>.dart は各クライアントをインポートして注入します。また、package:very_good_analysis の depend_on_referenced_packages により、インポートするすべてのパッケージを宣言する必要があります。
インポートがレイヤー境界を維持します。 アプリでデータパッケージを宣言すると、lint はブロックがそのパッケージをインポートするのを防げなくなります。境界となるのは、Core Standards のインポートルールです。データパッケージをインポートするのは、アプリのエントリーポイントとブートストラップのみです。
# 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
3 つのファイルの完全な内容と、エントリーポイントおよびブートストラップとして扱われるファイルを含むインポート境界の確認方法については、references/pubspec.md を参照してください。
データフロー
プレゼンテーションがイベントをディスパッチ → Bloc がリポジトリを呼び出す → リポジトリがデータクライアントを呼び出してドメインモデルを返す → BlocBuilder が新しい状態で再ビルドされます。
各レイヤーのコードについては、references/data-flow.md を参照してください。
アプリのブートストラップ
main_<flavor>.dart がすべてのデータクライアントとリポジトリをインポートして構築し、リポジトリを App ウィジェットに渡します。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,
),
);
}
MultiRepositoryProvider を使用する完全な main() と App ウィジェットについては、references/worked-example.md を参照してください。
アンチパターン
| アンチパターン | 問題 | 正しいアプローチ |
|---|---|---|
| ウィジェットが API クライアントを直接呼び出す | Repository 層とビジネスロジック層を迂回するため、変換や状態管理が行われません | ウィジェットがイベントをディスパッチ → Bloc が Repository を呼び出す → Repository が API クライアントを呼び出す |
| Repository が別の repository をインポートする | 循環または複雑に絡み合った依存関係グラフが生じ、独立したテストができなくなります | 各 repository は自己完結させます。必要に応じて、Bloc 層でデータを結合します |
| データ層にドメインモデルを置く | 外部 API の形式と内部ドメインが結び付くため、API の変更がアプリ全体に影響します | データ層にはレスポンスモデルを置き、Repository 層には変換を行うドメインモデルを置きます |
| repository にビジネスロジックを置く | オーケストレーションとルールが混在した、テストできない巨大なモノリスになります | Repository はデータを変換し、すべてのビジネスルールは Bloc/Cubit に記述します |
ローカルパッケージに git: や pub のバージョンを使用する | モノレポが破綻し、変更のたびにローカルですぐに編集する代わりに、公開やプッシュの作業が必要になります | モノレポ内のすべてのパッケージに path: 依存関係を使用します |
| データ/repository パッケージで Flutter をインポートする | Dart のみのコンテキスト(CLI ツール、サーバー)でパッケージを使用できなくなります | very_good_cli MCP サーバーの create dart_package ツールでスキャフォールドします。Flutter SDK への依存はありません |
| すべてをひとつの巨大な repository にまとめる | 責務が多すぎる巨大なオブジェクトになり、単独でのテストが不可能になります | ドメイン境界ごとに repository をひとつ作成します(user_repository、settings_repository) |
src/ を直接インポートする | カプセル化が破られ、利用側が内部構造に依存します | バレルファイルを通じて公開 API をエクスポートします。パッケージをインポートし、src/ のパスは決してインポートしません |
よくあるワークフロー
新しいデータソースの追加
very_good_cliMCP サーバーのcreate dart_packageツールでパッケージをスキャフォールドします:<name>_api_client --output-directory packages- 外部依存関係(例:
http、json_annotation)をpubspec.yamlに追加します fromJson/toJsonを備えたレスポンスモデルをlib/src/models/に作成します- すべてのモデルをエクスポートするバレルファイル
lib/src/models/models.dartを作成します - クライアントクラスを
lib/src/<name>_api_client.dartに実装します src/の内容をエクスポートするパッケージのバレルファイルlib/<name>_api_client.dartを作成しますlib/の構造を反映したユニットテストをtest/に記述します。testing スキルを参照してください- パッケージのディレクトリを対象に
very_good_cliMCP サーバーのtestツールを実行します。実行範囲を指定するにはdirectory: 'packages/<name>_api_client'を渡します
新しい Repository の追加
very_good_cliMCP サーバーのcreate dart_packageツールでパッケージをスキャフォールドします:<name>_repository --output-directory packagespubspec.yamlにデータ層パッケージへのパス依存関係を追加します- ドメインモデル用の依存関係に
equatableを追加します Equatableを継承するドメインモデルをlib/src/models/に作成します- バレルファイル
lib/src/models/models.dartを作成します - データクライアントをコンストラクターインジェクションする repository クラスを実装します
- レスポンスモデルからドメインモデルへの変換ロジックを追加します
- パッケージのバレルファイル
lib/<name>_repository.dartを作成します - モックしたデータクライアントを使用してユニットテストを記述します。testing スキルを参照してください
Repository と機能の接続
- repository パッケージと、そこから利用する各データパッケージへのパス依存関係をルートの
pubspec.yamlに追加します main_<flavor>.dartでデータクライアントと repository を作成し、repository をAppに渡しますAppのMultiRepositoryProviderにRepositoryProvider.valueを追加します- repository を注入した Bloc/Cubit を作成します。bloc スキルを参照してください
BlocProviderとBlocBuilderを使用して Page/View を構築します。bloc スキルを参照してくださいlib/とtest/でpackage:<data_package>/のインポートを Grep します。pubspec リファレンスの「Import Boundary」セクションで定義されているエントリポイントまたはブートストラップファイル以外に該当箇所がなくなれば、作業は完了です
追加リソース
- 完全な実践例とpubspec リファレンス
- データフローの解説 — 各レイヤーのコードとともにリクエストの流れを説明します
- モデル変換パターン — データモデルとドメインモデルの変換
- パッケージレベルのテスト — データクライアントとリポジトリを分離してテストします
- Bloc/Cubit のパターンと Page/View の分離については、bloc スキルを参照してください
- プロジェクトのスキャフォールディングには、
very_good_cliMCP サーバーのcreate dart_packageツールを使用してください - データクライアント、リポジトリ、Bloc のテストについては、testing スキルを参照してください