mcpField
L
layered-architecture

layered-architecture

github.com/verygoodopensource開発17571 回インストール今日更新
リポジトリ

layered-architecture について

Flutter 開発者向けのレイヤードモノレポ設計ガイドです。データ、リポジトリ、ビジネスロジック、プレゼンテーションを分離し、依存方向を定めます。モデル変換、依存性注入、アプリの初期化、パッケージ運用も扱います。

主な機能

  1. 014 層構造と依存方向
  2. 02データ層とリポジトリ層の Dart パッケージ化
  3. 03外部データソースのコンストラクター注入
  4. 04レスポンスモデルからドメインモデルへの変換
  5. 05モノレポのパス依存とエクスポート規則

活用シーン

  1. 01Flutter モノレポの機能構成の設計
  2. 02API クライアントやデータソースパッケージの追加
  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 アプリ向けのレイヤー構造を持つモノレポアーキテクチャです。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_cli MCP サーバーの 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
PresentationUI — ウィジェット、ページ、ビュー、レイアウト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_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>,
    );
  }
}

pubspec、バレルファイル、レスポンスモデル、例外クラスを含む完全な user_api_client パッケージについては、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,
    );
  }
}

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/ のパスは決してインポートしません

よくあるワークフロー

新しいデータソースの追加
  1. very_good_cli MCP サーバーの create dart_package ツールでパッケージをスキャフォールドします:<name>_api_client --output-directory packages
  2. 外部依存関係(例:http、json_annotation)を pubspec.yaml に追加します
  3. fromJson/toJson を備えたレスポンスモデルを lib/src/models/ に作成します
  4. すべてのモデルをエクスポートするバレルファイル lib/src/models/models.dart を作成します
  5. クライアントクラスを lib/src/<name>_api_client.dart に実装します
  6. src/ の内容をエクスポートするパッケージのバレルファイル lib/<name>_api_client.dart を作成します
  7. lib/ の構造を反映したユニットテストを test/ に記述します。testing スキルを参照してください
  8. パッケージのディレクトリを対象に very_good_cli MCP サーバーの test ツールを実行します。実行範囲を指定するには directory: 'packages/<name>_api_client' を渡します
新しい Repository の追加
  1. very_good_cli MCP サーバーの create dart_package ツールでパッケージをスキャフォールドします:<name>_repository --output-directory packages
  2. pubspec.yaml にデータ層パッケージへのパス依存関係を追加します
  3. ドメインモデル用の依存関係に equatable を追加します
  4. Equatable を継承するドメインモデルを lib/src/models/ に作成します
  5. バレルファイル lib/src/models/models.dart を作成します
  6. データクライアントをコンストラクターインジェクションする repository クラスを実装します
  7. レスポンスモデルからドメインモデルへの変換ロジックを追加します
  8. パッケージのバレルファイル lib/<name>_repository.dart を作成します
  9. モックしたデータクライアントを使用してユニットテストを記述します。testing スキルを参照してください
Repository と機能の接続
  1. repository パッケージと、そこから利用する各データパッケージへのパス依存関係をルートの pubspec.yaml に追加します
  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>/ のインポートを Grep します。pubspec リファレンスの「Import Boundary」セクションで定義されているエントリポイントまたはブートストラップファイル以外に該当箇所がなくなれば、作業は完了です

追加リソース

  • 完全な実践例とpubspec リファレンス
  • データフローの解説 — 各レイヤーのコードとともにリクエストの流れを説明します
  • モデル変換パターン — データモデルとドメインモデルの変換
  • パッケージレベルのテスト — データクライアントとリポジトリを分離してテストします
  • Bloc/Cubit のパターンと Page/View の分離については、bloc スキルを参照してください
  • プロジェクトのスキャフォールディングには、very_good_cli MCP サーバーの create dart_package ツールを使用してください
  • データクライアント、リポジトリ、Bloc のテストについては、testing スキルを参照してください

コメント