flutter-project-structure

Compare original and translation side by side

🇺🇸

Original

English
🇨🇳

Translation

Chinese

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

Flutter项目结构(单包 / Clean Architecture)

핵심 원칙

核心原则

  • 단일 패키지, 레이어 분리: Android/KMP처럼 Gradle 멀티모듈을 쓰지 않고
    lib/
    하위에서 레이어로 분리한다.
  • Clean Architecture 의존성 방향:
    presentation
    domain
    data
    .
    domain
    은 아무것도 의존하지 않는 순수 Dart 레이어.
  • Feature는
    presentation
    하위에 폴더로 표현한다.
    여러 화면에서 공유되는 코드는
    core/
    로 올리고, 단일 기능에만 쓰이는 코드는 해당 feature 폴더 안에 둔다.
  • Feature 간 직접 참조 금지. 교차 기능 공유 로직은
    core
    (공유 위젯·유틸) 또는
    domain
    (모델·유즈케이스)으로 올린다.

  • 单包分层:不采用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
내부 구성
dart
// ① State (freezed)

class IngredientState with _$IngredientState {
  const factory IngredientState({ ... }) = _IngredientState;
}

// ② Action (freezed sealed)

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
내부 구성
dart
// ① Root — getIt으로 VM 주입, ListenableBuilder로 상태 감지
class IngredientRoot extends StatelessWidget {
  
  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;

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

本项目遵循以下结构(基于
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枚举
│  └─ 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由两个文件组成:
<feature>_view_model.dart
内部结构
dart
// ① State (freezed)

class IngredientState with _$IngredientState {
  const factory IngredientState({ ... }) = _IngredientState;
}

// ② Action (freezed sealed)

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
内部结构
dart
// ① Root — 通过getIt注入VM,用ListenableBuilder监听状态
class IngredientRoot extends StatelessWidget {
  
  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;

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

어디에 무엇을 두는가

代码存放位置说明

코드 성격위치실제 예
도메인 모델 (freezed)
lib/domain/model/
recipe.dart
,
ingredient.dart
Repository 인터페이스
lib/domain/repository/
recipe_repository.dart
UseCase
lib/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 + ViewModel
lib/presentation/<feature>/<feature>_view_model.dart
ingredient_view_model.dart
Root + Screen 위젯
lib/presentation/<feature>/<feature>_screen.dart
ingredient_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

代码类型存放位置实际示例
领域模型(freezed)
lib/domain/model/
recipe.dart
ingredient.dart
Repository接口
lib/domain/repository/
recipe_repository.dart
UseCase
lib/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 + ViewModel
lib/presentation/<feature>/<feature>_view_model.dart
ingredient_view_model.dart
Root + Screen组件
lib/presentation/<feature>/<feature>_screen.dart
ingredient_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

의존성 규칙

依赖规则

레이어의존 가능
presentation
domain
,
core
data
domain
,
core
domain
다른
domain
파일,
core/domain
(순수 Dart만). 절대
data
,
presentation
,
flutter/material
금지
core/presentation
flutter/material
,
core/domain
core/domain
순수 Dart만
domain/
파일에서는
package:flutter/...
import가 나타나면 안 된다. 순수 Dart로 유지해 테스트와 재사용성을 확보한다.

层级可依赖对象
presentation
domain
core
data
domain
core
domain
其他
domain
文件、
core/domain
(仅限纯Dart)。绝对禁止依赖
data
presentation
flutter/material
core/presentation
flutter/material
core/domain
core/domain
仅限纯Dart
domain/
目录下的文件禁止引入
package:flutter/...
。保持纯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
    추가
이 순서를 지키면 각 레이어가 하위 레이어만 알게 되어 의존성이 뒤집히지 않는다.

新增feature(如
reviews
)时:
  • lib/domain/model/review.dart
    — 定义freezed模型
  • lib/domain/repository/review_repository.dart
    — 用
    abstract interface class
    定义接口
  • lib/domain/error/review_error.dart
    — 按需定义feature专属Error枚举(
    implements Error
  • lib/domain/use_case/get_reviews_use_case.dart
    — 创建包含单个
    execute()
    的UseCase
  • lib/data/data_source/remote/remote_review_data_source_impl.dart
    — 实现DataSource
  • lib/data/repository/mock_review_repository_impl.dart
    或实际Repository实现
  • 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에 둘지 판단

判断代码放core还是feature

feature 폴더 안에 두는 경우
  • 그 feature에서만 쓰는 State, Action, ViewModel, UI 모델, 화면 전용 위젯.
  • 예:
    ingredient_state.dart
    lib/presentation/ingredient/
    아래에 있고 다른 feature에서 쓰지 않는다.
core
로 올리는 경우
  • 2개 이상 feature가 실제로 쓰는 위젯/유틸/에러.
  • 예:
    BigButton
    ,
    SearchInputField
    ,
    Result
    ,
    NetworkError
    .
  • 단, "언젠가 공유될 것 같다"는 이유만으로는 올리지 않는다. 실제 두 번째 사용처가 생길 때 이동한다.

放在feature文件夹内的情况
  • 仅该feature使用的State、Action、ViewModel、UI模型、页面专属组件。
  • 示例:
    ingredient_state.dart
    放在
    lib/presentation/ingredient/
    下,不被其他feature引用。
移至core的情况
  • 被2个及以上feature实际使用的组件/工具/错误类型。
  • 示例:
    BigButton
    SearchInputField
    Result
    NetworkError
  • 注意:仅因「以后可能会共享」而移至core不可取,需等出现第二个实际使用场景时再迁移。

핵심 라이브러리

核心库

관심사라이브러리용도
DI
get_it
GetIt.instance
기반 싱글톤/팩토리 등록
라우팅
go_router
선언적 라우팅,
StatefulShellRoute
모델/상태/액션
freezed
+
freezed_annotation
sealed/immutable 데이터 클래스
JSON
json_serializable
+
json_annotation
fromJson
/
toJson
생성
스트림
rxdart
BehaviorSubject
로 반응형 저장소
테스트
flutter_test
위젯/단위 테스트
모든 코드 생성이 필요한 파일(
*.freezed.dart
,
*.g.dart
)은
dart run build_runner build --delete-conflicting-outputs
로 만든다.
关注点库名用途
依赖注入
get_it
基于
GetIt.instance
的单例/工厂注册
路由
go_router
声明式路由、
StatefulShellRoute
模型/状态/动作
freezed
+
freezed_annotation
密封/不可变数据类
JSON处理
json_serializable
+
json_annotation
生成
fromJson
/
toJson
方法
流处理
rxdart
BehaviorSubject
实现响应式存储
测试
flutter_test
组件/单元测试
所有需代码生成的文件(
*.freezed.dart
*.g.dart
)通过
dart run build_runner build --delete-conflicting-outputs
命令生成。