mcpField
F
flutter-project-structure

flutter-project-structure

github.com/junsuk5開発2446 回インストール24 週間前に更新
リポジトリ

flutter-project-structure について

単一パッケージの Clean Architecture を用いる Flutter プロジェクト向けの構成ガイドです。機能やレイヤーを整理する開発者に、依存ルールと機能追加のチェックリストを示します。

主な機能

  1. 01単一パッケージのレイヤー構成
  2. 02純粋な Dart ドメイン層の規則
  3. 03機能分離と共有コードの指針
  4. 04機能追加チェックリスト
  5. 05DI とルーティングの配置

活用シーン

  1. 01Flutter の lib/ 構成の計画
  2. 02機能ごとのモデルやユースケースの配置
  3. 03レイヤー間・機能間依存の確認
  4. 04ルーティングと DI 登録場所の統一

インストール

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

$npx skills add https://github.com/junsuk5/survival-flutter-skills --skill flutter-project-structure

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

詳細

Flutter プロジェクト構造(単一パッケージ/クリーンアーキテクチャ)

基本原則

  • 単一パッケージ、レイヤー分離:Android/KMP のように Gradle マルチモジュールを使わず、lib/ の下でレイヤーごとに分離します。
  • クリーンアーキテクチャの依存方向:presentation → domain ← data。domain は何にも依存しない純粋な Dart レイヤーです。
  • Feature は presentation の下にフォルダーとして配置します。 複数の画面で共有するコードは core/ に置き、単一の機能でのみ使うコードは、その feature フォルダー内に置きます。
  • Feature 間の直接参照は禁止です。機能をまたいで共有するロジックは、core(共有ウィジェット・ユーティリティ)または domain(モデル・ユースケース)に移します。

ディレクトリ構成

このプロジェクトは以下の構造に従います(lib/ を基準とします)。

lib/
├─ main.dart                          ← 앱 진입점, diSetup() 호출, MaterialApp.router
├─ core/                              ← 전역 공유 인프라
│  ├─ di/di_setup.dart                ← get_it 모듈 등록
│  ├─ routing/router.dart             ← GoRouter 정의
│  ├─ routing/route_paths.dart        ← 경로 상수
│  ├─ domain/error/                   ← Error, Result, NetworkError
│  └─ presentation/
│     ├─ components/                  ← 공용 위젯 (BigButton, SearchInputField 등)
│     └─ dialogs/                     ← 공용 다이얼로그
├─ data/
│  ├─ data_source/                    ← DataSource 인터페이스 + 구현 (local/, remote/)
│  ├─ repository/                     ← Repository 구현체 (*Impl)
│  └─ clipboard/                      ← 기타 플랫폼 서비스 구현
├─ domain/
│  ├─ model/                          ← freezed 도메인 모델
│  ├─ repository/                     ← Repository 인터페이스 (abstract interface class)
│  ├─ use_case/                       ← UseCase 클래스 (execute() 단일 진입점)
│  ├─ clipboard/                      ← 플랫폼 서비스 인터페이스
│  ├─ error/                          ← 기능별 Error enum
│  └─ filter/                         ← 도메인 값 객체
├─ presentation/
│  └─ <feature>/
│     ├─ <feature>_view_model.dart    ← 파일 상단: freezed State·Action, 하단: ChangeNotifier VM
│     └─ <feature>_screen.dart        ← 파일 상단: Root(VM 주입), 하단: Screen(순수 UI)
└─ ui/                                ← 색상/타이포 등 디자인 토큰

実例:lib/presentation/ingredient/、lib/presentation/saved_recipes/、lib/presentation/home/。

各 feature は 2 つのファイルで構成されます。

<feature>_view_model.dart の構成

// ① State (freezed)
@freezed
class IngredientState with _$IngredientState {
  const factory IngredientState({ ... }) = _IngredientState;
}

// ② Action (freezed sealed)
@freezed
sealed class IngredientAction with _$IngredientAction {
  const factory IngredientAction.load() = Load;
  const factory IngredientAction.delete(String id) = Delete;
}

// ③ ViewModel
class IngredientViewModel extends ChangeNotifier {
  IngredientState _state = const IngredientState();
  IngredientState get state => _state;

  void onAction(IngredientAction action) { ... }
}

<feature>_screen.dart の構成

