SuperAPI AI Directory

flutter-project-structure

Coding & development

View repository

Updated 10-15 20:05

About this skill

Flutter Project Structure (Single Package / Clean Architecture)

Core Principles

  • Single package, layer separation: Unlike Android/KMP, it does not use Gradle multi-modules; instead, it is separated into layers under lib/ .
  • Clean Architecture dependency direction: presentation → domain ← data. domainis a pure Dart layer that depends on nothing.
  • Feature는 presentation It is represented as folders under Code shared by multiple screens goes up to core/, and code used by only a single feature is placed inside that feature's folder.
  • No direct references between Features. Cross-feature shared logic goes up to core(shared widgets/utilities) or domain(models/use cases).

Directory Layout

This project follows the structure below (based onlib/ ):

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/                                ← 색상/타이포 등 디자인 토큰

Real example: lib/presentation/ingredient/, lib/presentation/saved_recipes/, lib/presentation/home/.

Each feature consists of 2 files:

<feature>_view_model.dart Internal composition of

// ① 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 Internal composition of

// ① 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) { ... }
}

What goes where

Code nature Location Real example
Domain model (freezed) lib/domain/model/ recipe.dart, ingredient.dart
Repository interface lib/domain/repository/ recipe_repository.dart
UseCase lib/domain/use_case/ get_saved_recipes_use_case.dart
DataSource interface lib/data/data_source/ recipe_data_source.dart
DataSource implementation lib/data/data_source/{local,remote}/ remote_recipe_data_source_impl.dart
Repository implementation lib/data/repository/ mock_recipe_repository_impl.dart
State + Action + ViewModel lib/presentation/<feature>/<feature>_view_model.dart ingredient_view_model.dart
Root + Screen widgets lib/presentation/<feature>/<feature>_screen.dart ingredient_screen.dart
Common widgets lib/core/presentation/components/ big_button.dart
Route definitions lib/core/routing/router.dart —
DI registration lib/core/di/di_setup.dart —
Shared error types lib/core/domain/error/ result.dart, network_error.dart
Feature-specific errors lib/domain/error/ bookmark_error.dart, new_recipe_error.dart

Dependency rules

Layer May depend on
presentation domain, core
data domain, core
domain Other domain files, core/domain (pure Dart only). Absolutely data, presentation, flutter/material forbidden
core/presentation flutter/material, core/domain
core/domain Pure Dart only

domain/ In package:flutter/... files, an import must not appear. Keep it pure Dart to ensure testability and reusability.


When adding a feature (checklist)

새 feature(예: reviews):

  • lib/domain/model/review.dart — define freezed model
  • lib/domain/repository/review_repository.dart — abstract interface classDefine the interface with
  • lib/domain/error/review_error.dart — feature-specific error enum if needed (implements Error)
  • lib/domain/use_case/get_reviews_use_case.dart — single execute() UseCase with
  • lib/data/data_source/remote/remote_review_data_source_impl.dart — implementation
  • lib/data/repository/mock_review_repository_impl.dart or real implementation
  • lib/presentation/reviews/reviews_view_model.dart — at the top of the file ReviewsState, ReviewsAction, at the bottom ReviewsViewModel
  • lib/presentation/reviews/reviews_screen.dart — at the top of the file ReviewsRoot, at the bottom ReviewsScreen
  • lib/core/di/di_setup.dart Register DataSource, Repository, UseCase, ViewModel in
  • lib/core/routing/route_paths.dart Path constants in , router.dart 에 GoRoute Add

By following this order, each layer only knows about the layers below it, so dependencies do not get inverted.


coreDecide whether to put it in or keep it in the feature

When placed inside the feature folder

  • State, Action, ViewModel, UI model, and screen-specific widgets used only by that feature.
  • 예: ingredient_state.dart 는 lib/presentation/ingredient/ It is below and is not used by other features.

coreWhen moving up to

  • Widgets/utilities/errors actually used by 2 or more features.
  • 예: BigButton, SearchInputField, Result, NetworkError.
  • However, do not move it up just because "it seems like it might be shared someday." Move it when the actual second usage appears.

Core libraries

Concerns Libraries Purpose
DI get_it GetIt.instance -based singleton/factory registration
Routing go_router Declarative routing, StatefulShellRoute
Model/State/Action freezed + freezed_annotation sealed/immutable data classes
JSON json_serializable + json_annotation fromJson/toJson generation
Streams rxdart BehaviorSubjectas a reactive repository
Testing flutter_test Widget/unit tests

All files that require code generation (*.freezed.dart, *.g.dart)은 dart run build_runner build --delete-conflicting-outputs is made with.