flutter
/

Hydrated Bloc: Automatic State Persistence for BLoC & Cubit

Last Sync: Today

On this page

12
0%
5 min read
Remaining
5 minleft

Click any section to jump — progress syncs automatically

flutter

Hydrated Bloc: Automatic State Persistence for BLoC & Cubit

What is Hydrated Bloc?

Hydrated Bloc is an extension of flutter_bloc that automatically persists your BLoC and Cubit states to disk. When the app is closed and reopened, the state is restored exactly as it was, providing a seamless offline‑first experience. It uses Hive internally to store state efficiently and supports complex state serialization.

Why Use Hydrated Bloc?

  • Automatic Persistence – No manual saving/loading code in your BLoCs.
  • Offline Support – State survives app restarts and is available immediately.
  • Simple API – Just extend HydratedBloc or HydratedCubit and override fromJson/toJson.
  • Performance – Uses Hive, a lightweight, fast key‑value store.
  • Flexible Storage – Configurable storage directory, supports custom Hive adapters.

Setting Up Hydrated Bloc

Add the required dependencies to your pubspec.yaml:

YAMLRead-only
1
dependencies:
  flutter_bloc: ^8.1.5
  hydrated_bloc: ^9.1.5
  path_provider: ^2.1.3

Initialize HydratedBloc before running your app. You need to provide a storage directory, typically obtained from path_provider.

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

Creating a Hydrated Cubit

Extend HydratedCubit instead of Cubit. Override fromJson and toJson to serialize/deserialize your state.

DARTRead-only
1
class CounterCubit extends HydratedCubit<int> {
  CounterCubit() : super(0);

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

  @override
  int fromJson(Map<String, dynamic> json) => json['value'] as int;

  @override
  Map<String, dynamic> toJson(int state) => {'value': state};
}

Creating a Hydrated Bloc

Similarly, extend HydratedBloc<Event, State> and implement the same serialization methods.

DARTRead-only
1
class TodoBloc extends HydratedBloc<TodoEvent, TodoState> {
  TodoBloc() : super(const TodoState(todos: [])) {
    on<AddTodo>(_onAddTodo);
    on<RemoveTodo>(_onRemoveTodo);
  }

  void _onAddTodo(AddTodo event, Emitter<TodoState> emit) {
    emit(state.copyWith(todos: [...state.todos, event.todo]));
  }

  void _onRemoveTodo(RemoveTodo event, Emitter<TodoState> emit) {
    emit(state.copyWith(
      todos: state.todos.where((t) => t.id != event.id).toList(),
    ));
  }

  @override
  TodoState fromJson(Map<String, dynamic> json) => TodoState.fromJson(json);

  @override
  Map<String, dynamic> toJson(TodoState state) => state.toJson();
}

Serializing Complex State

For states with custom objects, you need to convert them to JSON‑compatible maps. Use Equatable to simplify equality checks.

DARTRead-only
1
@immutable
class TodoState extends Equatable {
  final List<Todo> todos;
  final bool isLoading;
  final String? error;

  const TodoState({required this.todos, this.isLoading = false, this.error});

  TodoState copyWith({List<Todo>? todos, bool? isLoading, String? error}) {
    return TodoState(
      todos: todos ?? this.todos,
      isLoading: isLoading ?? this.isLoading,
      error: error ?? this.error,
    );
  }

  @override
  List<Object?> get props => [todos, isLoading, error];

  Map<String, dynamic> toJson() => {
    'todos': todos.map((t) => t.toJson()).toList(),
    'isLoading': isLoading,
    'error': error,
  };

  factory TodoState.fromJson(Map<String, dynamic> json) {
    return TodoState(
      todos: (json['todos'] as List).map((t) => Todo.fromJson(t)).toList(),
      isLoading: json['isLoading'] ?? false,
      error: json['error'],
    );
  }
}

class Todo {
  final String id;
  final String title;
  final bool completed;

  Todo({required this.id, required this.title, this.completed = false});

  Map<String, dynamic> toJson() => {
    'id': id,
    'title': title,
    'completed': completed,
  };

  factory Todo.fromJson(Map<String, dynamic> json) => Todo(
    id: json['id'],
    title: json['title'],
    completed: json['completed'],
  );
}

Storage Configuration

You can customize storage behavior by providing your own HydratedStorage instance. For example, to use a different Hive box name or encryption.

DARTRead-only
1
final storage = await HydratedStorage.build(
  storageDirectory: await getApplicationDocumentsDirectory(),
  boxName: 'my_custom_box',
  encryptionCipher: EncryptionCipher.aesEncrypt(key: myKey),
);
HydratedBloc.storage = storage;

Clearing Persisted State

To reset the state, you can clear the storage or delete the specific box.

