fill
This commit is contained in:
578
lib/core/di/ARCHITECTURE.md
Normal file
578
lib/core/di/ARCHITECTURE.md
Normal file
@@ -0,0 +1,578 @@
|
||||
# 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
|
||||
Reference in New Issue
Block a user