12 KiB
12 KiB
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)
// 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)
// 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
// Before
void main() {
setupDI();
runApp(MyApp());
}
// After
void main() {
runApp(
const ProviderScope(
child: MyApp(),
),
);
}
Step 2: Convert Widgets to ConsumerWidget
// 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
// 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
// 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
// Before (GetIt)
getIt.registerLazySingleton<MyService>(() => MyService());
// After (Riverpod)
final myServiceProvider = Provider<MyService>((ref) {
return MyService();
});
Pattern 2: Factory (New Instance Each Time)
// Before (GetIt)
getIt.registerFactory<MyService>(() => MyService());
// After (Riverpod)
final myServiceProvider = Provider.autoDispose<MyService>((ref) {
return MyService();
});
Pattern 3: Async Initialization
// 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
// 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)
void main() {
setUp(() {
// Clear and re-register
getIt.reset();
getIt.registerLazySingleton<AuthRepository>(
() => MockAuthRepository(),
);
});
test('test case', () {
final repo = getIt<AuthRepository>();
// Test
});
}
After (Riverpod)
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)
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)
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
// 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
// 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
// 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
// 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
// ❌ 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
// ❌ 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()
// ❌ 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
// ❌ 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:
-
Phase 1: Add Riverpod
- Add dependency
- Wrap app with ProviderScope
- Keep GetIt for now
-
Phase 2: Migrate Core
- Create core providers
- Migrate one feature at a time
- Both systems can coexist
-
Phase 3: Migrate Features
- Start with simplest feature
- Test thoroughly
- Move to next feature
-
Phase 4: Remove GetIt
- Once all migrated
- Remove GetIt setup
- Remove GetIt dependency
Checklist
- Added
flutter_riverpoddependency - Wrapped app with
ProviderScope - Created
lib/core/di/providers.dart - Defined all providers
- Converted widgets to
ConsumerWidget - Replaced
getIt<T>()withref.watch(provider) - Updated tests to use
ProviderContainer - Tested all features
- Removed GetIt setup code
- Removed GetIt dependency
Need Help?
- Check README.md for comprehensive guide
- See QUICK_REFERENCE.md for common patterns
- Review ARCHITECTURE.md for understanding design
- Visit Riverpod Documentation
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!