Complete Guide to Flutter Bloc: Persistent State Management with Hydrated Cubit and Bloc

Hydrated Bloc Block Pattern

Complete Guide to Flutter Bloc: Persistent State Management with Hydrated Cubit and Bloc

AI Reading

Quick summary of this article

This guide explains how to use the Bloc pattern with Hydrated Bloc in Flutter to manage and automatically save app state across restarts. It walks through building a simple app with a shopping cart and a brightness toggle, where both states persist even after closing and reopening the app.

  • Hydrated Bloc automatically saves and restores state using local storage, so user data like cart items or theme settings survive app restarts without extra code.
  • The app separates business logic into Cubits (for cart items) and Blocs (for brightness), making the code easier to maintain and test.
  • To use Hydrated Bloc, you must initialize storage in main.dart and override fromJson and toJson methods in each Bloc or Cubit to define how state is saved and loaded.
  • The cart example uses a HydratedCubit that stores a list of CartItem objects, while the brightness example uses a HydratedBloc that toggles between light and dark themes.
  • State changes are triggered by events (like ToggleBrightnessEvent) or direct method calls (like addToCart), and the UI rebuilds automatically using BlocBuilder.

Introduction to Persistent State Management in Flutter

Flutter is an amazing framework for building cross-platform mobile applications. However, when it comes to state management, developers need a solid solution that is scalable and maintainable. This is where the Bloc (Business Logic Component) pattern comes in. Combined with Hydrated Bloc, it becomes an extremely powerful tool to manage and persist state across app restarts.

In this guide, we will:

  • Create a Flutter app using Bloc and Cubit
  • Use HydratedBloc for persisting state
  • Structure the app into Cubits, Blocs, Models, and Screens
  • Implement a cart system and brightness toggle that retains state

Folder Structure

lib/
├── main.dart
├── blocs/
│   └── brightness_bloc.dart
├── cubits/
│   └── cart_cubit.dart
├── models/
│   └── cart_model.dart
└── screens/
    ├── cart_page.dart
    └── home_page.dart

Step 1: Setup Dependencies

In your pubspec.yaml:

dependencies:
  flutter:
    sdk: flutter
  flutter_bloc: ^9.1.0
  hydrated_bloc: ^10.0.0
  path_provider: ^2.0.15

Run:

flutter pub get

Step 2: main.dart (Initialize Hydrated Storage)

import ‘package:adv_bloc/blocs/brightness_bloc.dart’;
import ‘package:adv_bloc/screens/home_page.dart’;
import ‘package:flutter/foundation.dart’;
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();
  HydratedBloc.storage = await HydratedStorage.build(
    storageDirectory: kIsWeb
        ? HydratedStorageDirectory.web
        : HydratedStorageDirectory((await getTemporaryDirectory()).path),
  );
  runApp(
    BlocProvider(
      create: (_) => BrightnessBloc(),
      child: const MyApp(),
    ),
  );
}
class MyApp extends StatelessWidget {
  const MyApp({super.key});
  @override
  Widget build(BuildContext context) {
    return BlocBuilder<BrightnessBloc, Brightness>(
      builder: (context, brightness) {
        return MaterialApp(
          title: ‘Bloc Theme Demo’,
          theme: ThemeData(
            brightness: brightness,
          ),
          home: const HomePage(),
        );
      },
    );
  }
}

Step 3: blocs/brightness_bloc.dart

import ‘package:flutter/material.dart’;
import ‘package:hydrated_bloc/hydrated_bloc.dart’;
abstract class BrightnessEvent {}
class ToggleBrightnessEvent extends BrightnessEvent {}
class BrightnessBloc extends HydratedBloc<BrightnessEvent, Brightness> {
  BrightnessBloc() : super(Brightness.light) {
    // 👇 This is the missing part: registering the event handler!
    on<ToggleBrightnessEvent>((event, emit) {
      emit(state == Brightness.light ? Brightness.dark : Brightness.light);
    });
  }
  @override
  Brightness fromJson(Map<String, dynamic> json) {
    return Brightness.values[json[‘brightness’] as int];
  }
  @override
  Map<String, dynamic> toJson(Brightness state) {
    return {‘brightness’: state.index};
  }
}

