Hydrated Bloc
Block Pattern

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

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 *