Loading...
Loading...
Flutter 단일 패키지 프로젝트의 레이어 구조, 디렉터리 규칙, 의존성 방향을 정리한 스킬. 새 기능을 어디에 둘지, 새 화면·리포지토리·유즈케이스를 어느 디렉터리에 만들지, "폴더 구조를 어떻게 잡을까", "feature를 어디에 둘까", "core에 넣을까 말까" 같은 질문이 나올 때 반드시 사용하세요. "프로젝트 구조", "디렉터리 구조", "레이어 분리", "clean architecture", "where does X live", "새 기능 추가" 같은 표현에 트리거합니다.
npx skill4agent add junsuk5/survival-flutter-skills flutter-project-structurelib/presentationdomaindatadomainpresentationcore/coredomainlib/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>_view_model.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// ① 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 등록 | | — |
| 공유 에러 타입 | | |
| 기능별 에러 | | |
| 레이어 | 의존 가능 |
|---|---|
| |
| |
| 다른 |
| |
| 순수 Dart만 |
domain/package:flutter/...reviewslib/domain/model/review.dartlib/domain/repository/review_repository.dartabstract interface classlib/domain/error/review_error.dartimplements Errorlib/domain/use_case/get_reviews_use_case.dartexecute()lib/data/data_source/remote/remote_review_data_source_impl.dartlib/data/repository/mock_review_repository_impl.dartlib/presentation/reviews/reviews_view_model.dartReviewsStateReviewsActionReviewsViewModellib/presentation/reviews/reviews_screen.dartReviewsRootReviewsScreenlib/core/di/di_setup.dartlib/core/routing/route_paths.dartrouter.dartGoRoutecoreingredient_state.dartlib/presentation/ingredient/coreBigButtonSearchInputFieldResultNetworkError| 관심사 | 라이브러리 | 용도 |
|---|---|---|
| DI | | |
| 라우팅 | | 선언적 라우팅, |
| 모델/상태/액션 | | sealed/immutable 데이터 클래스 |
| JSON | | |
| 스트림 | | |
| 테스트 | | 위젯/단위 테스트 |
*.freezed.dart*.g.dartdart run build_runner build --delete-conflicting-outputs