mcpField
F
flutter-app-architecture

flutter-app-architecture

github.com/evanca開発663231 回インストール3 週間前に更新
リポジトリ

flutter-app-architecture について

Flutter アプリの設計やリファクタリングに使うアーキテクチャスキルです。レイヤーの責務、MVVM、依存性注入、単方向データフローを扱います。

主な機能

  1. 01UI・ロジック・データ層の指針
  2. 02MVVM コンポーネントの責務
  3. 03コンストラクターによる依存性注入
  4. 04Repository を単一のデータ源として管理
  5. 05ストレージとオフライン優先の指針

活用シーン

  1. 01レイヤー構造の Flutter アプリを構築
  2. 02ViewModel、Repository、Service のリファクタリング
  3. 03コンポーネント間の依存関係を設定
  4. 04共有ユースケース向けのドメインロジック追加

インストール

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

$npx skills add https://github.com/evanca/flutter-ai-rules --skill flutter-app-architecture

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

詳細

Flutter アプリケーションアーキテクチャスキル

このスキルでは、保守性とテスト容易性を高めるために、レイヤードアーキテクチャ、適切なデータフロー、MVVM パターンを用いて Flutter アプリケーションを構成する方法を定義します。

使用する場面

次の場合にこのスキルを使用します。

  • レイヤードアーキテクチャを用いて、新しい Flutter プロジェクトのスキャフォールディングを行う。
  • ビューモデル、リポジトリ、サービスを作成またはリファクタリングする。
  • アーキテクチャ上のコンポーネント間で依存性注入を設定する。
  • レイヤー間で単方向のデータフローを実装する。
  • 複雑なビジネスロジックや共有ユースケースのためにドメイン(ロジック)レイヤーを追加する。

1. レイヤー構造

すべてのアプリをUI レイヤーとデータレイヤーに分離します。複雑なアプリに限り、ロジック(ドメイン)レイヤーを追加します。

┌──────────────────────────────────────────────────────────────┐
│   UI Layer    │  Views + ViewModels                           │
├──────────────────────────────────────────────────────────────┤
│  Logic Layer  │  Use Cases / Interactors  (optional)         │
├──────────────────────────────────────────────────────────────┤
│   Data Layer  │  Repositories + Services                     │
└──────────────────────────────────────────────────────────────┘

ルール:

  • 隣接するレイヤー間でのみ通信できます。UI レイヤーからサービスに直接アクセスしてはいけません。
  • データの変更は常にデータレイヤー(SSOT = リポジトリ)で行います。UI レイヤーやロジックレイヤーで変更してはいけません。
  • 単方向のデータフローに従います。状態は下へ(データ → UI)、イベントは上へ(UI → データ)流れます。

2. コンポーネントの責務

ビュー
  • データの表示方法を記述します。ロジックは最小限にし、UI 関連のみにします。
  • ユーザー操作に応じて、イベントをビューモデルに渡します。
ビューモデル
  • アプリのデータを UI の状態に変換し、ビューに必要な現在の状態を維持します。
  • ビューにコールバック(コマンド)を公開し、リポジトリからデータを取得または変換します。
class BookingViewModel extends ChangeNotifier {
  final BookingRepository _repo;

  BookingViewModel(this._repo);

  List<Booking> _bookings = [];
  List<Booking> get bookings => List.unmodifiable(_bookings);

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

  Future<void> loadBookings() async {
    _isLoading = true;
    notifyListeners();

    _bookings = await _repo.getBookings();
    _isLoading = false;
    notifyListeners();
  }

  Future<void> cancelBooking(String id) async {
    await _repo.cancelBooking(id);
    _bookings = await _repo.getBookings();
    notifyListeners();
  }
}
リポジトリ(単一の信頼できる情報源)
  • データを変更できる唯一のクラスです。ほかのすべてのクラスはリポジトリからデータを読み取ります。
  • キャッシュ、エラー処理、データの更新ロジックを扱います。
  • サービスからの生データをドメインモデルに変換します。
class BookingRepository {
  final BookingApiService _apiService;
  final BookingLocalService _localService;

