flutter-clean-arch について
flutter-clean-arch は、Feature-first のクリーンアーキテクチャで Flutter アプリを構築する開発者向けのスキルです。Riverpod による状態管理、fpdart によるエラー処理、Dio + Retrofit による通信を、レイヤー別のガイドとともに扱います。
主な機能
- 01Feature-first のクリーンアーキテクチャ
- 02Riverpod の状態管理とコード生成
- 03fpdart Either によるエラー処理
- 04Dio + Retrofit の型安全なリクエスト
- 05データレイヤーでのレスポンス検証
活用シーン
- 01Flutter アプリのレイヤー構成の設計
- 02状態管理を伴う機能の実装
- 03REST API とエラー結果の処理
インストール
コマンドをコピーしてターミナルで実行します。
npx skills add https:/ / github.com/ duckyman-ai/ agent-skills --skill flutter-clean-archインストール後、Claude Code で/flutter-clean-archを入力して読み込まれたか確認します。
同梱ファイル
SKILL.md と一緒にインストールされる参考資料やスクリプト。
references/data_layer.mderror_handling.mdfeature_examples.mdmigration_guide.mdnetwork_setup.mdpresentation_layer.mdprovider_patterns.mdquick_start.mdretrofit_patterns.mdtesting_guide.md詳細
Flutter Clean Architecture スキル
Clean Architecture の原則に従い、機能優先の構成、状態管理に Riverpod、関数型エラーハンドリングに fpdart を使用して Flutter アプリケーションを生成します。
型安全な REST API 呼び出しには Dio + Retrofit を使用します。
中核となる原則
アーキテクチャ:Clean Architecture(機能優先)
- ドメイン層:純粋なビジネスロジック。依存関係はありません
- データ層:データソース、リポジトリの実装、データモデル
- プレゼンテーション層:UI、状態管理、ビューモデル
依存関係のルール:Presentation → Domain ← Data(Domain に外部依存関係はありません)
状態管理:コード生成を使用する Riverpod 3.0+
必須:Riverpod 3.0+ と Freezed 3.0+ — 古いパターンを使うと、コンパイルエラーやハルシネーションのループが発生します。
- Riverpod:
XxxRef refではなく、統一されたRef refを使用します- Freezed:ユニオン型には
sealed class、単一コンストラクターにはabstract classを使用します- パターンマッチング:
.map()/.when()ではなく、Dart 3 のswitchを使用します// Riverpod 3.x+ @riverpod SomeType someType(Ref ref) { ... } // Freezed 3.x+ — union type @freezed sealed class Result with _$Result { ... } // Freezed 3.x+ — pattern matching final res = switch (model) { First(:final a) => 'first $a', Second(:final b) => 'second $b', };Dart 3.3+、Riverpod 3.0+、Freezed 3.0+ が必要です。変更前後の例については、migration_guide.md を参照してください。
エラーハンドリング:関数型エラーハンドリングには fpdart の Either<Failure, T> を使用します
ネットワーキング:型安全な REST API 呼び出しには Dio + Retrofit を使用します
すべてのライブラリで常に最新の安定版を使用してください。依存関係を追加する前に、
pub.devで Riverpod、Freezed、Dio、Retrofit、fpdart、go_router の最新バージョンを確認してください。古いメジャーバージョン(例:Riverpod 2.x、Freezed 2.x)を安易に使用しないでください。
プロジェクト構成
lib/
├── core/
│ ├── constants/
│ │ ├── api_constants.dart
│ ├── errors/
│ │ ├── failures.dart
│ │ └── network_exceptions.dart
│ ├── network/
│ │ ├── dio_provider.dart
│ │ └── interceptors/
│ │ ├── auth_interceptor.dart
│ │ ├── logging_interceptor.dart
│ │ └── error_interceptor.dart
│ ├── storage/
│ ├── services/
│ ├── router/
│ │ └── app_router.dart
│ └── utils/
├── shared/
├── features/
│ └── [feature_name]/
│ ├── data/
│ │ ├── models/
│ │ │ └── [entity]_model.dart
│ │ ├── datasources/
│ │ │ └── [feature]_api_service.dart
│ │ └── repositories/
│ │ └── [feature]_repository_impl.dart
│ ├── domain/
│ │ ├── entities/
│ │ ├── repositories/
│ │ │ └── [feature]_repository.dart
│ │ └── usecases/
│ │ └── [action]_usecase.dart
│ └── presentation/
│ ├── providers/
│ │ └── [feature]_provider.dart
│ ├── screens/
│ │ └── [feature]_screen.dart
│ └── widgets/
│ └── [feature]_widget.dart
└── main.dart
クイックスタート
次の順序で機能を構築します:Domain → Data → Presentation。
1. ドメイン層
@freezed
sealed class User with _$User {
const factory User({required String id, required String name, required String email}) = _User;
}
abstract class UserRepository {
Future<Either<Failure, User>> getUser(String id);
}
class GetUser {
final UserRepository repository;
GetUser(this.repository);
Future<Either<Failure, User>> call(String id) => repository.getUser(id);
}
2. データ層
@freezed
sealed class UserModel with _$UserModel {
const UserModel._();
const factory UserModel({required String id, required String name, required String email}) = _UserModel;
factory UserModel.fromJson(Map<String, dynamic> json) => _$UserModelFromJson(json);
User toEntity() => User(id: id, name: name, email: email);
}
@RestApi()
abstract class UserApiService {
factory UserApiService(Dio dio) = _UserApiService;
@GET('/users/{id}')
Future<UserModel> getUser(@Path('id') String id);
}
3. プレゼンテーション層
@riverpod
class UserNotifier extends _$UserNotifier {
@override
FutureOr<User?> build() => null;
Future<void> fetchUser(String id) async {
state = const AsyncLoading();
final result = await ref.read(userRepositoryProvider).getUser(id);
state = result.fold(
(failure) => AsyncError(failure, StackTrace.current),
(user) => AsyncData(user),
);
}
}
すべてのファイルを含む、手順を追った完全なワークフローについては、quick_start.md を参照してください。
コード生成
# Generate all files
dart run build_runner build --delete-conflicting-outputs
# Watch mode
dart run build_runner watch --delete-conflicting-outputs
ベストプラクティス
推奨事項:
- ドメインエンティティは純粋な状態に保ちます(外部依存関係を持たせません)
- 不変のデータクラスには
sealedキーワードを付けて freezed を使用します - Either<Failure, T> ですべてのエラーケースを処理します
- 統一された
Ref型を使う riverpod_generator を使用します - モデル(データ)とエンティティ(ドメイン)を分離します
- ビジネスロジックはウィジェットではなくユースケースに配置します
- 型安全な API 呼び出しには Retrofit を使用します
- リポジトリで DioException を NetworkExceptions を使って処理します
- 横断的関心事(認証、ロギング)にはインターセプターを使用します
- リポジトリの境界で API レスポンスデータを検証します
- 外部画像には
Image.networkではなくCachedNetworkImageを使用します - 設定で API のベース URL を固定し、HTTPS を強制します
禁止事項:
- ドメイン層で Flutter/HTTP ライブラリをインポートしないでください
- プレゼンテーションロジックとビジネスロジックを混在させないでください
- Either を使用する場合、ウィジェット内で直接 try-catch を使わないでください
- 神オブジェクトや神プロバイダーを作成しないでください
- リポジトリパターンを省略しないでください
- 新しいコードでレガシーな
XxxRef型を使用しないでください - 検証せずに生の外部 URL をウィジェットに渡さないでください
- ユーザー入力による API ベース URL の実行時設定を許可しないでください
セキュリティ
API のレスポンスや外部コンテンツは信頼できない入力です。インジェクションやデータ破損を防ぐため、データ層の境界で検証とサニタイズを行ってください。
レスポンスの検証 — モデルにマッピングする前に、必ず API レスポンスの構造を検証してください。Retrofit と freezed は型付きデシリアライズに対応していますが、呼び出しを try-catch で囲み、リポジトリで重要なフィールド(ID、URL、数値の範囲)を確認してください。
@override
Future<Either<Failure, User>> getUser(String id) async {
try {
final userModel = await apiService.getUser(id);
if (userModel.id.isEmpty) {
return const Left(Failure.validation('Invalid user data'));
}
return Right(userModel.toEntity());
} on DioException catch (e) {
return Left(Failure.network(NetworkExceptions.fromDioError(e).message));
}
}
外部 URL — 生の API URL を直接 Image.network や WebView に渡さないでください。エラー処理を設定した CachedNetworkImage を使用し、URL のスキームを検証してください。
CachedNetworkImage(
imageUrl: user.avatarUrl ?? '',
errorWidget: (_, __, ___) => const Icon(Icons.person),
httpHeaders: {'Authorization': 'Bearer $token'},
)
入力のサニタイズ — API に送信する前にユーザー入力をサニタイズしてください。メールアドレスの形式を検証し、文字列の前後の空白を削除し、空の ID を拒否し、クエリパラメーターを適切にエンコードしてください。
ネットワークの強化 — AppConfig でベース URL を固定し(実行時にユーザーが変更できないようにする)、HTTPS を必須にし、本番環境では証明書ピンニングを使用して、保守的なタイムアウトを設定してください。
インターセプターの設定全体については network_setup.md を、リポジトリ境界での検証パターンについては data_layer.md をご覧ください。
よくある問題
| 問題 | 解決策 |
|---|---|
| Build runner の競合 | dart run build_runner clean && dart run build_runner build --delete-conflicting-outputs |
| Provider が見つからない | 生成されたファイルがインポートされていることを確認し、build_runner を実行してください |
| Either をアンラップできない | fold()、match()、または getOrElse() を使用して値を取り出してください |
XxxRef が見つからない | 代わりに統一された Ref 型を使用してください(Riverpod 3.x 以降) |
sealed キーワードのエラー | Dart 3.3 以降と Freezed 3.0 以降にアップグレードしてください |
.map / .when が見つからない | Freezed 3.0 以降ではこれらのメソッドが削除されています。代わりに Dart 3 の switch 式によるパターンマッチングを使用してください |
ナレッジリファレンス
主要ライブラリ(このスキルで使用):
- Flutter 3.19+:最新のフレームワーク機能
- Dart 3.3+:言語機能(パターン、レコード、
sealed修飾子) - Riverpod 3.0+:統一された
Ref型による状態管理 - Dio 5.9+:インターセプターを備えた HTTP クライアント
- Retrofit 4.9+:型安全な REST API コード生成
- freezed 3.0+:コード生成による不変データクラス
- json_serializable 6.x:JSON シリアライズ
- go_router 14.x+:宣言的ルーティング
- fpdart:Either 型による関数型エラー処理
参考資料
- quick_start.md - 機能作成のステップごとのワークフロー
- data_layer.md - モデル、Retrofit API サービス、リポジトリ、レスポンス検証
- presentation_layer.md - Provider、画面、ウィジェット、安全な画像読み込み
- network_setup.md - Dio Provider、インターセプター、ネットワーク例外、レスポンス検証
- error_handling.md - Either パターン、Failure 型、エラー処理方針
- retrofit_patterns.md - Retrofit API リクエストパターンの全体像
- provider_patterns.md - Riverpod の高度なパターン(ページネーション、キャッシュ、楽観的更新)
- testing_guide.md - 単体テスト、ウィジェットテスト、統合テストの方針
- feature_examples.md - Auth 機能の実装例全体
- migration_guide.md - Riverpod 2→3 と Freezed 2→3 の変更前・変更後の完全な例