关于 flutter-project-structure
为 Flutter 项目提供单包 Clean Architecture 目录与依赖规则的设计指南,适合希望规范功能、领域和数据层的开发者。它还给出新增功能时的文件布局与注册清单。
主要功能
- 01单包分层目录结构
- 02领域层纯 Dart 规则
- 03功能隔离与共享代码约定
- 04新增功能实施清单
- 05依赖注入与路由布局
适用场景
- 01规划 Flutter 项目的 lib/ 结构
- 02为新功能安排模型与用例
- 03检查跨层和跨功能依赖
- 04统一路由与依赖注册位置
安装
复制下面的命令,在终端中运行。
$
npx skills add https:/ / github.com/ junsuk5/ survival-flutter-skills --skill flutter-project-structure安装后在 Claude Code 中输入/flutter-project-structure确认已加载。
详细介绍
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 |
| 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/... 导入。保持纯 Dart,以确保可测试性和可复用性。
添加功能时(检查清单)
添加新 feature(例如 reviews)时:
-
lib/domain/model/review.dart— 定义 freezed 模型 -
lib/domain/repository/review_repository.dart— 使用abstract interface class定义接口 -
lib/domain/error/review_error.dart— 必要时定义功能专属错误枚举(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。 - 但不要仅仅因为“以后可能会共享”就移入
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 로 만든다.
评论
支持 Markdown