architecture-feature-first について
機能単位の構成とレイヤードアーキテクチャを用いる Flutter 向けスキルです。複数の状態管理方式に対応し、ロジックの配置、ファイル構成、依存関係の設定を示します。
主な機能
- 01機能単位のファイル構成
- 02UI・ロジック・データ層の境界
- 03状態管理方式に依存しない指針
- 04Repository と Service の責務
- 05依存性注入と機能追加の手順
活用シーン
- 01新しい Flutter アプリや機能の構成
- 02タイプ別構成から機能別構成へのリファクタリング
- 03ビジネスロジックの配置層を判断
- 04複雑なロジック向けのドメイン層追加
インストール
コマンドをコピーしてターミナルで実行します。
$
npx skills add https:/ / github.com/ evanca/ flutter-ai-rules --skill architecture-feature-firstインストール後、Claude Code で/architecture-feature-firstを入力して読み込まれたか確認します。
詳細
Flutter アーキテクチャ — フィーチャーファーストスキル
このスキルでは、推奨されるレイヤードアーキテクチャとフィーチャーファーストのファイル編成を使用して、Flutter アプリケーションを設計、構成、実装する方法を定義します。
状態管理方式に依存しません。UI レイヤーのビジネスロジックを担うコンポーネントは、選択した状態管理方式に応じて ViewModel、Controller、Cubit、Bloc、Provider、Notifier などと呼ばれます。アーキテクチャのルールは、いずれにも等しく適用されます。
使用する場面
次のような場合にこのスキルを使用します。
- 新しい Flutter アプリや機能のフォルダー/ファイル構成を設計するとき。
- 新しい View、ViewModel、Repository、Service を作成するとき。
- あるロジックをどのレイヤーが担うかを決めるとき。
- コンポーネント間の依存性注入を設定するとき。
- 複雑なビジネスロジックのためにドメイン(ロジック)レイヤーを追加するとき。
- 既存アプリの構成をタイプファーストからフィーチャーファーストにリファクタリングするとき。
1. レイヤー
すべてのアプリをUI レイヤーとデータレイヤーに分けます。複雑なアプリに限り、その間にロジック(ドメイン)レイヤーを追加します。
┌──────────────────────────────────────────────────────────────┐
│ UI Layer │ Views + business logic holders │
│ │ (ViewModel / Cubit / Controller / Provider) │
├──────────────────────────────────────────────────────────────┤
│ Logic Layer │ Use Cases / Interactors (optional) │
├──────────────────────────────────────────────────────────────┤
│ Data Layer │ Repositories + Services │
└──────────────────────────────────────────────────────────────┘
ルール:
- 隣接するレイヤー間でのみ通信できます。UI レイヤーから Service に直接アクセスしてはいけません。
- ロジックレイヤーを追加するのは、ビジネスロジックがビジネスロジックを担うコンポーネントに収まらないほど複雑な場合、または複数の画面で再利用される場合に限ります。
- データの変更は常にデータレイヤー(SSOT = Repository)で行います。UI レイヤーやロジックレイヤーでは変更しません。
- 単方向のデータフローに従います。状態は下方向(Data → UI)に流れ、イベントは上方向(UI → Data)に流れます。
2. フィーチャーファーストのファイル構成
コードはタイプ別ではなく、機能別に整理します。1 つの機能に属するすべてのレイヤーを、1 つのディレクトリにまとめます。
ディレクトリ構成の例
lib/
├── app.dart
├── main.dart
├── core/ # Shared utilities, theme, DI setup
│ ├── di/
│ │ └── service_locator.dart
│ ├── theme/
│ │ └── app_theme.dart
│ └── network/
│ └── api_client.dart
├── features/
│ ├── auth/
│ │ ├── data/
│ │ │ ├── auth_repository.dart
│ │ │ └── auth_api_service.dart
│ │ ├── domain/ # Optional — only for complex logic
│ │ │ └── login_usecase.dart
│ │ └── ui/
│ │ ├── auth_viewmodel.dart
│ │ ├── login_screen.dart
│ │ └── widgets/
│ │ └── login_form.dart
│ └── profile/
│ ├── data/
│ │ ├── profile_repository.dart
│ │ └── profile_api_service.dart
│ └── ui/
│ ├── profile_viewmodel.dart
│ └── profile_screen.dart
└── shared/ # Shared widgets, models, extensions
├── models/
│ └── user.dart
└── widgets/
└── loading_indicator.dart
各機能ディレクトリには、その機能に必要なファイルを配置します。ファイル名は、選択した状態管理方式に応じて付けます。
| 方式 | ビジネスロジックを担うファイル |
|---|---|
| MVVM / ChangeNotifier | *_viewmodel.dart / *_controller.dart |
| BLoC | *_cubit.dart / *_bloc.dart |
| Provider / Riverpod | *_provider.dart / *_notifier.dart |
3. コンポーネントの責務
View
- ユーザーへのデータの表示方法を記述します。ロジックは最小限にし、UI に関するものだけにします。
- ユーザーの操作に応じて、イベントをビジネスロジックを担うコンポーネントに渡します。
- 再利用可能なウィジェットは、
widgets/サブディレクトリ内の個別コンポーネントとして切り出します。 - 可能な限り
StatelessWidgetを使用し、build メソッドはシンプルに保ちます。
ビジネスロジックを担うコンポーネント(ViewModel / Cubit / Controller / Provider)
- アプリのデータを UI の状態に変換するロジックを持ち、View に必要な現在の状態を維持します。
- View にコールバック(コマンド)を公開し、Repository からデータを取得して変換します。
class AuthViewModel extends ChangeNotifier {
final AuthRepository _authRepo;
AuthViewModel(this._authRepo);
bool _isLoading = false;
bool get isLoading => _isLoading;
String? _error;
String? get error => _error;
Future<bool> login(String email, String password) async {
_isLoading = true;
_error = null;
notifyListeners();
try {
await _authRepo.login(email, password);
return true;
} catch (e) {
_error = e.toString();
return false;
} finally {
_isLoading = false;
notifyListeners();
}
}
}
Repository
- 指定された種類のモデルデータに対する唯一の信頼できる情報源(SSOT)です。
- データを変更できる唯一のクラスです。それ以外のすべてのクラスは、このクラスからデータを読み取ります。
- キャッシュ、エラー処理、データ更新のロジックを担います。
- Service からの生データをドメインモデルに変換します。
Service
- API エンドポイントをラップし、非同期レスポンスオブジェクトを公開します。
- データ読み込みを分離し、状態は持ちません。
4. ドメインレイヤー(ユースケース)
ユースケース/インタラクターを導入するのは、次の場合に限ります。
- ロジックが複雑で、UI レイヤーまたはデータレイヤーに適切に収まらない場合。
- ロジックが複数のビジネスロジックを担うコンポーネントで再利用される場合、または複数の Repository のデータを統合する場合。
シンプルな CRUD アプリにドメインレイヤーを追加しないでください。
5. 依存性注入
コンポーネントに依存関係を提供するために依存性注入を使用し、テスト容易性と柔軟性を高めます。
- コンストラクターを通じて、ビジネスロジックを担うコンポーネントに Repository を渡します。
- コンストラクターを通じて、Repository に Service を渡します。
- 実装を利用側の変更なしに差し替えられるよう、抽象インターフェースを定義します。
// In service_locator.dart — register dependencies at startup
void setupDependencies() {
final apiClient = ApiClient();
// Services
final authService = AuthApiService(apiClient);
final profileService = ProfileApiService(apiClient);
// Repositories
final authRepo = AuthRepository(authService);
final profileRepo = ProfileRepository(profileService);
// Register with your DI framework (get_it, provider, riverpod, etc.)
getIt.registerSingleton<AuthRepository>(authRepo);
getIt.registerSingleton<ProfileRepository>(profileRepo);
}
6. ワークフロー:新機能の追加
features/<name>/ディレクトリを作成します。data/、ui/、必要に応じてdomain/サブディレクトリを作成します。- サービスを実装します — API エンドポイントを
data/<name>_api_service.dartでラップします。 - リポジトリを実装します — サービスを注入し、
data/<name>_repository.dartでキャッシュとエラーハンドリングを追加します。 - ViewModel を実装します — リポジトリを注入し、
ui/<name>_viewmodel.dartで UI の状態とコマンドを公開します。 - ビューを実装します — ViewModel にバインドし、状態を描画して、
ui/<name>_screen.dartでイベントをディスパッチします。 - DI に登録します — 新しいサービス、リポジトリ、ViewModel をサービスロケーターに追加します。
- 確認します — ビューがサービスに直接アクセスしていないこと、およびデータが一方向に流れることを確認します。
参考資料
コメント
Markdown 対応