mcpField
F
flutter-project-structure

flutter-project-structure

github.com/junsuk5Development2446 installsUpdated 24w ago
Repository

About flutter-project-structure

A structural guide for Flutter projects using a single-package Clean Architecture layout. It is for developers organizing features and layers, and includes dependency rules plus a checklist for adding features.

Key features

  1. 01Single-package layered layout
  2. 02Pure Dart domain layer rules
  3. 03Feature isolation and shared-code guidance
  4. 04Feature addition checklist
  5. 05Dependency injection and routing layout

Use cases

  1. 01Plan a Flutter lib/ structure
  2. 02Place models and use cases for a feature
  3. 03Review cross-layer and cross-feature dependencies
  4. 04Standardize routing and dependency registration

Install

Copy the command and run it in a terminal.

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

After installing, type/flutter-project-structurein Claude Code to confirm it loaded.

Details

From the project README

Flutter 프로젝트 구조 (단일 패키지 / Clean Architecture)

핵심 원칙

  • 단일 패키지, 레이어 분리: Android/KMP처럼 Gradle 멀티모듈을 쓰지 않고 lib/ 하위에서 레이어로 분리한다.
  • Clean Architecture 의존성 방향: 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 — 필요한 경우 feature별 에러 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.
  • 단, "언젠가 공유될 것 같다"는 이유만으로는 올리지 않는다. 실제 두 번째 사용처가 생길 때 이동한다.

핵심 라이브러리

관심사라이브러리용도
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 로 만든다.

Comments

Markdown supported