498 lines
13 KiB
Markdown
498 lines
13 KiB
Markdown
# 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<AuthState>(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<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
|
|
|
|
```dart
|
|
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
|
|
|
|
```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<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
|
|
|
|
```dart
|
|
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:
|
|
|
|
```dart
|
|
// 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)
|
|
|
|
```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<T>()` 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.
|