flutter-apply-architecture-best-practices について
Flutter 開発者向けに、レイヤー構成と機能実装の手順を示すスキルです。MVVM、Repository パターン、各段階でのテストを重視します。
主な機能
- 01MVVM による UI 状態管理
- 02Repository によるデータアクセス分離
- 03必要に応じた Use Case 抽出
- 04機能実装ワークフロー
- 05ユニットテストによる検証
活用シーン
- 01Flutter アプリのレイヤー設計
- 02サービスを使う機能の実装
- 03複雑または共通の業務ロジックの分離
- 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'),
),
],
);
},
);
}
}
コメント
Markdown 対応