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

Dependency Injection with Riverpod

This directory contains the centralized dependency injection setup for the entire application using Riverpod.

Overview

The providers.dart file sets up all Riverpod providers following Clean Architecture principles:

  • Data Layer: Data sources and repositories
  • Domain Layer: Use cases (business logic)
  • Presentation Layer: State notifiers and UI state

Architecture Pattern

UI (ConsumerWidget)
    ↓
StateNotifier (Presentation)
    ↓
UseCase (Domain)
    ↓
Repository (Domain Interface)
    ↓
RepositoryImpl (Data)
    ↓
RemoteDataSource (Data)
    ↓
ApiClient (Core)

Provider Categories

1. Core Providers (Infrastructure)

  • secureStorageProvider - Secure storage singleton
  • apiClientProvider - HTTP client with auth interceptors

2. Auth Feature Providers

  • authProvider - Main auth state (use this in UI)
  • isAuthenticatedProvider - Quick auth status check
  • currentUserProvider - Current user data
  • loginUseCaseProvider - Login business logic
  • logoutUseCaseProvider - Logout business logic

3. Warehouse Feature Providers

  • warehouseProvider - Main warehouse state
  • warehousesListProvider - List of warehouses
  • selectedWarehouseProvider - Currently selected warehouse
  • getWarehousesUseCaseProvider - Fetch warehouses logic

4. Products Feature Providers

  • productsProvider - Main products state
  • productsListProvider - List of products
  • operationTypeProvider - Import/Export type
  • getProductsUseCaseProvider - Fetch products logic

Usage Guide

Basic Setup

  1. Wrap your app with ProviderScope:
void main() {
  runApp(
    const ProviderScope(
      child: MyApp(),
    ),
  );
}
  1. Use ConsumerWidget or ConsumerStatefulWidget:
class MyPage extends ConsumerWidget {
  @override
  Widget build(BuildContext context, WidgetRef ref) {
    // Access providers here
    final isAuthenticated = ref.watch(isAuthenticatedProvider);
    return Scaffold(body: ...);
  }
}

Common Patterns

1. Watch State (UI rebuilds when state changes)

final authState = ref.watch(authProvider);
final isLoading = ref.watch(isAuthLoadingProvider);
final products = ref.watch(productsListProvider);

2. Read State (One-time read, no rebuild)

final currentUser = ref.read(currentUserProvider);

3. Call Methods on StateNotifier

// Login
ref.read(authProvider.notifier).login(username, password);

// Logout
ref.read(authProvider.notifier).logout();

// Load warehouses
ref.read(warehouseProvider.notifier).loadWarehouses();

// Select warehouse
ref.read(warehouseProvider.notifier).selectWarehouse(warehouse);

// Load products
ref.read(productsProvider.notifier).loadProducts(
  warehouseId,
  warehouseName,
  'import',
);

4. Listen to State Changes

ref.listen<AuthState>(authProvider, (previous, next) {
  if (next.isAuthenticated) {
    // Navigate to home
  }
  if (next.error != null) {
    // Show error dialog
  }
});

Feature Examples

Authentication Flow

class LoginPage extends ConsumerWidget {
  @override
  Widget build(BuildContext context, WidgetRef ref) {
    final authState = ref.watch(authProvider);

    // Listen for auth changes
    ref.listen<AuthState>(authProvider, (previous, next) {
      if (next.isAuthenticated) {
        Navigator.pushReplacementNamed(context, '/warehouses');
      }
      if (next.error != null) {
        ScaffoldMessenger.of(context).showSnackBar(
          SnackBar(content: Text(next.error!)),
        );
      }
    });

    return Scaffold(
      body: Column(
        children: [
          TextField(
            controller: usernameController,
            decoration: InputDecoration(labelText: 'Username'),
          ),
          TextField(
            controller: passwordController,
            decoration: InputDecoration(labelText: 'Password'),
            obscureText: true,
          ),
          ElevatedButton(
            onPressed: authState.isLoading
                ? null
                : () {
                    ref.read(authProvider.notifier).login(
                      usernameController.text,
                      passwordController.text,
                    );
                  },
            child: authState.isLoading
                ? CircularProgressIndicator()
                : Text('Login'),
          ),
        ],
      ),
    );
  }
}

