How to Use Hydrated Bloc for Persistent State Management

Introduction

State management is a crucial aspect of Flutter development, especially when dealing with applications that need to maintain state across app restarts. Hydrated Bloc, an extension of the Bloc library, enables effortless state persistence by automatically storing and restoring state using local storage.

In this article, we will cover:

  • What Hydrated Bloc is and why it’s useful.
  • How to integrate Hydrated Bloc into a Flutter project.
  • A step-by-step implementation guide with examples.

What is Hydrated Bloc?

Hydrated Bloc is a state management solution built on top of Bloc, which persists state across app restarts using local storage such as SharedPreferences or Hive.

Why Use Hydrated Bloc?

  • Automatic State Persistence: Restores state automatically after app restarts.
  • Minimal Boilerplate: No need to manually save and retrieve state.
  • Efficient Storage: Uses binary storage for optimal performance.
  • Works with Bloc: Seamlessly integrates with the Bloc pattern.

Installing Hydrated Bloc in a Flutter Project

Add the following dependencies to your pubspec.yaml:

dependencies:
  flutter_bloc: ^8.0.0
  hydrated_bloc: ^9.1.0
  path_provider: ^2.0.11
  equatable: ^2.0.3

Run:

flutter pub get 

Setting Up Hydrated Bloc

1. Initializing Hydrated Storage

Before using Hydrated Bloc, we need to initialize storage in the main.dart file:

import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:hydrated_bloc/hydrated_bloc.dart';
import 'package:path_provider/path_provider.dart';
import 'counter_bloc.dart';

void main() async {
  WidgetsFlutterBinding.ensureInitialized();
  final storage = await HydratedStorage.build(storageDirectory: await getApplicationDocumentsDirectory());
  HydratedBloc.storage = storage;
  runApp(MyApp());
}

class MyApp extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return BlocProvider(
      create: (_) => CounterBloc(),
      child: MaterialApp(
        home: CounterScreen(),
      ),
    );
  }
}

2. Creating a Hydrated Bloc

Hydrated Bloc works like a regular Bloc but requires toJson and fromJson methods to serialize and deserialize state.

import 'package:hydrated_bloc/hydrated_bloc.dart';
import 'package:equatable/equatable.dart';

// Counter State
class CounterState extends Equatable {
  final int counter;
  const CounterState(this.counter);

  @override
  List<Object> get props => [counter];
}

// Counter Bloc
class CounterBloc extends HydratedBloc<String, CounterState> {
  CounterBloc() : super(const CounterState(0));

  void increment() => emit(CounterState(state.counter + 1));
  void decrement() => emit(CounterState(state.counter - 1));

  @override
  CounterState? fromJson(Map<String, dynamic> json) {
    return CounterState(json['counter'] as int);
  }

  @override
  Map<String, dynamic>? toJson(CounterState state) {
    return {'counter': state.counter};
  }
}

3. Creating the UI

import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import 'counter_bloc.dart';

class CounterScreen extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(title: Text("Hydrated Bloc Counter")),
      body: Center(
        child: BlocBuilder<CounterBloc, CounterState>(
          builder: (context, state) {
            return Text(
              'Counter: ${state.counter}',
              style: TextStyle(fontSize: 24),
            );
          },
        ),
      ),
      floatingActionButton: Row(
        mainAxisAlignment: MainAxisAlignment.end,
        children: [
          FloatingActionButton(
            onPressed: () => context.read<CounterBloc>().increment(),
            child: Icon(Icons.add),
          ),
          SizedBox(width: 10),
          FloatingActionButton(
            onPressed: () => context.read<CounterBloc>().decrement(),
            child: Icon(Icons.remove),
          ),
        ],
      ),
    );
  }
}

Handling Edge Cases

1. Clearing Persisted State

If needed, you can manually clear the persisted state using:

context.read<CounterBloc>().clear();

2. Handling Corrupt Storage

To handle corrupt storage scenarios, update the initialization in main.dart:

void main() async {
  WidgetsFlutterBinding.ensureInitialized();
  final storage = await HydratedStorage.build(
    storageDirectory: await getApplicationDocumentsDirectory(),
  );
  HydratedBloc.storage = storage;
  runApp(MyApp());
}

Conclusion

Hydrated Bloc is a powerful extension to Bloc that enables persistent state management with minimal effort. It is particularly useful for applications that require state retention across app restarts.

Key Takeaways:

  • Hydrated Bloc persists state across app restarts without manual intervention.
  • Requires toJson and fromJson to serialize and deserialize state.
  • Easy integration with existing Bloc-based architectures.

Call to Action

Try implementing Hydrated Bloc in your next Flutter project and experience seamless state persistence. Follow our blog for more Flutter development insights!