Files
minhthu/lib/core/di/ARCHITECTURE.md
2025-10-28 00:09:46 +07:00

579 lines
19 KiB
Markdown

# Dependency Injection Architecture
## Provider Dependency Graph
```
┌─────────────────────────────────────────────────────────────────────┐
│ PRESENTATION LAYER │
│ (UI State Management) │
└─────────────────────────────────────────────────────────────────────┘
│ depends on
┌─────────────────────────────────────────────────────────────────────┐
│ DOMAIN LAYER │
│ (Business Logic) │
└─────────────────────────────────────────────────────────────────────┘
│ depends on
┌─────────────────────────────────────────────────────────────────────┐
│ DATA LAYER │
│ (Repositories & Data Sources) │
└─────────────────────────────────────────────────────────────────────┘
│ depends on
┌─────────────────────────────────────────────────────────────────────┐
│ CORE LAYER │
│ (Infrastructure Services) │
└─────────────────────────────────────────────────────────────────────┘
```
## Complete Provider Dependency Tree
### Authentication Feature
```
secureStorageProvider (Core - Singleton)
├──> apiClientProvider (Core - Singleton)
│ │
│ └──> authRemoteDataSourceProvider
│ │
└─────────────────────────────┴──> authRepositoryProvider
┌──────────────────────────────┼────────────────────────────┐
│ │ │
loginUseCaseProvider logoutUseCaseProvider checkAuthStatusUseCaseProvider
│ │ │
└──────────────────────────────┴────────────────────────────┘
authProvider (StateNotifier)
┌──────────────────────────────┼────────────────────────────┐
│ │ │
isAuthenticatedProvider currentUserProvider authErrorProvider
```
### Warehouse Feature
```
apiClientProvider (Core - Singleton)
└──> warehouseRemoteDataSourceProvider
└──> warehouseRepositoryProvider
└──> getWarehousesUseCaseProvider
└──> warehouseProvider (StateNotifier)
┌──────────────────────────────────────────┼────────────────────────────────┐
│ │ │
warehousesListProvider selectedWarehouseProvider isWarehouseLoadingProvider
```
### Products Feature
```
apiClientProvider (Core - Singleton)
└──> productsRemoteDataSourceProvider
└──> productsRepositoryProvider
└──> getProductsUseCaseProvider
└──> productsProvider (StateNotifier)
┌──────────────────────────────────────────┼────────────────────────────────┐
│ │ │
productsListProvider operationTypeProvider isProductsLoadingProvider
```
## Layer-by-Layer Architecture
### 1. Core Layer (Infrastructure)
**Purpose**: Provide foundational services that all features depend on
**Providers**:
- `secureStorageProvider` - Manages encrypted storage
- `apiClientProvider` - HTTP client with auth interceptors
**Characteristics**:
- Singleton instances
- No business logic
- Pure infrastructure
- Used by all features
**Example**:
```dart
final secureStorageProvider = Provider<SecureStorage>((ref) {
return SecureStorage();
});
final apiClientProvider = Provider<ApiClient>((ref) {
final secureStorage = ref.watch(secureStorageProvider);
return ApiClient(secureStorage);
});
```
### 2. Data Layer
**Purpose**: Handle data operations - API calls, local storage, caching
**Components**:
- **Remote Data Sources**: Make API calls
- **Repositories**: Coordinate data sources, convert models to entities
**Providers**:
- `xxxRemoteDataSourceProvider` - API client wrappers
- `xxxRepositoryProvider` - Repository implementations
**Characteristics**:
- Depends on Core layer
- Implements Domain interfaces
- Handles data transformation
- Manages errors (exceptions → failures)
**Example**:
```dart
// Data Source
final authRemoteDataSourceProvider = Provider<AuthRemoteDataSource>((ref) {
final apiClient = ref.watch(apiClientProvider);
return AuthRemoteDataSourceImpl(apiClient);
});
// Repository
final authRepositoryProvider = Provider<AuthRepository>((ref) {
final remoteDataSource = ref.watch(authRemoteDataSourceProvider);
final secureStorage = ref.watch(secureStorageProvider);
return AuthRepositoryImpl(
remoteDataSource: remoteDataSource,
secureStorage: secureStorage,
);
});
```
### 3. Domain Layer (Business Logic)
**Purpose**: Encapsulate business rules and use cases
**Components**:
- **Entities**: Pure business objects
- **Repository Interfaces**: Define data contracts
- **Use Cases**: Single-purpose business operations
**Providers**:
- `xxxUseCaseProvider` - Business logic encapsulation
**Characteristics**:
- No external dependencies (pure Dart)
- Depends only on abstractions
- Contains business rules
- Reusable across features
- Testable in isolation
**Example**:
```dart
final loginUseCaseProvider = Provider<LoginUseCase>((ref) {
final repository = ref.watch(authRepositoryProvider);
return LoginUseCase(repository);
});
final getWarehousesUseCaseProvider = Provider<GetWarehousesUseCase>((ref) {
final repository = ref.watch(warehouseRepositoryProvider);
return GetWarehousesUseCase(repository);
});
```
### 4. Presentation Layer (UI State)
**Purpose**: Manage UI state and handle user interactions
**Components**:
- **State Classes**: Immutable state containers
- **State Notifiers**: Mutable state managers
- **Derived Providers**: Computed state values
**Providers**:
- `xxxProvider` (StateNotifier) - Main state management
- `isXxxLoadingProvider` - Loading state
- `xxxErrorProvider` - Error state
- `xxxListProvider` - Data lists
**Characteristics**:
- Depends on Domain layer
- Manages UI state
- Handles user actions
- Notifies UI of changes
- Can depend on multiple use cases
**Example**:
```dart
// Main state notifier
final authProvider = StateNotifierProvider<AuthNotifier, AuthState>((ref) {
final loginUseCase = ref.watch(loginUseCaseProvider);
final logoutUseCase = ref.watch(logoutUseCaseProvider);
final checkAuthStatusUseCase = ref.watch(checkAuthStatusUseCaseProvider);
final getCurrentUserUseCase = ref.watch(getCurrentUserUseCaseProvider);
return AuthNotifier(
loginUseCase: loginUseCase,
logoutUseCase: logoutUseCase,
checkAuthStatusUseCase: checkAuthStatusUseCase,
getCurrentUserUseCase: getCurrentUserUseCase,
);
});
// Derived providers
final isAuthenticatedProvider = Provider<bool>((ref) {
final authState = ref.watch(authProvider);
return authState.isAuthenticated;
});
```
## Data Flow Patterns
### 1. User Action Flow (Write Operation)
```
┌──────────────┐
│ UI Widget │
└──────┬───────┘
│ User taps button
┌──────────────────────┐
│ ref.read(provider │
│ .notifier) │
│ .someMethod() │
└──────┬───────────────┘
┌──────────────────────┐
│ StateNotifier │
│ - Set loading state │
└──────┬───────────────┘
┌──────────────────────┐
│ Use Case │
│ - Validate input │
│ - Business logic │
└──────┬───────────────┘
┌──────────────────────┐
│ Repository │
│ - Coordinate data │
│ - Error handling │
└──────┬───────────────┘
┌──────────────────────┐
│ Data Source │
│ - API call │
│ - Parse response │
└──────┬───────────────┘
┌──────────────────────┐
│ API Client │
│ - HTTP request │
│ - Add auth token │
└──────┬───────────────┘
│ Response ←─────
┌──────────────────────┐
│ StateNotifier │
│ - Update state │
│ - Notify listeners │
└──────┬───────────────┘
┌──────────────────────┐
│ UI Widget │
│ - Rebuild with │
│ new state │
└──────────────────────┘
```
### 2. State Observation Flow (Read Operation)
```
┌──────────────────────┐
│ UI Widget │
│ ref.watch(provider) │
└──────┬───────────────┘
│ Subscribes to
┌──────────────────────┐
│ StateNotifier │
│ Current State │
└──────┬───────────────┘
│ State changes
┌──────────────────────┐
│ UI Widget │
│ Automatically │
│ rebuilds │
└──────────────────────┘
```
## Provider Types Usage
### Provider (Immutable Services)
**Use for**: Services, repositories, use cases, utilities
```dart
final myServiceProvider = Provider<MyService>((ref) {
final dependency = ref.watch(dependencyProvider);
return MyService(dependency);
});
```
**Lifecycle**: Created once, lives forever (unless autoDispose)
### StateNotifierProvider (Mutable State)
**Use for**: Managing feature state that changes over time
```dart
final myStateProvider = StateNotifierProvider<MyNotifier, MyState>((ref) {
final useCase = ref.watch(useCaseProvider);
return MyNotifier(useCase);
});
```
**Lifecycle**: Created on first access, disposed when no longer used
### Derived Providers (Computed Values)
**Use for**: Computed values from other providers
```dart
final derivedProvider = Provider<DerivedData>((ref) {
final state = ref.watch(stateProvider);
return computeValue(state);
});
```
**Lifecycle**: Recomputed when dependencies change
## Best Practices by Layer
### Core Layer
✅ Keep providers pure and stateless
✅ Use singleton pattern
✅ No business logic
❌ Don't depend on feature providers
❌ Don't manage mutable state
### Data Layer
✅ Implement domain interfaces
✅ Convert models ↔ entities
✅ Handle all exceptions
✅ Use Either<Failure, T> return type
❌ Don't expose models to domain
❌ Don't contain business logic
### Domain Layer
✅ Pure Dart (no Flutter dependencies)
✅ Single responsibility per use case
✅ Validate input
✅ Return Either<Failure, T>
❌ Don't know about UI
❌ Don't know about data sources
### Presentation Layer
✅ Manage UI-specific state
✅ Call multiple use cases if needed
✅ Transform data for display
✅ Handle navigation logic
❌ Don't access data sources directly
❌ Don't perform business logic
## Testing Strategy
### Unit Testing
**Core Layer**: Test utilities and services
```dart
test('SecureStorage saves token', () async {
final storage = SecureStorage();
await storage.saveAccessToken('token');
expect(await storage.getAccessToken(), 'token');
});
```
**Domain Layer**: Test use cases with mock repositories
```dart
test('LoginUseCase returns user on success', () async {
final mockRepo = MockAuthRepository();
when(mockRepo.login(any)).thenAnswer((_) async => Right(mockUser));
final useCase = LoginUseCase(mockRepo);
final result = await useCase(loginRequest);
expect(result.isRight(), true);
});
```
**Presentation Layer**: Test state notifiers
```dart
test('AuthNotifier sets authenticated on login', () async {
final container = ProviderContainer(
overrides: [
loginUseCaseProvider.overrideWithValue(mockLoginUseCase),
],
);
await container.read(authProvider.notifier).login('user', 'pass');
expect(container.read(isAuthenticatedProvider), true);
});
```
### Widget Testing
```dart
testWidgets('Login page shows error on failure', (tester) async {
await tester.pumpWidget(
ProviderScope(
overrides: [
authRepositoryProvider.overrideWithValue(mockAuthRepository),
],
child: MaterialApp(home: LoginPage()),
),
);
// Interact with widget
await tester.tap(find.text('Login'));
await tester.pump();
// Verify error is shown
expect(find.text('Invalid credentials'), findsOneWidget);
});
```
## Performance Optimization
### 1. Use Derived Providers
Instead of computing in build():
```dart
// ❌ Bad - computes every rebuild
@override
Widget build(BuildContext context, WidgetRef ref) {
final warehouses = ref.watch(warehousesListProvider);
final ngWarehouses = warehouses.where((w) => w.isNGWareHouse).toList();
return ListView(...);
}
// ✅ Good - computed once per state change
final ngWarehousesProvider = Provider<List<WarehouseEntity>>((ref) {
final warehouses = ref.watch(warehousesListProvider);
return warehouses.where((w) => w.isNGWareHouse).toList();
});
@override
Widget build(BuildContext context, WidgetRef ref) {
final ngWarehouses = ref.watch(ngWarehousesProvider);
return ListView(...);
}
```
### 2. Use select() for Partial State
```dart
// ❌ Bad - rebuilds on any state change
final authState = ref.watch(authProvider);
final isLoading = authState.isLoading;
// ✅ Good - rebuilds only when isLoading changes
final isLoading = ref.watch(authProvider.select((s) => s.isLoading));
```
### 3. Use autoDispose for Temporary Providers
```dart
final temporaryProvider = Provider.autoDispose<MyService>((ref) {
final service = MyService();
ref.onDispose(() {
service.dispose();
});
return service;
});
```
## Common Patterns
### Pattern: Feature Initialization
```dart
@override
void initState() {
super.initState();
Future.microtask(() {
ref.read(warehouseProvider.notifier).loadWarehouses();
});
}
```
### Pattern: Conditional Navigation
```dart
ref.listen<AuthState>(authProvider, (previous, next) {
if (next.isAuthenticated && !(previous?.isAuthenticated ?? false)) {
Navigator.pushReplacementNamed(context, '/home');
}
});
```
### Pattern: Error Handling
```dart
ref.listen<String?>(authErrorProvider, (previous, next) {
if (next != null) {
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(content: Text(next)),
);
}
});
```
### Pattern: Pull-to-Refresh
```dart
RefreshIndicator(
onRefresh: () async {
await ref.read(warehouseProvider.notifier).refresh();
},
child: ListView(...),
)
```
## Dependency Injection Benefits
1. **Testability**: Easy to mock dependencies
2. **Maintainability**: Clear dependency tree
3. **Scalability**: Add features without touching existing code
4. **Flexibility**: Swap implementations easily
5. **Readability**: Explicit dependencies
6. **Type Safety**: Compile-time checks
7. **Hot Reload**: Works seamlessly with Flutter
8. **DevTools**: Inspect state in real-time
## Summary
This DI architecture provides:
- Clear separation of concerns
- Predictable data flow
- Easy testing at all levels
- Type-safe dependency injection
- Reactive state management
- Scalable feature structure
For more details, see:
- [README.md](./README.md) - Comprehensive guide
- [QUICK_REFERENCE.md](./QUICK_REFERENCE.md) - Quick lookup