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 singletonapiClientProvider- HTTP client with auth interceptors
2. Auth Feature Providers
authProvider- Main auth state (use this in UI)isAuthenticatedProvider- Quick auth status checkcurrentUserProvider- Current user dataloginUseCaseProvider- Login business logiclogoutUseCaseProvider- Logout business logic
3. Warehouse Feature Providers
warehouseProvider- Main warehouse statewarehousesListProvider- List of warehousesselectedWarehouseProvider- Currently selected warehousegetWarehousesUseCaseProvider- Fetch warehouses logic
4. Products Feature Providers
productsProvider- Main products stateproductsListProvider- List of productsoperationTypeProvider- Import/Export typegetProductsUseCaseProvider- Fetch products logic
Usage Guide
Basic Setup
- Wrap your app with ProviderScope:
void main() {
runApp(
const ProviderScope(
child: MyApp(),
),
);
}
- 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
-
Use ConsumerWidget: Always use
ConsumerWidgetorConsumerStatefulWidgetto access providers. -
Watch in build(): Only watch providers in the
build()method for reactive updates. -
Read for actions: Use
ref.read()for one-time reads or calling methods. -
Listen for side effects: Use
ref.listen()for navigation, dialogs, snackbars. -
Avoid over-watching: Don't watch entire state if you only need one field - use derived providers.
-
Keep providers pure: Don't perform side effects in provider definitions.
-
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
-
Provider not found: Make sure
ProviderScopewraps your app. -
State not updating: Use
ref.watch()instead ofref.read()in build method. -
Circular dependency: Check provider dependencies - avoid circular references.
-
Memory leaks: Use
autoDisposemodifier 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:
- Replace GetIt registration with Riverpod providers
- Change
GetIt.instance.get<T>()toref.watch(provider) - Use
ConsumerWidgetinstead of regularStatelessWidget - 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.