# 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**: ```dart void main() { runApp( const ProviderScope( child: MyApp(), ), ); } ``` 2. **Use ConsumerWidget or ConsumerStatefulWidget**: ```dart 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) ```dart final authState = ref.watch(authProvider); final isLoading = ref.watch(isAuthLoadingProvider); final products = ref.watch(productsListProvider); ``` #### 2. Read State (One-time read, no rebuild) ```dart final currentUser = ref.read(currentUserProvider); ``` #### 3. Call Methods on StateNotifier ```dart // 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 ```dart ref.listen(authProvider, (previous, next) { if (next.isAuthenticated) { // Navigate to home } if (next.error != null) { // Show error dialog } }); ``` ## Feature Examples ### Authentication Flow ```dart class LoginPage extends ConsumerWidget { @override Widget build(BuildContext context, WidgetRef ref) { final authState = ref.watch(authProvider); // Listen for auth changes ref.listen(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 ```dart class WarehouseSelectionPage extends ConsumerStatefulWidget { @override ConsumerState createState() => _WarehouseSelectionPageState(); } class _WarehouseSelectionPageState extends ConsumerState { @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 ```dart 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 createState() => _ProductsPageState(); } class _ProductsPageState extends ConsumerState { @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 ```dart class MyApp extends ConsumerStatefulWidget { @override ConsumerState createState() => _MyAppState(); } class _MyAppState extends ConsumerState { @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: ```dart // Get warehouses filtered by type final ngWarehousesProvider = Provider>((ref) { final warehouses = ref.watch(warehousesListProvider); return warehouses.where((w) => w.isNGWareHouse).toList(); }); // Get products count per operation type final importProductsCountProvider = Provider((ref) { final products = ref.watch(productsListProvider); final operationType = ref.watch(operationTypeProvider); return operationType == 'import' ? products.length : 0; }); ``` ### Override Providers (for testing) ```dart 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 ```dart 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 ```dart 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 ```dart 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()` to `ref.watch(provider)` 3. Use `ConsumerWidget` instead of regular `StatelessWidget` 4. Move initialization logic to `initState()` or provider initialization ## Resources - [Riverpod Documentation](https://riverpod.dev) - [Clean Architecture Guide](https://blog.cleancoder.com/uncle-bob/2012/08/13/the-clean-architecture.html) - [Flutter State Management](https://docs.flutter.dev/development/data-and-backend/state-mgmt/options) ## Support For questions or issues with DI setup, contact the development team or refer to the project documentation.