Files
iiEsaywebUIapp/docs/riverpod_migration_example.md
cogwheel0 f18d378c3c docs: add comprehensive Riverpod 3.0 migration documentation and Priority 1 implementation
Priority 1 (COMPLETE):
- Add riverpod_lint and custom_lint packages
- Update analysis_options.yaml with custom_lint plugin
- Update AGENTS.md with Riverpod 3.0 best practices
- Fix unsafe ref usage in modern_chat_input.dart
- All tests passing, zero breaking changes

Priority 2 (PLANNED):
- Complete migration plan for 39 providers (RIVERPOD_PRIORITY2_PLAN.md)
- Quick reference guide (RIVERPOD_PRIORITY2_QUICKREF.md)
- Progress tracker (RIVERPOD_PRIORITY2_TRACKER.md)
- Master documentation index (RIVERPOD_MIGRATION_INDEX.md)
- Analysis and summary documents

Documentation includes:
- Step-by-step migration examples
- 6-phase implementation plan (23-33 hours)
- Testing strategies and rollback procedures
- Risk assessment and mitigation
- Timeline and resource estimates
2025-09-30 14:27:50 +05:30

10 KiB

Riverpod Migration Example

Example: Migrating SearchQueryNotifier

This example shows step-by-step how to migrate a simple provider from manual declaration to code generation.


Current Code (Manual NotifierProvider)

File: lib/core/providers/app_providers.dart (lines ~1200-1209)

// Manual provider declaration
final searchQueryProvider = NotifierProvider<SearchQueryNotifier, String>(
  SearchQueryNotifier.new,
);

class SearchQueryNotifier extends Notifier<String> {
  @override
  String build() => '';

  void set(String query) => state = query;
}

Usage in code:

// Reading value
final query = ref.watch(searchQueryProvider);

// Updating value
ref.read(searchQueryProvider.notifier).set('new search');

Migrated Code (Code Generation)

File: lib/core/providers/app_providers.dart

Step 1: Add annotation and extend generated class

@riverpod
class SearchQuery extends _$SearchQuery {  // Note: Class name changes
  @override
  String build() => '';

  void set(String query) => state = query;
}

Step 2: Run build_runner

dart run build_runner build --delete-conflicting-outputs

This generates app_providers.g.dart with:

// GENERATED CODE - DO NOT MODIFY BY HAND

// **************************************************************************
// RiverpodGenerator
// **************************************************************************

String _$searchQueryHash() => r'...';

/// See also [SearchQuery].
@ProviderFor(SearchQuery)
final searchQueryProvider = AutoDisposeNotifierProvider<SearchQuery, String>.internal(
  SearchQuery.new,
  name: r'searchQueryProvider',
  debugGetCreateSourceHash: _$searchQueryHash,
  dependencies: null,
  allTransitiveDependencies: null,
);

typedef _$SearchQuery = AutoDisposeNotifier<String>;

Step 3: Update imports (if needed)

No changes needed! The provider name stays the same: searchQueryProvider

Step 4: Usage remains identical

// Reading value - NO CHANGE
final query = ref.watch(searchQueryProvider);

// Updating value - NO CHANGE
ref.read(searchQueryProvider.notifier).set('new search');

Benefits of Migration

Before (Manual)

// 8 lines of boilerplate
final searchQueryProvider = NotifierProvider<SearchQueryNotifier, String>(
  SearchQueryNotifier.new,
);

class SearchQueryNotifier extends Notifier<String> {
  @override
  String build() => '';

  void set(String query) => state = query;
}

Issues:

  • More verbose
  • Need to manually create provider variable
  • Easy to forget to update provider declaration when class changes
  • No automatic dependency tracking

After (Code Generation)

// 6 lines, cleaner
@riverpod
class SearchQuery extends _$SearchQuery {
  @override
  String build() => '';

  void set(String query) => state = query;
}

Benefits:

  • Less boilerplate
  • Provider auto-generated
  • Type-safe
  • Better IDE support
  • Automatic dependency tracking
  • Easier to add family or modifiers later

More Complex Example: ThemeModeNotifier

Current Code (Manual)

final themeModeProvider = NotifierProvider<ThemeModeNotifier, ThemeMode>(
  ThemeModeNotifier.new,
);

class ThemeModeNotifier extends Notifier<ThemeMode> {
  late final OptimizedStorageService _storage;

  @override
  ThemeMode build() {
    _storage = ref.watch(optimizedStorageServiceProvider);
    final storedMode = _storage.getThemeMode();
    if (storedMode != null) {
      return ThemeMode.values.firstWhere(
        (e) => e.toString() == storedMode,
        orElse: () => ThemeMode.system,
      );
    }
    return ThemeMode.system;
  }

  void setTheme(ThemeMode mode) {
    state = mode;
    _storage.setThemeMode(mode.toString());
  }
}

Migrated Code (Code Generation)

@riverpod
class AppThemeMode extends _$AppThemeMode {  // Renamed to avoid conflict with ThemeMode enum
  late final OptimizedStorageService _storage;

  @override
  ThemeMode build() {
    _storage = ref.watch(optimizedStorageServiceProvider);
    final storedMode = _storage.getThemeMode();
    if (storedMode != null) {
      return ThemeMode.values.firstWhere(
        (e) => e.toString() == storedMode,
        orElse: () => ThemeMode.system,
      );
    }
    return ThemeMode.system;
  }

  void setTheme(ThemeMode mode) {
    state = mode;
    _storage.setThemeMode(mode.toString());
  }
}