Step 4: models/cart_model.dart

class CartItem {
  final String id;
  final String name;
  final double price;
  CartItem({required this.id, required this.name, required this.price});
}

Step 5: cubits/cart_cubit.dart

import ‘package:adv_bloc/models/cart_model.dart’;
import ‘package:flutter_bloc/flutter_bloc.dart’;
import ‘package:hydrated_bloc/hydrated_bloc.dart’;
class CartCubit extends HydratedCubit<List<CartItem>> {
  CartCubit() : super([]);
  void addToCart(CartItem item) {
    final updatedItems = List<CartItem>.from(state)..add(item);
    emit(updatedItems);
  }
  void removeFromCart(CartItem item) {
    final updatedItems = List<CartItem>.from(state)..remove(item);
    emit(updatedItems);
  }
  @override
  List<CartItem> fromJson(Map<String, dynamic> json) {
    final List<dynamic> itemsJson = json[‘cart’] ?? [];
    return itemsJson
        .map((e) => CartItem(
              id: e[‘id’],
              name: e[‘name’],
              price: e[‘price’].toDouble(),
            ))
        .toList();
  }
  @override
  Map<String, dynamic> toJson(List<CartItem> state) {
    return {
      ‘cart’: state
          .map((e) => {
                ‘id’: e.id,
                ‘name’: e.name,
                ‘price’: e.price,
              })
          .toList(),
    };
  }
}

Step 6: screens/home_page.dart

import ‘package:adv_bloc/blocs/brightness_bloc.dart’;
import ‘package:adv_bloc/cubits/cart_cubit.dart’;
import ‘package:adv_bloc/main.dart’;
import ‘package:adv_bloc/screens/cart_page.dart’;
import ‘package:flutter/material.dart’;
import ‘package:flutter_bloc/flutter_bloc.dart’;
class HomePage extends StatelessWidget {
  const HomePage({super.key});
  @override
  Widget build(BuildContext context) {
    return BlocProvider(
      create: (_) => CartCubit(),
      child: const CartPage(),
    );
  }
}

Step 7: screens/cart_page.dart

import ‘package:adv_bloc/blocs/brightness_bloc.dart’;
import ‘package:adv_bloc/cubits/cart_cubit.dart’;
import ‘package:adv_bloc/models/cart_model.dart’;
import ‘package:flutter/material.dart’;
import ‘package:flutter_bloc/flutter_bloc.dart’;
class CartPage extends StatelessWidget {
  const CartPage({super.key});
  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(title: const Text(‘Cart’)),
      body: BlocBuilder<CartCubit, List<CartItem>>(
        builder: (context, cartItems) {
          return cartItems.isEmpty
              ? const Center(child: Text(‘Your cart is empty!’))
              : ListView.builder(
                  itemCount: cartItems.length,
                  itemBuilder: (context, index) {
                    final item = cartItems[index];
                    return ListTile(
                      title: Text(item.name),
                      subtitle: Text(\$${item.price.toStringAsFixed(2)}),
                      trailing: IconButton(
                        icon: const Icon(Icons.remove_shopping_cart),
                        onPressed: () {
                          context.read<CartCubit>().removeFromCart(item);
                        },
                      ),
                    );
                  },
                );
        },
      ),
      floatingActionButton: Column(
        mainAxisAlignment: MainAxisAlignment.end,
        children: <Widget>[
          FloatingActionButton(
            child: const Icon(Icons.add_shopping_cart),
            onPressed: () {
              context.read<CartCubit>().addToCart(
                  CartItem(id: ‘1’, name: ‘Product 1’, price: 29.99));
            },
          ),
          const SizedBox(height: 4),
          FloatingActionButton(
            child: const Icon(Icons.brightness_6),
            onPressed: () {
              context.read<BrightnessBloc>().add(ToggleBrightnessEvent());
            },
          ),
        ],
      ),
    );
  }
}

Conclusion

With Hydrated Bloc, Flutter state management becomes not just reactive and powerful, but persistent across sessions. By separating business logic using Cubits and Blocs, and saving them using Hydrated Bloc, you create maintainable, testable, and user-friendly apps.

Leave a Reply

Your email address will not be published. Required fields are marked *