Eshiet Ekemini A graduate of University of Uyo and a tech enthusiast, Ekemini has been building for mobile for two years, with a particular focus on Kotlin and Flutter.

Persisting application state with Hydrated BLoC in Flutter

4 min read 1291

Persisting State With Hydrated Bloc Pattern In Flutter

Remember BLoC?

BLoC is an extremely powerful solution for state management in the Flutter ecosystem. The acronym BloC simply means a business logic component. In computer sciences, this is referred to as the logic layer or business logic part of a program that encapsulates rules dictating how data can be created, stored, or modified.

The BLoC library was created to cater to state management, making it simple, powerful to use (even with large-scale business logic), and also testable at the same time.

BLoC is made up of events and states. It takes in events, and based on predefined rules, yields a different state once data has been processed to meet certain criteria.

What is Hydrated BLoC?

Hydrated BLoC, on the other hand, is an extension of the bloc package which provides out-of-the-box persistence for our blocs or cubits.

There are lots of benefits associated with properly persisting the state of our applications. It makes our applications easier for users to make use of, especially when they do not have to re-enter certain data every time they launch our application.

This situation occurs mainly because of how our operating system tends to clear or destroy our activities and states that are contained in it, whenever we close our application.

For instance, most users would prefer using a weather application that by default, shows the weather situation of your recent location or the last location you checked, rather than having to manually search your location anytime they open it.

Another good example of situations where state persistence is of utmost importance can be experienced when using a browser application. Rather than always having to browse the internet afresh, most people would love to continue from the last page they were on while using their browser application, and this is where saving the state of your application should be a huge consideration for you.

Why use Hydrated BLoC?

If you are using the BLoC library for state management in Flutter, then you do not need to write a lot of code to save and restore your state. You can simply make use of the hydrated BLoC from the BLoC library, and this is similar to onSaveInstanceState() for those that come from a native Android development background.

In this tutorial, we are going to build a simple random number generator. In order to demonstrate how to use persist the state of our application, we will make use of our Hydrated BLoC to ensure that whenever the app is restarted, it displays the last random number that was generated.

We made a custom demo for .
No really. Click here to check it out.

Getting started

In order to use Hydrated BLoC for State persistence, this article assumes you have a basic understanding of using the BLoC library for state management.

During the course of this project, we will need to persist our blocs, and we‘ll start by adding the necessary dependencies to help us do that.

One of these is the latest version of the hydrated bloc library, and we add other dependencies as shown in our pubspec.yaml file below:

dependencies:
 hydrated_bloc: ^8.1.0
 flutter_bloc: ^8.0.0
 equatable: ^0.5.1
 json_annotation: ^3.0.0
 path: ^1.8.0
 path_provider: ^2.0.9

The next step you need to do is point the Hydrated BLoC library to a path where it should persist data on our local storage

The code snippet inside of our main method below helps us achieve this task:

Future<void> main() async {
  WidgetsFlutterBinding.ensureInitialized();
 final storage = await HydratedStorage.build(
  storageDirectory: kIsWeb
      ? HydratedStorage.webStorageDirectory
      : await getTemporaryDirectory(),
);

HydratedBlocOverrides.runZoned(
  () => runApp(AppView()),
  storage: storage,
);
}

The reason we call WidgetsFlutterBinding.ensureInitialized() before runApp is that Hydrated BLoC has to communicate with native code, and to ensure that we do this seamlessly, we check that everything is natively initialized.

  • The HydratedStorage.build() function is then used to create a storage for our application. The storageDirectory parameter is set to that of the webStorageDirectory depending on the platform, else it would by default be set to the device’s temporary storage
  • The HydratedStorage.build() also checks if any previously saved data exists and tries to restore that data by deserializing it and emitting the state that was last saved on our application. This is possible because Hydrated BLoC uses Hive under the hood to store data
  • In order to ensure that our app runs safely after all the processes we’ve declared, we need to wrap the call to runApp with HydratedBlocOverrides.runZoned()

Making a hydrated BLoC

For our view, we have a simple UI consisting of a text view and two buttons. One of our buttons is for generating a random number and the other is for resetting the generated random number to zero:

