flutter-project-structure
Compare original and translation side by side
🇺🇸
Original
English🇨🇳
Translation
ChineseFlutter 프로젝트 구조 (단일 패키지 / Clean Architecture)
Flutter项目结构(单包 / Clean Architecture)
핵심 원칙
核心原则
- 단일 패키지, 레이어 분리: Android/KMP처럼 Gradle 멀티모듈을 쓰지 않고 하위에서 레이어로 분리한다.
lib/ - Clean Architecture 의존성 방향: →
presentation←domain.data은 아무것도 의존하지 않는 순수 Dart 레이어.domain - Feature는 하위에 폴더로 표현한다. 여러 화면에서 공유되는 코드는
presentation로 올리고, 단일 기능에만 쓰이는 코드는 해당 feature 폴더 안에 둔다.core/ - Feature 간 직접 참조 금지. 교차 기능 공유 로직은 (공유 위젯·유틸) 또는
core(모델·유즈케이스)으로 올린다.domain
- 单包分层:不采用Android/KMP那样的Gradle多模块模式,在目录下按层级拆分。
lib/ - Clean Architecture依赖方向:→
presentation←domain。data是不依赖任何外部的纯Dart层。domain - Feature在下以文件夹形式体现。多页面共享的代码移至
presentation,仅单个功能使用的代码放在对应feature文件夹内。core/ - 禁止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.dartdart
// ① 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.dartdart
// ① 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.dartdart
// ① 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.dartdart
// ① 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) | | |
| Repository 인터페이스 | | |
| UseCase | | |
| DataSource 인터페이스 | | |
| DataSource 구현 | | |
| Repository 구현 | | |
| State + Action + ViewModel | | |
| Root + Screen 위젯 | | |
| 공용 위젯 | | |
| 라우트 정의 | | — |
| DI 등록 | | — |
| 공유 에러 타입 | | |
| 기능별 에러 | | |
| 代码类型 | 存放位置 | 实际示例 |
|---|---|---|
| 领域模型(freezed) | | |
| Repository接口 | | |
| UseCase | | |
| DataSource接口 | | |
| DataSource实现 | | |
| Repository实现 | | |
| State + Action + ViewModel | | |
| Root + Screen组件 | | |
| 通用组件 | | |
| 路由定义 | | — |
| DI注册 | | — |
| 共享错误类型 | | |
| 功能专属错误 | | |
의존성 규칙
依赖规则
| 레이어 | 의존 가능 |
|---|---|
| |
| |
| 다른 |
| |
| 순수 Dart만 |
domain/package:flutter/...| 层级 | 可依赖对象 |
|---|---|
| |
| |
| 其他 |
| |
| 仅限纯Dart |
domain/package:flutter/...기능을 추가할 때 (체크리스트)
新增功能检查清单
새 feature(예: )를 추가할 때:
reviews- — freezed 모델 정의
lib/domain/model/review.dart - —
lib/domain/repository/review_repository.dart로 인터페이스 정의abstract interface class - — 필요한 경우 feature별 에러 enum (
lib/domain/error/review_error.dart)implements Error - — 단일
lib/domain/use_case/get_reviews_use_case.dart를 가진 UseCaseexecute() - — 구현
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, 하단에ReviewsActionReviewsViewModel - — 파일 상단에
lib/presentation/reviews/reviews_screen.dart, 하단에ReviewsRootReviewsScreen - 에 DataSource, Repository, UseCase, ViewModel 등록
lib/core/di/di_setup.dart - 에 경로 상수,
lib/core/routing/route_paths.dart에router.dart추가GoRoute
이 순서를 지키면 각 레이어가 하위 레이어만 알게 되어 의존성이 뒤집히지 않는다.
新增feature(如)时:
reviews- — 定义freezed模型
lib/domain/model/review.dart - — 用
lib/domain/repository/review_repository.dart定义接口abstract interface class - — 按需定义feature专属Error枚举(
lib/domain/error/review_error.dart)implements Error - — 创建包含单个
lib/domain/use_case/get_reviews_use_case.dart的UseCaseexecute() - — 实现DataSource
lib/data/data_source/remote/remote_review_data_source_impl.dart - 或实际Repository实现
lib/data/repository/mock_review_repository_impl.dart - — 文件顶部定义
lib/presentation/reviews/reviews_view_model.dart、ReviewsState,底部实现ReviewsActionReviewsViewModel - — 文件顶部定义
lib/presentation/reviews/reviews_screen.dart,底部实现ReviewsRootReviewsScreen - 在中注册DataSource、Repository、UseCase、ViewModel
lib/core/di/di_setup.dart - 在添加路径常量,在
lib/core/routing/route_paths.dart中添加router.dartGoRoute
遵循此顺序可确保各层级仅依赖下层,避免依赖倒置。
core
에 올릴지 feature에 둘지 판단
core判断代码放core还是feature
feature 폴더 안에 두는 경우
- 그 feature에서만 쓰는 State, Action, ViewModel, UI 모델, 화면 전용 위젯.
- 예: 는
ingredient_state.dart아래에 있고 다른 feature에서 쓰지 않는다.lib/presentation/ingredient/
core- 2개 이상 feature가 실제로 쓰는 위젯/유틸/에러.
- 예: ,
BigButton,SearchInputField,Result.NetworkError - 단, "언젠가 공유될 것 같다"는 이유만으로는 올리지 않는다. 실제 두 번째 사용처가 생길 때 이동한다.
放在feature文件夹内的情况
- 仅该feature使用的State、Action、ViewModel、UI模型、页面专属组件。
- 示例:放在
ingredient_state.dart下,不被其他feature引用。lib/presentation/ingredient/
移至core的情况
- 被2个及以上feature实际使用的组件/工具/错误类型。
- 示例:、
BigButton、SearchInputField、Result。NetworkError - 注意:仅因「以后可能会共享」而移至core不可取,需等出现第二个实际使用场景时再迁移。
핵심 라이브러리
核心库
| 관심사 | 라이브러리 | 용도 |
|---|---|---|
| DI | | |
| 라우팅 | | 선언적 라우팅, |
| 모델/상태/액션 | | sealed/immutable 데이터 클래스 |
| JSON | | |
| 스트림 | | |
| 테스트 | | 위젯/단위 테스트 |
모든 코드 생성이 필요한 파일(, )은 로 만든다.
*.freezed.dart*.g.dartdart run build_runner build --delete-conflicting-outputs| 关注点 | 库名 | 用途 |
|---|---|---|
| 依赖注入 | | 基于 |
| 路由 | | 声明式路由、 |
| 模型/状态/动作 | | 密封/不可变数据类 |
| JSON处理 | | 生成 |
| 流处理 | | 用 |
| 测试 | | 组件/单元测试 |
所有需代码生成的文件(、)通过命令生成。
*.freezed.dart*.g.dartdart run build_runner build --delete-conflicting-outputs