// ① Root — getIt으로 VM 주입, ListenableBuilder로 상태 감지
class IngredientRoot extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    final vm = getIt<IngredientViewModel>();
    return ListenableBuilder(
      listenable: vm,
      builder: (_, __) => IngredientScreen(
        state: vm.state,
        onAction: vm.onAction,
      ),
    );
  }
}

// ② Screen — 순수 UI, state·onAction만 인자로 받음
class IngredientScreen extends StatelessWidget {
  const IngredientScreen({required this.state, required this.onAction});
  final IngredientState state;
  final void Function(IngredientAction) onAction;

  @override
  Widget build(BuildContext context) { ... }
}

配置場所

コードの種類配置場所実例
ドメインモデル(freezed)lib/domain/model/recipe.dart、ingredient.dart
Repository インターフェースlib/domain/repository/recipe_repository.dart
UseCaselib/domain/use_case/get_saved_recipes_use_case.dart
DataSource インターフェースlib/data/data_source/recipe_data_source.dart
DataSource 実装lib/data/data_source/{local,remote}/remote_recipe_data_source_impl.dart
Repository 実装lib/data/repository/mock_recipe_repository_impl.dart
State + Action + ViewModellib/presentation/<feature>/<feature>_view_model.dartingredient_view_model.dart
Root + Screen ウィジェットlib/presentation/<feature>/<feature>_screen.dartingredient_screen.dart
共通ウィジェットlib/core/presentation/components/big_button.dart
ルート定義lib/core/routing/router.dart—
DI 登録lib/core/di/di_setup.dart—
共有エラー型lib/core/domain/error/result.dart、network_error.dart
機能別エラーlib/domain/error/bookmark_error.dart、new_recipe_error.dart

依存関係のルール

レイヤー依存可能な対象
presentationdomain、core
datadomain、core
domain他の domain ファイル、core/domain(純粋な Dart のみ)。data、presentation、flutter/material は絶対に禁止
core/presentationflutter/material、core/domain
core/domain純粋な Dart のみ

domain/ ファイルに package:flutter/... の import が含まれていてはいけません。テストと再利用性を確保するため、純粋な Dart に保ちます。


機能を追加するとき(チェックリスト)

新しい feature(例:reviews)を追加するとき:

  • lib/domain/model/review.dart — freezed モデルを定義
  • lib/domain/repository/review_repository.dart — abstract interface class でインターフェースを定義
  • lib/domain/error/review_error.dart — 必要に応じて機能別エラー enum(implements Error)
  • lib/domain/use_case/get_reviews_use_case.dart — 単一の execute() を持つ UseCase
  • lib/data/data_source/remote/remote_review_data_source_impl.dart — 実装
  • lib/data/repository/mock_review_repository_impl.dart または実際の実装
  • lib/presentation/reviews/reviews_view_model.dart — ファイル上部に ReviewsState、ReviewsAction、下部に ReviewsViewModel
  • lib/presentation/reviews/reviews_screen.dart — ファイル上部に ReviewsRoot、下部に ReviewsScreen
  • lib/core/di/di_setup.dart に DataSource、Repository、UseCase、ViewModel を登録
  • lib/core/routing/route_paths.dart にパス定数を追加し、router.dart に GoRoute を追加

この順序を守ると、各レイヤーは下位レイヤーだけを認識するため、依存関係が逆転しません。


core に置くか feature に置くかの判断

feature フォルダー内に置く場合

  • その feature でのみ使う State、Action、ViewModel、UI モデル、画面専用ウィジェット。
  • 例:ingredient_state.dart は lib/presentation/ingredient/ の下にあり、他の feature では使いません。

core に置く場合

  • 2 つ以上の feature で実際に使うウィジェット/ユーティリティ/エラー。
  • 例:BigButton、SearchInputField、Result、NetworkError。
  • ただし、「いつか共有されそう」という理由だけで移動しないでください。2 つ目の実際の使用箇所ができた時点で移動します。

핵심 라이브러리

관심사라이브러리용도
DIget_itGetIt.instance 기반 싱글톤/팩토리 등록
라우팅go_router선언적 라우팅, StatefulShellRoute
모델/상태/액션freezed + freezed_annotationsealed/immutable 데이터 클래스
JSONjson_serializable + json_annotationfromJson/toJson 생성
스트림rxdartBehaviorSubject로 반응형 저장소
테스트flutter_test위젯/단위 테스트

모든 코드 생성이 필요한 파일(*.freezed.dart, *.g.dart)은 dart run build_runner build --delete-conflicting-outputs 로 만든다.

コメント