class RandomNumberView extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    final textTheme = Theme.of(context).textTheme;
    return Scaffold(
      appBar: AppBar(title: const Text('Counter')),
      body: Container(
        decoration: BoxDecoration(color: ThemeData().primaryColor),
        child: Center(
          child: BlocBuilder<RandomNumberBloc, int>(
            builder: (context, state) {
              return Column(
                mainAxisAlignment: MainAxisAlignment.center,
                children: [
                  Text('$state',
                      style: textTheme.headline2?.copyWith(
                          fontSize: 48,
                          fontWeight: FontWeight.bold,
                          color: Colors.white)),
                  const SizedBox(
                    height: 50,
                  ),
                  Button(
                    title: "Random Number",
                    action: () {
                      context
                          .read<RandomNumberBloc>()
                          .add(GenerateRandomNumber(max: 20, min: 1));
                    },
                  ),
                  const SizedBox(
                    height: 10,
                  ),
                  Button(
                    title: "Reset",
                    action: () {
                      context.read<RandomNumberBloc>().add(ResetRandomNumber());
                      HydratedBlocOverrides.current?.storage.clear();
                    },
                  )
                ],
              );
            },
          ),
        ),
      ),
    );
  }
}

In order to make our bloc available to the rest of our widget tree, we are going to pass it down to the widget tree using the BlocProvider.

BlocProvider is used to provide a widget with access to a bloc, and it uses dependency injection (DI) to ensure that a single instance of the bloc is available to multiple widgets within the widget tree:

class RandomNumberGeneratorPage extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return BlocProvider<RandomNumberBloc>(
      create: (_) => RandomNumberBloc(),
      child: RandomNumberView(),
    );
  }
}

In order to use Hydrated BLoC, we have to replace our regular Bloc with HydratedBloc or use the mixin HydratedMixin, and this is what our RandomNumberBloc looks like:

class RandomNumberBloc extends HydratedBloc<RandomNumberEvent, int> {
  RandomNumberBloc() : super(0) {
  on<GenerateRandomNumber>((event, emit) =>
        emit(_fetchRandomNumber(maxNumber: event.max, minNumber: event.min)));
    on<ResetRandomNumber>((event, emit) => emit(0));
  }

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

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

  int _fetchRandomNumber({required int maxNumber, required int minNumber}) {
    return minNumber + Random().nextInt(maxNumber - minNumber + 1);
  }
}

And for our event class, we have just two events. One for generating a random number and the other for resetting the generated random number:

abstract class RandomNumberEvent {}

class GenerateRandomNumber extends RandomNumberEvent {
  final int max;
  final int min;

  GenerateRandomNumber({required this.max, required this.min});
}


class ResetRandomNumber extends RandomNumberEvent {}

Here we do not have a state class, and this is because our state is simply an integer. Hence, this is less complexity, which would demand writing a full class for it.

Storing and retrieving state

In order to store our data, we need to serialize it for more complex models. By this, I mean we have to convert it to JSON format. To achieve this, we have to override the fromJson and toJson methods in our bloc class, which extends HydratedBloc or uses the HydratedMixin.

When our state goes through our bloc class, it gets saved by default because Hydrated BLoC uses Hive under the hood to persist data. And whenever our app is restarted, it listens to our states and data ,which has been saved to our previous states so it won’t be lost.

App States And Data

Final thoughts

State persistence can be a necessity and provide a seamless and user friendly experience to users of our application.

Achieving this can be in order, and simple as possible using the hydratedBloc pattern as demonstrated above.

You can find the codebase of this application here.

: Full visibility into your web apps

LogRocket is a frontend application monitoring solution that lets you replay problems as if they happened in your own browser. Instead of guessing why errors happen, or asking users for screenshots and log dumps, LogRocket lets you replay the session to quickly understand what went wrong. It works perfectly with any app, regardless of framework, and has plugins to log additional context from Redux, Vuex, and @ngrx/store.

In addition to logging Redux actions and state, LogRocket records console logs, JavaScript errors, stacktraces, network requests/responses with headers + bodies, browser metadata, and custom logs. It also instruments the DOM to record the HTML and CSS on the page, recreating pixel-perfect videos of even the most complex single-page apps.

.
Eshiet Ekemini A graduate of University of Uyo and a tech enthusiast, Ekemini has been building for mobile for two years, with a particular focus on Kotlin and Flutter.

Leave a Reply