DARTRead-only
1
// Clear entire storage
HydratedBloc.storage.clear();

// Or delete only your BLoC's state (if you know the key)
HydratedBloc.storage.delete('CounterCubit');

Testing Hydrated Blocs

When testing, you can use a temporary storage directory to avoid polluting real storage.

DARTRead-only
1
setUp(() async {
  final storage = await HydratedStorage.build(
    storageDirectory: Directory.systemTemp,
  );
  HydratedBloc.storage = storage;
});

test('counter persists', () {
  final cubit = CounterCubit();
  cubit.increment();
  expect(cubit.state, 1);
  
  // Create new cubit; state should be restored
  final newCubit = CounterCubit();
  expect(newCubit.state, 1);
});

Best Practices

  • Use Equatable for state classes – Simplifies serialization and prevents unnecessary rebuilds.
  • Keep states small – Store only UI state; use a database for large datasets.
  • Initialize storage early – Always call HydratedStorage.build before runApp.
  • Avoid storing sensitive data – Hydrated Bloc stores data in plain text unless encrypted.
  • Test serialization – Write tests that verify toJson and fromJson work correctly.
  • Use copyWith methods – Makes state updates clean and immutable.
  • Provide fallbacks – In fromJson, handle missing keys gracefully.

Common Mistakes

  • ❌ Forgetting to initialize storage – Causes StateError when trying to save. ✅ Initialize before runApp.
  • ❌ Storing large objects – Slows down app startup and uses disk space. ✅ Use a database for large data; keep hydrated state minimal.
  • ❌ Not handling null in fromJson – Can throw exceptions when storage is empty. ✅ Provide default values.
  • ❌ Mutating state objects – Modifying state after emission can lead to inconsistencies. ✅ Always create new immutable state objects.
  • ❌ Using same storage for multiple apps – Conflicts can occur. ✅ Use different storage directories per app.

Conclusion

Hydrated Bloc is an essential tool for building offline‑ready Flutter apps. With minimal code, you can persist any BLoC or Cubit state, ensuring a consistent user experience across sessions. Combine it with proper state design and testing to create robust, performant applications.

Try it yourself

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';

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

class CounterCubit extends HydratedCubit<int> {
  CounterCubit() : super(0);

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

  @override
  int fromJson(Map<String, dynamic> json) => json['value'] as int;

  @override
  Map<String, dynamic> toJson(int state) => {'value': state};
}

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

class CounterPage extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    final cubit = context.watch<CounterCubit>();
    return Scaffold(
      appBar: AppBar(title: Text('Hydrated Bloc Demo')),
      body: Center(
        child: Column(
          mainAxisAlignment: MainAxisAlignment.center,
          children: [
            Text('Counter value:', style: TextStyle(fontSize: 20)),
            Text('${cubit.state}', style: TextStyle(fontSize: 40)),
            SizedBox(height: 20),
            Row(
              mainAxisAlignment: MainAxisAlignment.center,
              children: [
                ElevatedButton(
                  onPressed: cubit.increment,
                  child: Icon(Icons.add),
                ),
                SizedBox(width: 20),
                ElevatedButton(
                  onPressed: cubit.decrement,
                  child: Icon(Icons.remove),
                ),
              ],
            ),
            SizedBox(height: 20),
            Text('Try closing and reopening the app – the counter persists!'),
          ],
        ),
      ),
    );
  }
}

Test Your Knowledge

Q1
of 3

Which class should your Cubit extend to enable automatic state persistence?

A
Cubit
B
HydratedCubit
C
PersistedCubit
D
StorageCubit
Q2
of 3

What method must you override to convert your state to a JSON‑compatible map?

A
fromJson
B
toJson
C
serialize
D
encode
Q3
of 3

Where should you initialize HydratedBloc.storage?

A
Inside the first BLoC
B
Inside the build method of MyApp
C
Before runApp
D
In a separate isolate

Frequently Asked Questions

Can I use Hydrated Bloc with both Bloc and Cubit?

Yes, both HydratedBloc and HydratedCubit are available.

What happens if I change my state class structure?

You need to handle versioning manually. Consider adding a version field to your state JSON and writing migration logic in fromJson.

Does Hydrated Bloc work on all platforms?

Yes, it uses path_provider to get platform‑specific storage directories and Hive for storage, which supports mobile, desktop, and web.

Can I encrypt the stored data?

Yes, by providing an EncryptionCipher when building HydratedStorage.

How do I clear state for a specific BLoC?

Call clear() on the storage with the BLoC's type name as the key, or override onStorageReady and delete the box.

Previous

bloc lazy loading

Next

bloc local storage

Related Content

Need help?

Explore our comprehensive docs or start a chat with our tech experts.