Files
retail/RIVERPOD_DI_MIGRATION.md
Phuoc Nguyen bdaf0b96c5 fix
2025-10-10 17:36:10 +07:00

7.3 KiB

Riverpod Dependency Injection Migration

Date: October 10, 2025 Status: COMPLETE


Problem

The authentication system was trying to use GetIt for dependency injection, causing the following error:

Bad state: GetIt: Object/factory with type AuthRepository is not registered inside GetIt.

Additionally, there was a circular dependency error in the auth provider:

Bad state: Tried to read the state of an uninitialized provider.
This generally means that have a circular dependency, and your provider end-up depending on itself.

Solution

Migrated from GetIt to pure Riverpod dependency injection. All dependencies are now managed through Riverpod providers.


Changes Made

1. Updated Auth Provider (lib/features/auth/presentation/providers/auth_provider.dart)

Before:

import '../../../../core/di/injection_container.dart';

@riverpod
AuthRepository authRepository(Ref ref) {
  return sl<AuthRepository>();  // Using GetIt
}

@riverpod
class Auth extends _$Auth {
  @override
  AuthState build() {
    _checkAuthStatus();  // Circular dependency - calling async in build
    return const AuthState();
  }
}

After:

import '../../../../core/network/dio_client.dart';
import '../../../../core/storage/secure_storage.dart';
import '../../data/datasources/auth_remote_datasource.dart';
import '../../data/repositories/auth_repository_impl.dart';

/// Provider for DioClient (singleton)
@Riverpod(keepAlive: true)
DioClient dioClient(Ref ref) {
  return DioClient();
}

/// Provider for SecureStorage (singleton)
@Riverpod(keepAlive: true)
SecureStorage secureStorage(Ref ref) {
  return SecureStorage();
}

/// Provider for AuthRemoteDataSource
@Riverpod(keepAlive: true)
AuthRemoteDataSource authRemoteDataSource(Ref ref) {
  final dioClient = ref.watch(dioClientProvider);
  return AuthRemoteDataSourceImpl(dioClient: dioClient);
}

/// Provider for AuthRepository
@Riverpod(keepAlive: true)
AuthRepository authRepository(Ref ref) {
  final remoteDataSource = ref.watch(authRemoteDataSourceProvider);
  final secureStorage = ref.watch(secureStorageProvider);
  final dioClient = ref.watch(dioClientProvider);

  return AuthRepositoryImpl(
    remoteDataSource: remoteDataSource,
    secureStorage: secureStorage,
    dioClient: dioClient,
  );
}

@riverpod
class Auth extends _$Auth {
  @override
  AuthState build() {
    // Don't call async operations in build
    return const AuthState();
  }

  /// Initialize auth state - call this on app start
  Future<void> initialize() async {
    // Auth initialization logic moved here
  }
}

2. Removed GetIt Setup (lib/main.dart)

Before:

import 'core/di/service_locator.dart';

void main() async {
  WidgetsFlutterBinding.ensureInitialized();
  await Hive.initFlutter();

  // Setup dependency injection
  await setupServiceLocator();  // GetIt initialization

  runApp(const ProviderScope(child: RetailApp()));
}

After:

void main() async {
  WidgetsFlutterBinding.ensureInitialized();
  await Hive.initFlutter();

  // Run the app with Riverpod (no GetIt needed - using Riverpod for DI)
  runApp(const ProviderScope(child: RetailApp()));
}

3. Initialize Auth State on App Start (lib/app.dart)

Before:

class RetailApp extends ConsumerWidget {
  const RetailApp({super.key});

  @override
  Widget build(BuildContext context, WidgetRef ref) {
    return MaterialApp(/* ... */);
  }
}

After:

class RetailApp extends ConsumerStatefulWidget {
  const RetailApp({super.key});

  @override
  ConsumerState<RetailApp> createState() => _RetailAppState();
}

class _RetailAppState extends ConsumerState<RetailApp> {
  @override
  void initState() {
    super.initState();
    // Initialize auth state on app start
    WidgetsBinding.instance.addPostFrameCallback((_) {
      ref.read(authProvider.notifier).initialize();
    });
  }

  @override
  Widget build(BuildContext context) {
    return MaterialApp(/* ... */);
  }
}

Dependency Injection Architecture

Provider Hierarchy

DioClient (singleton)
    ↓
SecureStorage (singleton)
    ↓
AuthRemoteDataSource (uses DioClient)
    ↓
AuthRepository (uses AuthRemoteDataSource, SecureStorage, DioClient)
    ↓
Auth State Notifier (uses AuthRepository)

Provider Usage

// Access DioClient
final dioClient = ref.read(dioClientProvider);

// Access SecureStorage
final secureStorage = ref.read(secureStorageProvider);

// Access AuthRepository
final authRepository = ref.read(authRepositoryProvider);

// Access Auth State
final authState = ref.watch(authProvider);

// Call Auth Methods
await ref.read(authProvider.notifier).login(email: '...', password: '...');
await ref.read(authProvider.notifier).logout();

Benefits of Riverpod DI

  1. No Manual Registration: Providers are automatically available
  2. Type Safety: Compile-time type checking
  3. Dependency Graph: Riverpod manages dependencies automatically
  4. Testability: Easy to override providers in tests
  5. Code Generation: Auto-generates provider code
  6. No Circular Dependencies: Proper lifecycle management
  7. Singleton Management: Use keepAlive: true for singletons

GetIt Files (Now Unused)

These files are no longer needed but kept for reference:

  • lib/core/di/service_locator.dart - Old GetIt setup
  • lib/core/di/injection_container.dart - Old GetIt container

You can safely delete these files if GetIt is not used anywhere else in the project.


Migration Checklist

  • Create Riverpod providers for DioClient
  • Create Riverpod providers for SecureStorage
  • Create Riverpod providers for AuthRemoteDataSource
  • Create Riverpod providers for AuthRepository
  • Remove GetIt references from auth_provider.dart
  • Fix circular dependency in Auth.build()
  • Remove GetIt setup from main.dart
  • Initialize auth state in app.dart
  • Regenerate code with build_runner
  • Test compilation (0 errors)

Build Status

✅ Errors: 0
✅ Warnings: 61 (info-level only)
✅ Build: SUCCESS
✅ Code Generation: COMPLETE

Testing the App

  1. Run the app:

    flutter run
    
  2. Expected behavior:

    • App starts and shows login page (if not authenticated)
    • Login with valid credentials
    • Token is saved and added to Dio headers automatically
    • Navigate to Settings to see user profile
    • Logout button works correctly
    • After logout, back to login page

Key Takeaways

  1. Riverpod providers replace GetIt for dependency injection
  2. Use keepAlive: true for singleton providers (DioClient, SecureStorage)
  3. Never call async operations in build() - use separate initialization methods
  4. Initialize auth state in app.dart using addPostFrameCallback
  5. All dependencies are managed through providers - no manual registration needed

Next Steps (Optional)

If you want to further clean up:

  1. Delete unused GetIt files:

    rm lib/core/di/service_locator.dart
    rm lib/core/di/injection_container.dart
    
  2. Remove GetIt from dependencies in pubspec.yaml:

    # Remove this line:
    get_it: ^8.0.2
    
  3. Run flutter pub get to update dependencies


Status: MIGRATION COMPLETE - NO ERRORS

The app now uses pure Riverpod for all dependency injection!