fill
This commit is contained in:
497
lib/core/di/README.md
Normal file
497
lib/core/di/README.md
Normal file
@@ -0,0 +1,497 @@
|
||||
# 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.
|
||||
Reference in New Issue
Block a user