Warehouse Selection Flow

class WarehouseSelectionPage extends ConsumerStatefulWidget {
  @override
  ConsumerState<WarehouseSelectionPage> createState() =>
      _WarehouseSelectionPageState();
}

class _WarehouseSelectionPageState
    extends ConsumerState<WarehouseSelectionPage> {
  @override
  void initState() {
    super.initState();
    // Load warehouses when page opens
    Future.microtask(() {
      ref.read(warehouseProvider.notifier).loadWarehouses();
    });
  }

  @override
  Widget build(BuildContext context) {
    final warehouses = ref.watch(warehousesListProvider);
    final isLoading = ref.watch(isWarehouseLoadingProvider);
    final error = ref.watch(warehouseErrorProvider);

    return Scaffold(
      appBar: AppBar(title: Text('Select Warehouse')),
      body: isLoading
          ? Center(child: CircularProgressIndicator())
          : error != null
              ? Center(child: Text(error))
              : ListView.builder(
                  itemCount: warehouses.length,
                  itemBuilder: (context, index) {
                    final warehouse = warehouses[index];
                    return ListTile(
                      title: Text(warehouse.name),
                      subtitle: Text(warehouse.code),
                      trailing: Text('${warehouse.totalCount} items'),
                      onTap: () {
                        // Select warehouse
                        ref.read(warehouseProvider.notifier)
                            .selectWarehouse(warehouse);

                        // Navigate to operation selection
                        Navigator.pushNamed(
                          context,
                          '/operations',
                          arguments: warehouse,
                        );
                      },
                    );
                  },
                ),
    );
  }
}

Products List Flow

class ProductsPage extends ConsumerStatefulWidget {
  final int warehouseId;
  final String warehouseName;
  final String operationType; // 'import' or 'export'

  const ProductsPage({
    required this.warehouseId,
    required this.warehouseName,
    required this.operationType,
  });

  @override
  ConsumerState<ProductsPage> createState() => _ProductsPageState();
}

class _ProductsPageState extends ConsumerState<ProductsPage> {
  @override
  void initState() {
    super.initState();
    // Load products when page opens
    Future.microtask(() {
      ref.read(productsProvider.notifier).loadProducts(
        widget.warehouseId,
        widget.warehouseName,
        widget.operationType,
      );
    });
  }

  @override
  Widget build(BuildContext context) {
    final products = ref.watch(productsListProvider);
    final isLoading = ref.watch(isProductsLoadingProvider);
    final error = ref.watch(productsErrorProvider);

    return Scaffold(
      appBar: AppBar(
        title: Text('${widget.warehouseName} - ${widget.operationType}'),
        actions: [
          IconButton(
            icon: Icon(Icons.refresh),
            onPressed: () {
              ref.read(productsProvider.notifier).refreshProducts();
            },
          ),
        ],
      ),
      body: isLoading
          ? Center(child: CircularProgressIndicator())
          : error != null
              ? Center(child: Text(error))
              : ListView.builder(
                  itemCount: products.length,
                  itemBuilder: (context, index) {
                    final product = products[index];
                    return ProductListItem(product: product);
                  },
                ),
    );
  }
}

Check Auth Status on App Start

class MyApp extends ConsumerStatefulWidget {
  @override
  ConsumerState<MyApp> createState() => _MyAppState();
}

class _MyAppState extends ConsumerState<MyApp> {
  @override
  void initState() {
    super.initState();
    // Check if user is already authenticated
    Future.microtask(() {
      ref.read(authProvider.notifier).checkAuthStatus();
    });
  }

