297 lines
9.2 KiB
Markdown
297 lines
9.2 KiB
Markdown
|
|
# Directory Structure
|
||
|
|
|
||
|
|
> How Flutter app code is organized in this project.
|
||
|
|
|
||
|
|
---
|
||
|
|
|
||
|
|
## Overview
|
||
|
|
|
||
|
|
This Flutter app follows a **feature-first architecture** with clear separation of concerns:
|
||
|
|
|
||
|
|
- **Feature modules** in `features/` for bounded product capabilities
|
||
|
|
- **Core infrastructure** in `core/` for cross-feature protocols
|
||
|
|
- **Shared UI components** in `shared/` for reusable widgets
|
||
|
|
- **Data layer** in `data/` for infrastructure abstractions
|
||
|
|
|
||
|
|
---
|
||
|
|
|
||
|
|
## Directory Layout
|
||
|
|
|
||
|
|
```
|
||
|
|
apps/lib/
|
||
|
|
├── main.dart # Only root entry file
|
||
|
|
├── app/ # App bootstrap & DI
|
||
|
|
│ ├── di/ # Dependency injection setup
|
||
|
|
│ ├── router.dart # Route definitions
|
||
|
|
│ └── app.dart # App configuration
|
||
|
|
├── core/ # Cross-feature infrastructure
|
||
|
|
│ ├── auth/ # Session store, auth state
|
||
|
|
│ ├── config/ # Env configuration
|
||
|
|
│ ├── logging/ # Structured logging
|
||
|
|
│ └── network/ # HTTP client, error mapping
|
||
|
|
├── data/ # Shared infrastructure ONLY
|
||
|
|
│ ├── cache/ # Cache implementations
|
||
|
|
│ ├── network/ # Network adapters
|
||
|
|
│ └── storage/ # Local storage
|
||
|
|
├── features/ # Feature modules
|
||
|
|
│ ├── auth/ # Authentication feature
|
||
|
|
│ ├── home/ # Home feature
|
||
|
|
│ ├── divination/ # Divination feature
|
||
|
|
│ ├── settings/ # Settings feature
|
||
|
|
│ └── ... # Other features
|
||
|
|
├── shared/ # Reusable UI components
|
||
|
|
│ ├── widgets/ # Shared widgets
|
||
|
|
│ └── theme/ # App theme, design tokens
|
||
|
|
└── l10n/ # Localization
|
||
|
|
```
|
||
|
|
|
||
|
|
---
|
||
|
|
|
||
|
|
## Module Organization
|
||
|
|
|
||
|
|
### Feature Module Structure
|
||
|
|
|
||
|
|
Each feature follows consistent structure:
|
||
|
|
|
||
|
|
```
|
||
|
|
features/<feature>/
|
||
|
|
├── data/ # Data layer
|
||
|
|
│ ├── apis/ # API clients
|
||
|
|
│ ├── repositories/ # Repository implementations
|
||
|
|
│ ├── services/ # Feature-specific services
|
||
|
|
│ └── models/ # Data models/DTOs
|
||
|
|
└── presentation/ # Presentation layer
|
||
|
|
├── bloc/ # State management (BLoC/Cubit)
|
||
|
|
├── screens/ # Screen widgets
|
||
|
|
└── widgets/ # Feature-specific widgets
|
||
|
|
```
|
||
|
|
|
||
|
|
**Example: `features/auth/`**
|
||
|
|
|
||
|
|
```
|
||
|
|
features/auth/
|
||
|
|
├── data/
|
||
|
|
│ ├── apis/
|
||
|
|
│ │ └── auth_api.dart # HTTP API calls
|
||
|
|
│ ├── repositories/
|
||
|
|
│ │ └── auth_repository.dart # Repository implementation
|
||
|
|
│ └── models/
|
||
|
|
│ ├── auth_user.dart # User model
|
||
|
|
│ └── session_response.dart # Session DTO
|
||
|
|
└── presentation/
|
||
|
|
├── bloc/
|
||
|
|
│ ├── auth_bloc.dart # AuthBloc (ChangeNotifier)
|
||
|
|
│ └── auth_state.dart # AuthState
|
||
|
|
└── screens/
|
||
|
|
└── login_screen.dart # Login screen widget
|
||
|
|
```
|
||
|
|
|
||
|
|
### Core Module Structure
|
||
|
|
|
||
|
|
**Core contains cross-feature infrastructure:**
|
||
|
|
|
||
|
|
```
|
||
|
|
core/
|
||
|
|
├── auth/
|
||
|
|
│ └── session_store.dart # Global session management
|
||
|
|
├── config/
|
||
|
|
│ └── env.dart # Environment configuration
|
||
|
|
├── logging/
|
||
|
|
│ ├── logger.dart # Logger interface
|
||
|
|
│ ├── log_service.dart # LogService implementation
|
||
|
|
│ ├── log_entry.dart # Log entry model
|
||
|
|
│ └── error_handler.dart # Global error handler
|
||
|
|
└── network/
|
||
|
|
├── api_problem.dart # RFC7807 error model
|
||
|
|
└── api_problem_mapper.dart # Error mapping
|
||
|
|
```
|
||
|
|
|
||
|
|
### Shared Widget Structure
|
||
|
|
|
||
|
|
**Shared contains reusable UI components:**
|
||
|
|
|
||
|
|
```
|
||
|
|
shared/
|
||
|
|
├── widgets/
|
||
|
|
│ ├── app_banner.dart # App-wide banner
|
||
|
|
│ ├── app_loading_indicator.dart # Loading indicator
|
||
|
|
│ ├── bottom_nav_bar.dart # Navigation bar
|
||
|
|
│ └── divination/ # Divination domain widgets
|
||
|
|
│ ├── gua_icon.dart # Gua icon widget
|
||
|
|
│ ├── yao_glyph.dart # Yao glyph widget
|
||
|
|
│ └── ...
|
||
|
|
└── theme/
|
||
|
|
├── app_theme.dart # Theme definition
|
||
|
|
└── design_tokens.dart # Spacing, radius, colors
|
||
|
|
```
|
||
|
|
|
||
|
|
---
|
||
|
|
|
||
|
|
## Placement Rules
|
||
|
|
|
||
|
|
### Where to Put Code
|
||
|
|
|
||
|
|
| Code Type | Location | Reason |
|
||
|
|
|-----------|----------|--------|
|
||
|
|
| Feature business logic | `features/<feature>/` | Bounded context |
|
||
|
|
| Cross-feature protocol | `core/` | Shared by multiple features |
|
||
|
|
| Reusable UI widget | `shared/widgets/` | Reusable by multiple screens |
|
||
|
|
| Infrastructure abstraction | `data/` | Cache/network/storage |
|
||
|
|
| Feature repository/model | `features/<feature>/data/` | Feature-scoped data |
|
||
|
|
|
||
|
|
### Decision Tree
|
||
|
|
|
||
|
|
```
|
||
|
|
Is it feature-specific business logic?
|
||
|
|
→ Yes: features/<feature>/
|
||
|
|
→ No: Is it reusable UI?
|
||
|
|
→ Yes: shared/widgets/
|
||
|
|
→ No: Is it infrastructure?
|
||
|
|
→ Yes: data/
|
||
|
|
→ No: Is it cross-feature protocol?
|
||
|
|
→ Yes: core/
|
||
|
|
→ No: Re-evaluate
|
||
|
|
```
|
||
|
|
|
||
|
|
### Forbidden Patterns
|
||
|
|
|
||
|
|
**❌ Do NOT:**
|
||
|
|
|
||
|
|
1. Place feature business repositories in `data/`
|
||
|
|
- Wrong: `data/repositories/auth_repository.dart`
|
||
|
|
- Right: `features/auth/data/repositories/auth_repository.dart`
|
||
|
|
|
||
|
|
2. Create directories under `lib/` other than allowed second-level
|
||
|
|
- Wrong: `lib/utils/`, `lib/helpers/`, `lib/constants/`
|
||
|
|
- Right: Use `core/`, `shared/`, or feature-specific locations
|
||
|
|
|
||
|
|
3. Put feature-specific UI in `shared/widgets/`
|
||
|
|
- Wrong: `shared/widgets/auth_login_form.dart`
|
||
|
|
- Right: `features/auth/presentation/widgets/login_form.dart`
|
||
|
|
|
||
|
|
4. Import feature data layer from other features
|
||
|
|
- Wrong: `import 'package:app/features/auth/data/repositories/auth_repository.dart'` in `features/home/`
|
||
|
|
- Right: Access via app-level facade or DI
|
||
|
|
|
||
|
|
---
|
||
|
|
|
||
|
|
## Naming Conventions
|
||
|
|
|
||
|
|
### Files
|
||
|
|
|
||
|
|
- **snake_case**: `auth_bloc.dart`, `login_screen.dart`
|
||
|
|
- **Feature prefix for shared**: `app_loading_indicator.dart`, `app_banner.dart`
|
||
|
|
|
||
|
|
### Classes
|
||
|
|
|
||
|
|
- **PascalCase**: `AuthBloc`, `AuthState`, `LoginScreen`
|
||
|
|
- **Suffixes**:
|
||
|
|
- `*Bloc` / `*Cubit` - State management
|
||
|
|
- `*Repository` - Data access
|
||
|
|
- `*Api` - HTTP clients
|
||
|
|
- `*Service` - Business services
|
||
|
|
- `*Screen` - Screen widgets
|
||
|
|
- `*Widget` - Reusable widgets
|
||
|
|
|
||
|
|
### Directories
|
||
|
|
|
||
|
|
- **Plural for collections**: `screens/`, `widgets/`, `models/`
|
||
|
|
- **Singular for features**: `auth/`, `home/`, `divination/`
|
||
|
|
|
||
|
|
---
|
||
|
|
|
||
|
|
## Examples
|
||
|
|
|
||
|
|
### Well-organized Feature: `features/divination/`
|
||
|
|
|
||
|
|
```
|
||
|
|
features/divination/
|
||
|
|
├── data/
|
||
|
|
│ ├── apis/
|
||
|
|
│ │ └── divination_api.dart # HTTP API
|
||
|
|
│ ├── repositories/
|
||
|
|
│ │ └── divination_repository.dart # Repository
|
||
|
|
│ ├── services/
|
||
|
|
│ │ └── voice_recorder.dart # Feature service
|
||
|
|
│ └── models/
|
||
|
|
│ ├── divination_result.dart # Domain model
|
||
|
|
│ ├── divination_params.dart # Request params
|
||
|
|
│ └── follow_up_message.dart # Message model
|
||
|
|
└── presentation/
|
||
|
|
└── screens/
|
||
|
|
├── divination_screen.dart # Main screen
|
||
|
|
├── auto_divination_screen.dart # Auto mode
|
||
|
|
├── manual_divination_screen.dart # Manual mode
|
||
|
|
└── follow_up_chat_screen.dart # Follow-up chat
|
||
|
|
```
|
||
|
|
|
||
|
|
### Shared Widget: `shared/widgets/app_loading_indicator.dart`
|
||
|
|
|
||
|
|
```dart
|
||
|
|
import 'package:flutter/material.dart';
|
||
|
|
|
||
|
|
class AppLoadingIndicator extends StatelessWidget {
|
||
|
|
const AppLoadingIndicator({super.key});
|
||
|
|
|
||
|
|
@override
|
||
|
|
Widget build(BuildContext context) {
|
||
|
|
return const CircularProgressIndicator();
|
||
|
|
}
|
||
|
|
}
|
||
|
|
```
|
||
|
|
|
||
|
|
### Core Infrastructure: `core/logging/logger.dart`
|
||
|
|
|
||
|
|
```dart
|
||
|
|
import 'log_service.dart';
|
||
|
|
|
||
|
|
class Logger {
|
||
|
|
final String module;
|
||
|
|
|
||
|
|
Logger(this.module, this._service);
|
||
|
|
|
||
|
|
static void setLogService(LogService service) {
|
||
|
|
_globalLogService = service;
|
||
|
|
}
|
||
|
|
|
||
|
|
void error({
|
||
|
|
required String message,
|
||
|
|
required Object error,
|
||
|
|
required StackTrace stackTrace,
|
||
|
|
}) {
|
||
|
|
_service!.error(
|
||
|
|
message: message,
|
||
|
|
error: error,
|
||
|
|
stackTrace: stackTrace,
|
||
|
|
module: module,
|
||
|
|
);
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
Logger getLogger(String module) => Logger.get(module);
|
||
|
|
```
|
||
|
|
|
||
|
|
---
|
||
|
|
|
||
|
|
## Key Principles
|
||
|
|
|
||
|
|
### Directory Contract (Must)
|
||
|
|
|
||
|
|
1. **Only allowed second-level directories**: `app/`, `core/`, `data/`, `features/`, `shared/`, `l10n/`
|
||
|
|
2. **Only one root entry**: `lib/main.dart`
|
||
|
|
3. **No ad-hoc directories**: No `utils/`, `helpers/`, `constants/` under `lib/`
|
||
|
|
4. **Feature isolation**: Features should not import each other's data layer
|
||
|
|
|
||
|
|
### Layer Boundaries
|
||
|
|
|
||
|
|
1. **Presentation** → **Data** (via Repository interface)
|
||
|
|
2. **Data** → **Core/Infrastructure** (via DI)
|
||
|
|
3. **Core** → **Nothing** (foundation layer)
|
||
|
|
4. **Shared** → **Core** (for utilities)
|
||
|
|
5. **Feature** → **Core** + **Shared** (for cross-cutting concerns)
|
||
|
|
|
||
|
|
### Data Layer Boundary (Must)
|
||
|
|
|
||
|
|
- `data/` = infrastructure abstractions (cache/network/storage)
|
||
|
|
- `features/<feature>/data/` = feature business repositories/models
|
||
|
|
- **NEVER** mix these boundaries
|