πŸš€ Migrating from HydratedBloc to Riverpod for Page State Management

This guide shows how to replace HydratedBloc with clean, testable, and maintainable Riverpod code for persisting page states like FilterState.

πŸ“‹ What We’re Replacing

Before (HydratedBloc approach):

class HomeScreenCubit extends HydratedCubit<HomeScreenCubitState> {
  HomeScreenCubit() : super(HomeScreenCubitState(filterState: HomeFilterState()));

  @override
  HomeScreenCubitState? fromJson(Map<String, dynamic> json) {
    return HomeScreenCubitState(
      filterState: FilterState.fromJson(json[FilterState.jsonKey]),
    );
  }

  @override
  Map<String, dynamic>? toJson(HomeScreenCubitState state) {
    return {
      FilterState.jsonKey: FilterState.toJson(state.filterState),
    };
  }
}

After (Riverpod approach):

final homeFilterStateProvider = StateNotifierProvider<HomeFilterStateNotifier, FilterState>((ref) {
  return HomeFilterStateNotifier(ref);
});

class HomeFilterStateNotifier extends StateNotifier<FilterState> {
  // Clean, testable, maintainable implementation
  // Automatic persistence with SharedPreferences
  // Clear separation of concerns
}

🎯 Key Benefits of Riverpod Approach

  1. πŸ§ͺ More Testable: Easy to mock providers and test state changes
  2. πŸ”§ Better Maintainability: Clear separation between business logic and persistence
  3. πŸ“¦ Smaller Bundle Size: No need for HydratedBloc dependency
  4. πŸš€ Better Performance: Granular reactivity with Riverpod
  5. 🧹 Easy Cache Management: Built-in cache clearing functionality
  6. πŸ“± Better State Management: Automatic state restoration when returning to pages

πŸ“š Complete Implementation

1. Provider Setup (home_filter_provider.dart)

The provider handles:

  • βœ… Automatic state persistence with SharedPreferences
  • βœ… State restoration when returning to the page
  • βœ… Easy cache clearing for user preferences
  • βœ… Granular access to different filter aspects
  • βœ… Error handling for persistence failures

2. Updated Screen (home_screen_riverpod.dart)

Key changes:

// Before: StatefulWidget with BlocProvider
class HomeScreen extends StatefulWidget

// After: ConsumerStatefulWidget with Riverpod
class HomeScreen extends ConsumerStatefulWidget

// Before: BlocBuilder and context.watch<HomeScreenCubit>()
final filterState = context.watch<HomeScreenCubit>().state.filterState;

// After: Direct provider watching
final filterState = ref.watch(homeFilterStateProvider);

// Before: context.read<HomeScreenCubit>().setFilterState()
context.read<HomeScreenCubit>().setFilterState(newState);

// After: Provider notifier calls
ref.read(homeFilterStateProvider.notifier).updateFilterState(newState);

3. App Initialization (app_initialization.dart)

Shows how to properly initialize SharedPreferences with Riverpod:

void main() async {
  WidgetsFlutterBinding.ensureInitialized();

  final container = await AppInitialization.initializeApp();

  runApp(
    UncontrolledProviderScope(
      container: container,
      child: MyApp(),
    ),
  );
}

🎨 Usage Examples

Basic State Watching

class MyWidget extends ConsumerWidget {
  @override
  Widget  (BuildContext context, WidgetRef ref) {
    // Watch the entire filter state
    final filterState = ref.watch(homeFilterStateProvider);

    // Or watch specific aspects
    final showFilters = ref.watch(activeShowFiltersProvider);
    final contentFilters = ref.watch(activeContentFiltersProvider);

    return YourWidgetTree();
  }
}

Updating State

// Reset to default
ref.read(homeFilterStateProvider.notifier).resetToDefault();

// Apply specific changes
ref.read(homeFilterStateProvider.notifier).applyFilterChange(change);

// Update entire state
ref.read(homeFilterStateProvider.notifier).updateFilterState(newState);

Cache Management

final cacheManager = ref.read(appCacheManagerProvider);

// Clear only filter cache
await cacheManager.clearFilterCache();

// Clear all app cache
await cacheManager.clearAllApplicationCache();

πŸ§ͺ Testing Benefits

Before (HydratedBloc):

// Complex setup with mock storage
testWidgets('filter state test', (tester) async {
  final storage = MockStorage();
  HydratedBloc.storage = storage;
  // Complex setup...
});

After (Riverpod):

// Clean, simple mocking
testWidgets('filter state test', (tester) async {
  await tester.pumpWidget(
    ProviderScope(
      overrides: [
        homeFilterStateProvider.overrideWith((ref) => MockFilterNotifier()),
      ],
      child: MyApp(),
    ),
  );
});

πŸ”„ Migration Steps

  1. Install Dependencies:

    dependencies:
      hooks_riverpod: ^2.4.9
      shared_preferences: ^2.2.2
    
  2. Remove HydratedBloc Dependencies:

    # Remove these
    # hydrated_bloc: ^9.1.3
    # path_provider: ^2.1.1
    
  3. Replace BlocProvider with ProviderScope in your app root

  4. Convert StatefulWidgets to ConsumerStatefulWidgets

  5. Replace context.watch/read with ref.watch/read

  6. Set up SharedPreferences initialization

πŸŽ‰ Result

  • βœ… Persistent State: FilterState automatically persists and restores
  • βœ… Easy Cache Clearing: Built-in functionality for clearing all preserved states
  • βœ… Better Performance: Granular reactivity with Riverpod
  • βœ… Cleaner Code: Separation of business logic and persistence
  • βœ… Better Testing: Easy mocking and testing
  • βœ… Future-Proof: Extensible for additional page states

The user returns to HomeScreen and their previous FilterState is automatically restored, and they can easily clear all preserved states when needed!