// Generated provider will be: appThemeModeProvider

Important: Class renamed from ThemeModeNotifier to AppThemeMode to avoid name conflict with the ThemeMode enum from Flutter.

Update Usage

// Before
final mode = ref.watch(themeModeProvider);
ref.read(themeModeProvider.notifier).setTheme(ThemeMode.dark);

// After
final mode = ref.watch(appThemeModeProvider);
ref.read(appThemeModeProvider.notifier).setTheme(ThemeMode.dark);

Migration tool can help:

# Find all usages
grep -r "themeModeProvider" lib/

# Replace with IDE refactoring or:
find lib -type f -name "*.dart" -exec sed -i '' 's/themeModeProvider/appThemeModeProvider/g' {} +

Provider Function Example

FutureProvider to @riverpod function

Before:

final serverConfigsProvider = FutureProvider<List<ServerConfig>>((ref) async {
  final storage = ref.watch(optimizedStorageServiceProvider);
  return storage.getServerConfigs();
});

After:

@riverpod
Future<List<ServerConfig>> serverConfigs(ServerConfigsRef ref) async {
  final storage = ref.watch(optimizedStorageServiceProvider);
  return storage.getServerConfigs();
}

// Generated provider name: serverConfigsProvider (same!)

Usage - NO CHANGE:

final configs = ref.watch(serverConfigsProvider);
// or
final configs = await ref.read(serverConfigsProvider.future);

Family Provider Example

Before (Manual)

final loadConversationProvider = FutureProvider.family<Conversation, String>((
  ref,
  conversationId,
) async {
  final api = ref.watch(apiServiceProvider);
  if (api == null) {
    throw Exception('No API service available');
  }
  return await api.getConversation(conversationId);
});

After (Code Generation)

@riverpod
Future<Conversation> loadConversation(
  LoadConversationRef ref,
  String conversationId,  // Family parameter
) async {
  final api = ref.watch(apiServiceProvider);
  if (api == null) {
    throw Exception('No API service available');
  }
  return await api.getConversation(conversationId);
}

// Usage stays the same!
// ref.watch(loadConversationProvider(conversationId))

Benefits:

  • Automatic .family modifier handling
  • Type-safe parameters
  • Better parameter completion in IDE
  • Can add multiple parameters easily

Keep Alive Example

Before

@Riverpod(keepAlive: true)
class AuthStateManager extends _$AuthStateManager {
  // ...
}

After

No change needed! Already using code generation correctly.


Migration Checklist

For each provider to migrate:

  • Identify the provider type (Notifier, AsyncNotifier, function)
  • Check for name conflicts (e.g., ThemeModeNotifier vs ThemeMode)
  • Add @riverpod annotation
  • Change class to extend _$ClassName
  • Remove manual provider declaration
  • Run dart run build_runner build
  • Update all usages (IDE refactoring recommended)
  • Test the provider functionality
  • Commit the change

Testing After Migration

Unit Test Example

Before:

test('searchQuery updates correctly', () {
  final container = ProviderContainer();
  
  expect(container.read(searchQueryProvider), '');
  
  container.read(searchQueryProvider.notifier).set('test');
  
  expect(container.read(searchQueryProvider), 'test');
});

After:

test('searchQuery updates correctly', () {
  final container = ProviderContainer();
  
  // Same test code - no changes needed!
  expect(container.read(searchQueryProvider), '');
  
  container.read(searchQueryProvider.notifier).set('test');
  
  expect(container.read(searchQueryProvider), 'test');
});

Tests remain identical!


Common Pitfalls

1. Class Name Conflicts

Problem:

@riverpod
class ThemeMode extends _$ThemeMode {  // ❌ Conflicts with Flutter's ThemeMode
  // ...
}

Solution:

@riverpod
class AppThemeMode extends _$AppThemeMode {  // ✅ Unique name
  // ...
}

2. Forgetting to Run Build Runner

Problem: After adding @riverpod, code doesn't compile.

Error: The getter '_$SearchQuery' isn't defined for the class 'SearchQuery'.

Solution:

dart run build_runner build --delete-conflicting-outputs

3. Mixing Manual and Generated Providers

Problem: Some providers use @riverpod, others use manual NotifierProvider.

Solution: Be consistent! Migrate all providers in a file together to maintain consistency.


IDE Support

VS Code

Add to .vscode/tasks.json:

{
  "version": "2.0.0",
  "tasks": [
    {
      "label": "build_runner watch",
      "type": "shell",
      "command": "dart run build_runner watch --delete-conflicting-outputs",
      "isBackground": true,
      "problemMatcher": []
    }
  ]
}

Run with Cmd+Shift+P → "Tasks: Run Task" → "build_runner watch"

Android Studio / IntelliJ

  1. Run → Edit Configurations
  2. Add new "Shell Script" configuration
  3. Script text: dart run build_runner watch --delete-conflicting-outputs
  4. Working directory: $ProjectFileDir$

Summary

Effort per provider: ~5-10 minutes
Risk level: 🟢 Low (tests verify behavior)
Benefit: High (consistency, maintainability, developer experience)

Recommended order:

  1. Start with simple Notifier classes (like SearchQueryNotifier)
  2. Move to FutureProvider functions
  3. Then tackle complex AsyncNotifier classes
  4. Keep @Riverpod(keepAlive: true) providers for last (already correct)

Total providers to migrate: ~30-40 (based on codebase analysis)
Estimated total time: 5-8 hours spread across multiple sessions