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

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 storage
  • apiClientProvider - 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 wrappers
  • xxxRepositoryProvider - 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 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:

// 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

  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: