This commit is contained in:
2025-10-28 00:09:46 +07:00
parent 9ebe7c2919
commit de49f564b1
110 changed files with 15392 additions and 3996 deletions

View File

@@ -0,0 +1,569 @@
# Migration Guide to Riverpod DI
This guide helps you migrate from other dependency injection solutions (GetIt, Provider, etc.) to Riverpod.
## From GetIt to Riverpod
### Before (GetIt)
```dart
// Setup
final getIt = GetIt.instance;
void setupDI() {
// Core
getIt.registerLazySingleton<SecureStorage>(() => SecureStorage());
getIt.registerLazySingleton<ApiClient>(() => ApiClient(getIt()));
// Auth
getIt.registerLazySingleton<AuthRemoteDataSource>(
() => AuthRemoteDataSourceImpl(getIt()),
);
getIt.registerLazySingleton<AuthRepository>(
() => AuthRepositoryImpl(
remoteDataSource: getIt(),
secureStorage: getIt(),
),
);
getIt.registerLazySingleton<LoginUseCase>(
() => LoginUseCase(getIt()),
);
}
// Usage in widget
class MyWidget extends StatelessWidget {
@override
Widget build(BuildContext context) {
final authRepo = getIt<AuthRepository>();
final loginUseCase = getIt<LoginUseCase>();
return Container();
}
}
```
### After (Riverpod)
```dart
// Setup (in lib/core/di/providers.dart)
final secureStorageProvider = Provider<SecureStorage>((ref) {
return SecureStorage();
});
final apiClientProvider = Provider<ApiClient>((ref) {
final secureStorage = ref.watch(secureStorageProvider);
return ApiClient(secureStorage);
});
final authRemoteDataSourceProvider = Provider<AuthRemoteDataSource>((ref) {
final apiClient = ref.watch(apiClientProvider);
return AuthRemoteDataSourceImpl(apiClient);
});
final authRepositoryProvider = Provider<AuthRepository>((ref) {
final remoteDataSource = ref.watch(authRemoteDataSourceProvider);
final secureStorage = ref.watch(secureStorageProvider);
return AuthRepositoryImpl(
remoteDataSource: remoteDataSource,
secureStorage: secureStorage,
);
});
final loginUseCaseProvider = Provider<LoginUseCase>((ref) {
final repository = ref.watch(authRepositoryProvider);
return LoginUseCase(repository);
});
// Usage in widget
class MyWidget extends ConsumerWidget {
@override
Widget build(BuildContext context, WidgetRef ref) {
final authRepo = ref.watch(authRepositoryProvider);
final loginUseCase = ref.watch(loginUseCaseProvider);
return Container();
}
}
```
## Key Differences
| Aspect | GetIt | Riverpod |
|--------|-------|----------|
| Setup | Manual registration in setup function | Declarative provider definitions |
| Access | `getIt<Type>()` anywhere | `ref.watch(provider)` in widgets |
| Widget Base | `StatelessWidget` / `StatefulWidget` | `ConsumerWidget` / `ConsumerStatefulWidget` |
| Dependencies | Manual injection | Automatic via `ref.watch()` |
| Lifecycle | Manual disposal | Automatic disposal |
| Testing | Override with `getIt.registerFactory()` | Override with `ProviderScope` |
| Type Safety | Runtime errors if not registered | Compile-time errors |
| Reactivity | Manual with ChangeNotifier | Built-in with StateNotifier |
## Migration Steps
### Step 1: Wrap App with ProviderScope
```dart
// Before
void main() {
setupDI();
runApp(MyApp());
}
// After
void main() {
runApp(
const ProviderScope(
child: MyApp(),
),
);
}
```
### Step 2: Convert Widgets to ConsumerWidget
```dart
// Before
class MyPage extends StatelessWidget {
@override
Widget build(BuildContext context) {
return Container();
}
}
// After
class MyPage extends ConsumerWidget {
@override
Widget build(BuildContext context, WidgetRef ref) {
return Container();
}
}
```
### Step 3: Replace GetIt Calls
```dart
// Before
final useCase = getIt<LoginUseCase>();
final result = await useCase(request);
// After
final useCase = ref.watch(loginUseCaseProvider);
final result = await useCase(request);
```
### Step 4: Convert State Management
```dart
// Before (ChangeNotifier + Provider)
class AuthNotifier extends ChangeNotifier {
bool _isAuthenticated = false;
bool get isAuthenticated => _isAuthenticated;
void login() {
_isAuthenticated = true;
notifyListeners();
}
}
// Register
getIt.registerLazySingleton(() => AuthNotifier());
// Usage
final authNotifier = getIt<AuthNotifier>();
authNotifier.addListener(() {
// Handle change
});
// After (StateNotifier + Riverpod)
class AuthNotifier extends StateNotifier<AuthState> {
AuthNotifier() : super(AuthState.initial());
void login() {
state = state.copyWith(isAuthenticated: true);
}
}
// Provider
final authProvider = StateNotifierProvider<AuthNotifier, AuthState>((ref) {
return AuthNotifier();
});
// Usage
final authState = ref.watch(authProvider);
// Widget automatically rebuilds on state change
```
## Common Patterns Migration
### Pattern 1: Singleton Service
```dart
// Before (GetIt)
getIt.registerLazySingleton<MyService>(() => MyService());
// After (Riverpod)
final myServiceProvider = Provider<MyService>((ref) {
return MyService();
});
```
### Pattern 2: Factory (New Instance Each Time)
```dart
// Before (GetIt)
getIt.registerFactory<MyService>(() => MyService());
// After (Riverpod)
final myServiceProvider = Provider.autoDispose<MyService>((ref) {
return MyService();
});
```
### Pattern 3: Async Initialization
```dart
// Before (GetIt)
final myServiceFuture = getIt.getAsync<MyService>();
// After (Riverpod)
final myServiceProvider = FutureProvider<MyService>((ref) async {
final service = MyService();
await service.initialize();
return service;
});
```
### Pattern 4: Conditional Registration
```dart
// Before (GetIt)
if (isProduction) {
getIt.registerLazySingleton<ApiClient>(
() => ProductionApiClient(),
);
} else {
getIt.registerLazySingleton<ApiClient>(
() => MockApiClient(),
);
}
// After (Riverpod)
final apiClientProvider = Provider<ApiClient>((ref) {
if (isProduction) {
return ProductionApiClient();
} else {
return MockApiClient();
}
});
```
## Testing Migration
### Before (GetIt)
```dart
void main() {
setUp(() {
// Clear and re-register
getIt.reset();
getIt.registerLazySingleton<AuthRepository>(
() => MockAuthRepository(),
);
});
test('test case', () {
final repo = getIt<AuthRepository>();
// Test
});
}
```
### After (Riverpod)
```dart
void main() {
test('test case', () {
final container = ProviderContainer(
overrides: [
authRepositoryProvider.overrideWithValue(mockAuthRepository),
],
);
final repo = container.read(authRepositoryProvider);
// Test
container.dispose();
});
}
```
## Widget Testing Migration
### Before (GetIt + Provider)
```dart
testWidgets('widget test', (tester) async {
// Setup mocks
getIt.reset();
getIt.registerLazySingleton<AuthRepository>(
() => mockAuthRepository,
);
await tester.pumpWidget(
ChangeNotifierProvider(
create: (_) => AuthNotifier(),
child: MaterialApp(home: LoginPage()),
),
);
// Test
});
```
### After (Riverpod)
```dart
testWidgets('widget test', (tester) async {
await tester.pumpWidget(
ProviderScope(
overrides: [
authRepositoryProvider.overrideWithValue(mockAuthRepository),
],
child: MaterialApp(home: LoginPage()),
),
);
// Test
});
```
## State Management Migration
### From ChangeNotifier to StateNotifier
```dart
// Before
class CounterNotifier extends ChangeNotifier {
int _count = 0;
int get count => _count;
void increment() {
_count++;
notifyListeners();
}
}
// Usage
final counter = context.watch<CounterNotifier>();
Text('${counter.count}');
// After
class CounterNotifier extends StateNotifier<int> {
CounterNotifier() : super(0);
void increment() {
state = state + 1;
}
}
final counterProvider = StateNotifierProvider<CounterNotifier, int>((ref) {
return CounterNotifier();
});
// Usage
final count = ref.watch(counterProvider);
Text('$count');
```
## Benefits of Migration
### 1. Type Safety
```dart
// GetIt - Runtime error if not registered
final service = getIt<MyService>(); // May crash at runtime
// Riverpod - Compile-time error
final service = ref.watch(myServiceProvider); // Compile-time check
```
### 2. Automatic Disposal
```dart
// GetIt - Manual disposal
class MyWidget extends StatefulWidget {
@override
void dispose() {
getIt<MyService>().dispose();
super.dispose();
}
}
// Riverpod - Automatic
final myServiceProvider = Provider.autoDispose<MyService>((ref) {
final service = MyService();
ref.onDispose(() => service.dispose());
return service;
});
```
### 3. Easy Testing
```dart
// GetIt - Need to reset and re-register
setUp(() {
getIt.reset();
getIt.registerLazySingleton<MyService>(() => MockMyService());
});
// Riverpod - Simple override
final container = ProviderContainer(
overrides: [
myServiceProvider.overrideWithValue(mockMyService),
],
);
```
### 4. Better Developer Experience
- No need to remember to register dependencies
- No need to call setup function
- Auto-completion works better
- Compile-time safety
- Built-in DevTools support
## Common Pitfalls
### Pitfall 1: Using ref.watch() in callbacks
```dart
// ❌ Wrong
ElevatedButton(
onPressed: () {
final user = ref.watch(currentUserProvider); // Error!
print(user);
},
child: Text('Print User'),
)
// ✅ Correct
ElevatedButton(
onPressed: () {
final user = ref.read(currentUserProvider);
print(user);
},
child: Text('Print User'),
)
```
### Pitfall 2: Not using ConsumerWidget
```dart
// ❌ Wrong - StatelessWidget doesn't have ref
class MyPage extends StatelessWidget {
@override
Widget build(BuildContext context) {
final data = ref.watch(dataProvider); // Error: ref not available
return Container();
}
}
// ✅ Correct - Use ConsumerWidget
class MyPage extends ConsumerWidget {
@override
Widget build(BuildContext context, WidgetRef ref) {
final data = ref.watch(dataProvider);
return Container();
}
}
```
### Pitfall 3: Calling methods in build()
```dart
// ❌ Wrong - Causes infinite loop
@override
Widget build(BuildContext context, WidgetRef ref) {
ref.read(authProvider.notifier).checkAuthStatus(); // Infinite loop!
return Container();
}
// ✅ Correct - Call in initState
@override
void initState() {
super.initState();
Future.microtask(() {
ref.read(authProvider.notifier).checkAuthStatus();
});
}
```
### Pitfall 4: Not disposing ProviderContainer in tests
```dart
// ❌ Wrong - Memory leak
test('test case', () {
final container = ProviderContainer();
// Test
});
// ✅ Correct - Always dispose
test('test case', () {
final container = ProviderContainer();
addTearDown(container.dispose);
// Test
});
```
## Incremental Migration Strategy
You can migrate gradually:
1. **Phase 1: Add Riverpod**
- Add dependency
- Wrap app with ProviderScope
- Keep GetIt for now
2. **Phase 2: Migrate Core**
- Create core providers
- Migrate one feature at a time
- Both systems can coexist
3. **Phase 3: Migrate Features**
- Start with simplest feature
- Test thoroughly
- Move to next feature
4. **Phase 4: Remove GetIt**
- Once all migrated
- Remove GetIt setup
- Remove GetIt dependency
## Checklist
- [ ] Added `flutter_riverpod` dependency
- [ ] Wrapped app with `ProviderScope`
- [ ] Created `lib/core/di/providers.dart`
- [ ] Defined all providers
- [ ] Converted widgets to `ConsumerWidget`
- [ ] Replaced `getIt<T>()` with `ref.watch(provider)`
- [ ] Updated tests to use `ProviderContainer`
- [ ] Tested all features
- [ ] Removed GetIt setup code
- [ ] Removed GetIt dependency
## Need Help?
- Check [README.md](./README.md) for comprehensive guide
- See [QUICK_REFERENCE.md](./QUICK_REFERENCE.md) for common patterns
- Review [ARCHITECTURE.md](./ARCHITECTURE.md) for understanding design
- Visit [Riverpod Documentation](https://riverpod.dev)
## Summary
Riverpod provides:
- ✅ Compile-time safety
- ✅ Better testing
- ✅ Automatic disposal
- ✅ Built-in state management
- ✅ No manual setup required
- ✅ Better developer experience
- ✅ Type-safe dependency injection
- ✅ Reactive by default
The migration effort is worth it for better code quality and maintainability!