19 KiB
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 storageapiClientProvider- HTTP client with auth interceptors
Characteristics:
- Singleton instances
- No business logic
- Pure infrastructure
- Used by all features
Example:
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 wrappersxxxRepositoryProvider- Repository implementations
Characteristics:
- Depends on Core layer
- Implements Domain interfaces
- Handles data transformation
- Manages errors (exceptions → failures)
Example:
// 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:
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 managementisXxxLoadingProvider- Loading statexxxErrorProvider- Error statexxxListProvider- Data lists
Characteristics:
- Depends on Domain layer
- Manages UI state
- Handles user actions
- Notifies UI of changes
- Can depend on multiple use cases
Example:
// 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
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
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
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
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
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
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
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():
// ❌ 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
// ❌ 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
final temporaryProvider = Provider.autoDispose<MyService>((ref) {
final service = MyService();
ref.onDispose(() {
service.dispose();
});
return service;
});
Common Patterns
Pattern: Feature Initialization
@override
void initState() {
super.initState();
Future.microtask(() {
ref.read(warehouseProvider.notifier).loadWarehouses();
});
}
Pattern: Conditional Navigation
ref.listen<AuthState>(authProvider, (previous, next) {
if (next.isAuthenticated && !(previous?.isAuthenticated ?? false)) {
Navigator.pushReplacementNamed(context, '/home');
}
});
Pattern: Error Handling
ref.listen<String?>(authErrorProvider, (previous, next) {
if (next != null) {
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(content: Text(next)),
);
}
});
Pattern: Pull-to-Refresh
RefreshIndicator(
onRefresh: () async {
await ref.read(warehouseProvider.notifier).refresh();
},
child: ListView(...),
)
Dependency Injection Benefits
- Testability: Easy to mock dependencies
- Maintainability: Clear dependency tree
- Scalability: Add features without touching existing code
- Flexibility: Swap implementations easily
- Readability: Explicit dependencies
- Type Safety: Compile-time checks
- Hot Reload: Works seamlessly with Flutter
- 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 - Comprehensive guide
- QUICK_REFERENCE.md - Quick lookup