fill
This commit is contained in:
569
lib/core/di/MIGRATION_GUIDE.md
Normal file
569
lib/core/di/MIGRATION_GUIDE.md
Normal 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!
|
||||
Reference in New Issue
Block a user