flutter-app-architecture について
Flutter アプリの設計やリファクタリングに使うアーキテクチャスキルです。レイヤーの責務、MVVM、依存性注入、単方向データフローを扱います。
主な機能
- 01UI・ロジック・データ層の指針
- 02MVVM コンポーネントの責務
- 03コンストラクターによる依存性注入
- 04Repository を単一のデータ源として管理
- 05ストレージとオフライン優先の指針
活用シーン
- 01レイヤー構造の Flutter アプリを構築
- 02ViewModel、Repository、Service のリファクタリング
- 03コンポーネント間の依存関係を設定
- 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. ワークフロー:新機能のスキャフォールディング
- サービスを作成する — 型付きレスポンスの解析に対応した API ラッパーを実装します。
- リポジトリを作成する — サービスを注入し、キャッシュとエラー処理のロジックを実装します。
- ビューモデルを作成する — リポジトリを注入し、UI の状態とコマンドを公開します。
- ビューを作成する — ビューモデルにバインドし、状態を描画してイベントをディスパッチします。
- DI を設定する — 依存性注入コンテナーにすべてのコンポーネントを登録します。
- 検証する — ビューがサービスに直接アクセスしないこと、データが単方向に流れることを確認します。
6. データストレージ
- 設定や環境設定にはキーと値のストレージ(例:
shared_preferences)を使用します。 - 複雑なリレーショナルデータにはSQL ストレージ(例:
drift、sqflite)を使用します。 - サーバーの確認前に UI を更新する楽観的更新を実装し、応答性が高く感じられるようにします。
- リポジトリでローカルデータソースとリモートデータソースを組み合わせ、オフラインファーストに対応します。
7. コーディング規約
- 可能な場合は
StatelessWidgetを使用し、不要なStatefulWidgetは避けます。 - build メソッドはシンプルに保ち、描画に専念させます。
- フィールドやトップレベル変数には
finalを優先します。クラスが対応している場合はconstコンストラクターを優先します。 - 公開 API では明示的な型付けを優先します(例:動的なシグネチャより
Command0<void>)。 - わかりやすい定数名を使用します(例:
_kTableTodoより_todoTableName)。
参考資料
コメント
Markdown 対応