# 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()); getIt.registerLazySingleton(() => ApiClient(getIt())); // Auth getIt.registerLazySingleton( () => AuthRemoteDataSourceImpl(getIt()), ); getIt.registerLazySingleton( () => AuthRepositoryImpl( remoteDataSource: getIt(), secureStorage: getIt(), ), ); getIt.registerLazySingleton( () => LoginUseCase(getIt()), ); } // Usage in widget class MyWidget extends StatelessWidget { @override Widget build(BuildContext context) { final authRepo = getIt(); final loginUseCase = getIt(); return Container(); } } ``` ### After (Riverpod) ```dart // Setup (in lib/core/di/providers.dart) final secureStorageProvider = Provider((ref) { return SecureStorage(); }); final apiClientProvider = Provider((ref) { final secureStorage = ref.watch(secureStorageProvider); return ApiClient(secureStorage); }); final authRemoteDataSourceProvider = Provider((ref) { final apiClient = ref.watch(apiClientProvider); return AuthRemoteDataSourceImpl(apiClient); }); final authRepositoryProvider = Provider((ref) { final remoteDataSource = ref.watch(authRemoteDataSourceProvider); final secureStorage = ref.watch(secureStorageProvider); return AuthRepositoryImpl( remoteDataSource: remoteDataSource, secureStorage: secureStorage, ); }); final loginUseCaseProvider = Provider((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()` 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(); 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.addListener(() { // Handle change }); // After (StateNotifier + Riverpod) class AuthNotifier extends StateNotifier { AuthNotifier() : super(AuthState.initial()); void login() { state = state.copyWith(isAuthenticated: true); } } // Provider final authProvider = StateNotifierProvider((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()); // After (Riverpod) final myServiceProvider = Provider((ref) { return MyService(); }); ``` ### Pattern 2: Factory (New Instance Each Time) ```dart // Before (GetIt) getIt.registerFactory(() => MyService()); // After (Riverpod) final myServiceProvider = Provider.autoDispose((ref) { return MyService(); }); ``` ### Pattern 3: Async Initialization ```dart // Before (GetIt) final myServiceFuture = getIt.getAsync(); // After (Riverpod) final myServiceProvider = FutureProvider((ref) async { final service = MyService(); await service.initialize(); return service; }); ``` ### Pattern 4: Conditional Registration ```dart // Before (GetIt) if (isProduction) { getIt.registerLazySingleton( () => ProductionApiClient(), ); } else { getIt.registerLazySingleton( () => MockApiClient(), ); } // After (Riverpod) final apiClientProvider = Provider((ref) { if (isProduction) { return ProductionApiClient(); } else { return MockApiClient(); } }); ``` ## Testing Migration ### Before (GetIt) ```dart void main() { setUp(() { // Clear and re-register getIt.reset(); getIt.registerLazySingleton( () => MockAuthRepository(), ); }); test('test case', () { final repo = getIt(); // 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( () => 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(); Text('${counter.count}'); // After class CounterNotifier extends StateNotifier { CounterNotifier() : super(0); void increment() { state = state + 1; } } final counterProvider = StateNotifierProvider((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(); // 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().dispose(); super.dispose(); } } // Riverpod - Automatic final myServiceProvider = Provider.autoDispose((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(() => 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()` 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!