  @override
  Widget build(BuildContext context) {
    final isAuthenticated = ref.watch(isAuthenticatedProvider);

    return MaterialApp(
      home: isAuthenticated
          ? WarehouseSelectionPage()
          : LoginPage(),
    );
  }
}

Advanced Usage

Custom Providers

Create custom computed providers for complex logic:

// Get warehouses filtered by type
final ngWarehousesProvider = Provider<List<WarehouseEntity>>((ref) {
  final warehouses = ref.watch(warehousesListProvider);
  return warehouses.where((w) => w.isNGWareHouse).toList();
});

// Get products count per operation type
final importProductsCountProvider = Provider<int>((ref) {
  final products = ref.watch(productsListProvider);
  final operationType = ref.watch(operationTypeProvider);
  return operationType == 'import' ? products.length : 0;
});

Override Providers (for testing)

testWidgets('Login page test', (tester) async {
  await tester.pumpWidget(
    ProviderScope(
      overrides: [
        // Override with mock
        authRepositoryProvider.overrideWithValue(mockAuthRepository),
      ],
      child: LoginPage(),
    ),
  );
});

Best Practices

  1. Use ConsumerWidget: Always use ConsumerWidget or ConsumerStatefulWidget to access providers.

  2. Watch in build(): Only watch providers in the build() method for reactive updates.

  3. Read for actions: Use ref.read() for one-time reads or calling methods.

  4. Listen for side effects: Use ref.listen() for navigation, dialogs, snackbars.

  5. Avoid over-watching: Don't watch entire state if you only need one field - use derived providers.

  6. Keep providers pure: Don't perform side effects in provider definitions.

  7. Dispose properly: StateNotifier automatically disposes, but be careful with custom providers.

Debugging

Enable Logging

void main() {
  runApp(
    ProviderScope(
      observers: [ProviderLogger()],
      child: MyApp(),
    ),
  );
}

class ProviderLogger extends ProviderObserver {
  @override
  void didUpdateProvider(
    ProviderBase provider,
    Object? previousValue,
    Object? newValue,
    ProviderContainer container,
  ) {
    print('''
{
  "provider": "${provider.name ?? provider.runtimeType}",
  "newValue": "$newValue"
}''');
  }
}

Common Issues

  1. Provider not found: Make sure ProviderScope wraps your app.

  2. State not updating: Use ref.watch() instead of ref.read() in build method.

  3. Circular dependency: Check provider dependencies - avoid circular references.

  4. Memory leaks: Use autoDispose modifier for providers that should be disposed.

Testing

Unit Testing Providers

test('Auth provider login success', () async {
  final container = ProviderContainer(
    overrides: [
      authRepositoryProvider.overrideWithValue(mockAuthRepository),
    ],
  );

  when(mockAuthRepository.login(any))
      .thenAnswer((_) async => Right(mockUser));

  await container.read(authProvider.notifier).login('user', 'pass');

  expect(container.read(isAuthenticatedProvider), true);
  expect(container.read(currentUserProvider), mockUser);

  container.dispose();
});

Widget Testing

testWidgets('Login button triggers login', (tester) async {
  await tester.pumpWidget(
    ProviderScope(
      overrides: [
        authRepositoryProvider.overrideWithValue(mockAuthRepository),
      ],
      child: MaterialApp(home: LoginPage()),
    ),
  );

  await tester.enterText(find.byKey(Key('username')), 'testuser');
  await tester.enterText(find.byKey(Key('password')), 'password');
  await tester.tap(find.byKey(Key('loginButton')));
  await tester.pump();

  verify(mockAuthRepository.login(any)).called(1);
});

Migration Guide

If you were using GetIt or other DI solutions:

  1. Replace GetIt registration with Riverpod providers
  2. Change GetIt.instance.get<T>() to ref.watch(provider)
  3. Use ConsumerWidget instead of regular StatelessWidget
  4. Move initialization logic to initState() or provider initialization

Resources

Support

For questions or issues with DI setup, contact the development team or refer to the project documentation.