  BookingRepository(this._apiService, this._localService);

  Future<List<Booking>> getBookings() async {
    try {
      final remote = await _apiService.fetchBookings();
      await _localService.cacheBookings(remote);
      return remote;
    } catch (_) {
      return _localService.getCachedBookings();
    }
  }

  Future<void> cancelBooking(String id) async {
    await _apiService.cancelBooking(id);
    await _localService.removeCachedBooking(id);
  }
}
サービス
  • API エンドポイントをラップし、非同期のレスポンスオブジェクトを公開します。
  • データ読み込みを分離し、状態を保持しません。
class BookingApiService {
  final http.Client _client;
  BookingApiService(this._client);

  Future<List<Booking>> fetchBookings() async {
    final response = await _client.get(Uri.parse('/api/bookings'));
    if (response.statusCode != 200) {
      throw HttpException('Failed to load bookings');
    }
    final data = jsonDecode(response.body) as List;
    return data.map((json) => Booking.fromJson(json)).toList();
  }
}

3. 依存性注入

依存関係はコンストラクター経由で渡します。テスト時に実装を差し替えられるよう、抽象インターフェースを定義します。

// Abstract interface for the repository
abstract class BookingRepository {
  Future<List<Booking>> getBookings();
  Future<void> cancelBooking(String id);
}

// Concrete implementation
class BookingRepositoryImpl implements BookingRepository {
  final BookingApiService _api;
  BookingRepositoryImpl(this._api);

  @override
  Future<List<Booking>> getBookings() => _api.fetchBookings();

  @override
  Future<void> cancelBooking(String id) => _api.cancelBooking(id);
}

4. ユースケース(ドメインレイヤー)

次の場合にのみユースケースを導入します。

  • ロジックが複雑である、または UI レイヤーやデータレイヤーにきれいに収まらない場合。
  • 複数のビューモデル間でロジックが再利用される、または複数のリポジトリのデータを統合する場合。
class GetUpcomingBookingsUseCase {
  final BookingRepository _bookingRepo;
  final UserRepository _userRepo;

  GetUpcomingBookingsUseCase(this._bookingRepo, this._userRepo);

  Future<List<Booking>> call() async {
    final user = await _userRepo.getCurrentUser();
    final bookings = await _bookingRepo.getBookings();
    return bookings
        .where((b) => b.userId == user.id && b.date.isAfter(DateTime.now()))
        .toList();
  }
}

5. ワークフロー:新機能のスキャフォールディング

  1. サービスを作成する — 型付きレスポンスの解析に対応した API ラッパーを実装します。
  2. リポジトリを作成する — サービスを注入し、キャッシュとエラー処理のロジックを実装します。
  3. ビューモデルを作成する — リポジトリを注入し、UI の状態とコマンドを公開します。
  4. ビューを作成する — ビューモデルにバインドし、状態を描画してイベントをディスパッチします。
  5. DI を設定する — 依存性注入コンテナーにすべてのコンポーネントを登録します。
  6. 検証する — ビューがサービスに直接アクセスしないこと、データが単方向に流れることを確認します。

6. データストレージ

  • 設定や環境設定にはキーと値のストレージ(例:shared_preferences)を使用します。
  • 複雑なリレーショナルデータにはSQL ストレージ(例:drift、sqflite)を使用します。
  • サーバーの確認前に UI を更新する楽観的更新を実装し、応答性が高く感じられるようにします。
  • リポジトリでローカルデータソースとリモートデータソースを組み合わせ、オフラインファーストに対応します。

7. コーディング規約

  • 可能な場合は StatelessWidget を使用し、不要な StatefulWidget は避けます。
  • build メソッドはシンプルに保ち、描画に専念させます。
  • フィールドやトップレベル変数には final を優先します。クラスが対応している場合は const コンストラクターを優先します。
  • 公開 API では明示的な型付けを優先します(例:動的なシグネチャより Command0<void>)。
  • わかりやすい定数名を使用します(例:_kTableTodo より _todoTableName)。

参考資料

コメント