579 lines
19 KiB
Markdown
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
|