mcpField
F
flutter-apply-architecture-best-practices

flutter-apply-architecture-best-practices

github.com/flutter開発3.1k43.7k 回インストール2 日前に更新
リポジトリ

flutter-apply-architecture-best-practices について

Flutter 開発者向けに、レイヤー構成と機能実装の手順を示すスキルです。MVVM、Repository パターン、各段階でのテストを重視します。

主な機能

  1. 01MVVM による UI 状態管理
  2. 02Repository によるデータアクセス分離
  3. 03必要に応じた Use Case 抽出
  4. 04機能実装ワークフロー
  5. 05ユニットテストによる検証

活用シーン

  1. 01Flutter アプリのレイヤー設計
  2. 02サービスを使う機能の実装
  3. 03複雑または共通の業務ロジックの分離
  4. 04ViewModel と Repository のテスト

インストール

コマンドをコピーしてターミナルで実行します。

$npx skills add https://github.com/flutter/agent-plugins --skill flutter-apply-architecture-best-practices

インストール後、Claude Code で/flutter-apply-architecture-best-practicesを入力して読み込まれたか確認します。

詳細

Flutter アプリケーションのアーキテクチャ設計

目次

アーキテクチャレイヤー

アプリケーションを明確なレイヤーに分割し、関心の分離を厳格に徹底します。UI の描画とビジネスロジック、データ取得を決して混在させないでください。

UI レイヤー(プレゼンテーション)

MVVM(Model-View-ViewModel)パターンを実装し、UI の状態とロジックを管理します。

  • ビュー: 再利用可能で簡潔なウィジェットを作成します。ビュー内のロジックは、アニメーション、レイアウト制約、簡単なルーティングなど、UI 固有の操作に限定します。必要なデータはすべて ViewModel から渡します。
  • ViewModel: UI の状態を管理し、ユーザー操作を処理します。状態を公開するには ChangeNotifier を継承するか、Listenable を使用します。ビューには不変の状態スナップショットを公開します。コンストラクターを通じてリポジトリを ViewModel に注入します。
データレイヤー

リポジトリパターンを実装してデータアクセスロジックを分離し、信頼できる唯一の情報源を作成します。

  • サービス: 外部 API(HTTP クライアント、ローカルデータベース、プラットフォームプラグイン)をラップするステートレスクラスを作成します。生の API モデルまたは Result ラッパーを返します。
  • リポジトリ: 1 つ以上のサービスを利用します。生の API モデルをクリーンなドメインモデルに変換します。キャッシュ、オフライン同期、再試行ロジックを処理します。ドメインモデルを ViewModel に公開します。
ロジックレイヤー(ドメイン - 任意)
  • ユースケース: アプリケーションに ViewModel を複雑にするビジネスロジックが含まれる場合、または複数の ViewModel 間でロジックを再利用する必要がある場合にのみ、このレイヤーを実装します。このロジックを専用のユースケース(インタラクター)クラスに切り出し、ViewModel とリポジトリの間に配置します。

プロジェクト構成

ハイブリッドなアプローチを用いてコードベースを整理します。UI コンポーネントは機能ごとにまとめ、データ/ドメインコンポーネントは種類ごとにまとめます。

lib/
├── data/
│   ├── models/         # API models
│   ├── repositories/   # Repository implementations
│   └── services/       # API clients, local storage wrappers
├── domain/
│   ├── models/         # Clean domain models
│   └── use_cases/      # Optional business logic classes
└── ui/
    ├── core/           # Shared widgets, themes, typography
    └── features/
        └── [feature_name]/
            ├── view_models/
            └── views/

ワークフロー:新機能の実装

アプリケーションに新機能を追加する際は、この順序に従って作業します。進捗を記録するにはチェックリストをコピーしてください。

タスクの進捗
  • ステップ 1:ドメインモデルを定義する。 freezed または built_value を使用して、その機能の不変データクラスを作成します。
  • ステップ 2:サービスを実装する。 外部 API との通信を処理するサービスクラスを作成または更新します。
  • ステップ 3:リポジトリを実装する。 サービスを利用し、ドメインモデルを返すリポジトリを作成します。
  • ステップ 4:条件に応じたロジックを適用する(ドメインレイヤー)。
    • 機能で複雑なデータ変換または複数のリポジトリにまたがるロジックが必要な場合: ユースケースクラスを作成します。
    • 機能が単純な CRUD 操作の場合: ステップ 5 に進みます。
  • ステップ 5:ViewModel を実装する。 ChangeNotifier を継承する ViewModel を作成します。必要なリポジトリ/ユースケースを注入します。不変の状態とコマンドメソッドを公開します。
  • ステップ 6:ビューを実装する。 UI ウィジェットを作成します。ListenableBuilder または AnimatedBuilder を使用して ViewModel の変更を監視します。
  • ステップ 7:依存関係を注入する。 新しいサービス、リポジトリ、ViewModel を依存性注入コンテナー(例:provider または get_it)に登録します。
  • ステップ 8:検証を実行する。 ViewModel とリポジトリのユニットテストを実行します。
    • フィードバックループ: テストを実行 -> 失敗を確認 -> ロジックを修正 -> 合格するまで再実行します。

例

データレイヤー:サービスとリポジトリ
// 1. Service (Raw API interaction)
class ApiClient {
  Future<UserApiModel> fetchUser(String id) async {
    // HTTP GET implementation...
  }
}

// 2. Repository (Single source of truth, returns Domain Model)
class UserRepository {
  UserRepository({required ApiClient apiClient}) : _apiClient = apiClient;
  
  final ApiClient _apiClient;
  User? _cachedUser;

  Future<User> getUser(String id) async {
    if (_cachedUser != null) return _cachedUser!;
    
    final apiModel = await _apiClient.fetchUser(id);
    _cachedUser = User(id: apiModel.id, name: apiModel.fullName); // Transform to Domain Model
    return _cachedUser!;
  }
}
UI レイヤー:ViewModel とビュー
// 3. ViewModel (State management and presentation logic)
class ProfileViewModel extends ChangeNotifier {
  ProfileViewModel({required UserRepository userRepository}) 
      : _userRepository = userRepository;

  final UserRepository _userRepository;

  User? _user;
  User? get user => _user;

  bool _isLoading = false;
  bool get isLoading => _isLoading;

  Future<void> loadProfile(String id) async {
    _isLoading = true;
    notifyListeners();

    try {
      _user = await _userRepository.getUser(id);
    } finally {
      _isLoading = false;
      notifyListeners();
    }
  }
}

// 4. View (Dumb UI component)
class ProfileView extends StatelessWidget {
  const ProfileView({super.key, required this.viewModel});

  final ProfileViewModel viewModel;

  @override
  Widget build(BuildContext context) {
    return ListenableBuilder(
      listenable: viewModel,
      builder: (context, _) {
        if (viewModel.isLoading) {
          return const Center(child: CircularProgressIndicator());
        }
        
        final user = viewModel.user;
        if (user == null) {
          return const Center(child: Text('User not found'));
        }

        return Column(
          children: [
            Text(user.name),
            ElevatedButton(
              onPressed: () => viewModel.loadProfile(user.id),
              child: const Text('Refresh'),
            ),
          ],
        );
      },
    );
  }
}

コメント