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

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.