Hydrated Bloc in Flutter: Simplifying State Management

Tejaswini Dev
7 min readJul 24, 2023

--

Hydrated Bloc in Flutter enables state persistence, saving and restoring the app’s state across restarts for a seamless user experience.

Introduction

Flutter, Google’s open-source UI software development kit, has rapidly gained popularity among developers for its ability to build beautiful, fast, and responsive native applications across various platforms. As applications grow in complexity, managing the app’s state becomes a crucial aspect of development. In this context, Bloc (Business Logic Component) emerges as a popular state management pattern in the Flutter community. To optimize the Bloc pattern further, developers have adopted the “Hydrated Bloc” approach, bringing efficiency and performance improvements to state management.

Understanding Bloc Pattern in Flutter

The Bloc pattern is a powerful state management solution that separates the business logic of an application from its UI. It helps in maintaining a clear, unidirectional data flow and makes code more maintainable and testable. In the traditional Bloc pattern, every time the app starts or restarts, the state is reset, leading to the loss of previously stored data. This is where Hydrated Bloc comes into play, providing a solution to persist and restore the application’s state even after restarting the app.

Stay Refreshed and Hydrated with Hydrated Bloc in Flutter!

What is Hydrated Bloc?

Hydrated Bloc is an extension of the original Bloc pattern in Flutter. It introduces the ability to save the current state of the application to persistent storage (e.g., disk, database, or shared preferences) and restore it when the app is reopened. This feature is especially useful for applications that require users to log in or maintain preferences across sessions.

How does Hydrated Bloc work?

Hydrated Bloc adds an essential layer to the original Bloc pattern — the HydratedBloc. It is designed to automatically persist the state and rehydrate it when the app is restarted. The process involves serializing the state object into a JSON representation and storing it in a chosen storage medium. Upon restarting the app, the stored JSON is retrieved, deserialized, and the original state is reconstructed.

The primary difference between a regular Bloc and a Hydrated Bloc is in how the state is initialized. In a regular Bloc, the state is created anew each time the app starts. In contrast, a Hydrated Bloc checks for a stored state during its initialization phase. If a previous state exists, it is used to initialize the application’s state. Otherwise, the state is initialized with default values, just like a regular Bloc.

Benefits of Hydrated Bloc

  • Persistent State: By leveraging Hydrated Bloc, applications can save and restore their state across app restarts. This is crucial for scenarios where maintaining user sessions, preferences, or application settings is necessary.
  • Improved User Experience: Users can seamlessly return to where they left off without losing their progress or data. This leads to a more satisfying and user-friendly experience.
  • Simplified State Management: Hydrated Bloc builds upon the already well-structured Bloc pattern, providing an elegant way to incorporate persistent state management without significant code changes.
  • Error Resilience: In cases where an application encounters unexpected errors, having a persistent state can help in gracefully recovering the app’s state without losing critical data.
  • Easy Integration: Hydrated Bloc is designed to be easy to integrate into existing projects. It doesn’t require drastic architectural changes and can be implemented without much effort.

Now, Let’s dive into the code and understand it step-by-step to cache the bloc state, resulting in the following outcome:

Preserving State with Hydrated Bloc!

Step 1: Add dependencies

dependencies:
flutter:
sdk: flutter
flutter_bloc: ^8.1.3
hydrated_bloc: ^9.1.2
equatable: ^2.0.5

Step 2: Initialize Hydrated Storage

Future<void> main() async {
WidgetsFlutterBinding.ensureInitialized();
HydratedBloc.storage = await HydratedStorage.build(
storageDirectory: await getApplicationDocumentsDirectory(),
);
runApp(App());
}

Step 3: Create Bloc State

class WeatherState extends Equatable {
final WeatherStateStatus status;
final WeatherDataModel? weatherModel;

const WeatherState({
required this.status,
required this.weatherModel,
});

WeatherState copyWith({
WeatherStateStatus? status,
WeatherDataModel? weatherModel,
}) =>
WeatherState(
status: status ?? this.status,
weatherModel: weatherModel ?? this.weatherModel,
);

@override
List<Object?> get props => [status, weatherModel];

// ...
}
  • The first step is to define the state class, which typically represents the data that your Bloc manages. It should be a simple Dart class and can contain any data required to represent the state of your application feature or screen.
  • It’s essential to make the state class immutable, meaning once an instance of the state is created, its values should not be changed directly. Instead, a new instance with updated values should be created to represent a new state.
  • A common approach to making the state class immutable is by declaring all its properties as final.
  • The state class should extend the Equatable class provided by the equatable package. This package makes it easier to compare instances of the state class for equality.

Step 4: Create Bloc Events

part of 'weather_bloc.dart';

abstract class WeatherEvent extends Equatable {
const WeatherEvent();

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

class FetchWeatherDetails extends WeatherEvent {
final String? location;

const FetchWeatherDetails({required this.location});
}
  • Define the events which are represented as classes and it can trigger state changes in the application.

Step 5: Create Bloc

class WeatherBloc extends Bloc<WeatherEvent, WeatherState> with HydratedMixin {
WeatherBloc()
: super(const WeatherState(
status: WeatherStateStatus.loading,
weatherModel: null,
)) {
on<FetchWeatherDetails>((event, emit) async {
await _onWeatherApiCallEvent(event, emit);
});
}
// ...
}
  • Create a Bloc class that extends HydratedBloc or use the HydratedBloc mixin to handle the business logic and state transitions based on the incoming events.

Step 6: Implement Bloc Logic

class WeatherBloc extends Bloc<WeatherEvent, WeatherState> with HydratedMixin {
WeatherBloc()
: super(const WeatherState(
status: WeatherStateStatus.loading,
weatherModel: null,
)) {
on<FetchWeatherDetails>((event, emit) async {
await _onWeatherApiCallEvent(event, emit);
});
}

Future<void> _onWeatherApiCallEvent(
FetchWeatherDetails event,
Emitter<WeatherState> emit,
) async {
try {
emit(state.copyWith(
status: (event.location != null && event.location!.isNotEmpty)
? WeatherStateStatus.searching
: WeatherStateStatus.loading));
if (event.location != null &&
event.location!.isNotEmpty &&
event.location!.length > 2) {
WeatherDataModel? weatherModel =
await RestAPIService().fetchCurrentWeatherDetails(
location: event.location ?? "",
);
(weatherModel != null)
? emit(state.copyWith(
weatherModel: weatherModel,
status: WeatherStateStatus.loaded,
))
: emit(state.copyWith(
status: WeatherStateStatus.noData, weatherModel: null));
} else {
emit(state.copyWith(
status: WeatherStateStatus.loading, weatherModel: null));
}
} catch (e) {
emit(
state.copyWith(status: WeatherStateStatus.error, weatherModel: null));
}
}
// ...
}
  • The bloc starts with an initial state representing the loading status.
  • When a “FetchWeatherDetails” event is received, it triggers an API call to get weather data based on the provided location.
  • The bloc updates its state according to the result of the API call, showing statuses like “loading,” “searching,” “loaded,” “no data,” or “error.”

Step 7: Implement fromJson and toJson methods in the Bloc to enable state persistence.

class WeatherBloc extends Bloc<WeatherEvent, WeatherState> with HydratedMixin {
WeatherBloc()
: super(const WeatherState(
status: WeatherStateStatus.loading,
weatherModel: null,
)) {
on<FetchWeatherDetails>((event, emit) async {
await _onWeatherApiCallEvent(event, emit);
});
}

// ...

@override
WeatherState? fromJson(Map<String, dynamic> json) {
try {
final weather = WeatherDataModel.fromJson(json);
return WeatherState(
status: WeatherStateStatus.loaded,
weatherModel: weather,
);
} catch (e) {
return null;
}
}

@override
Map<String, dynamic>? toJson(WeatherState state) {
if (state.status == WeatherStateStatus.loaded) {
return state.weatherModel?.toJson();
} else {
return null;
}
}
}

fromJson

  • The fromJson method is responsible for converting the serialized JSON data, which represents the saved state, back into a usable state object. This process is known as deserialization.
  • When the app is reopened or resumed, the HydratedBloc attempts to retrieve the stored JSON representation of the state from the persistent storage.
  • The retrieved JSON data is then passed to the fromJson method, which should be implemented in the custom HydratedBloc class.
  • Inside the fromJson method, the developer defines the logic to convert the JSON data back into the corresponding state object used by the Bloc.
  • This state object is then used to initialize the Bloc’s state, and the application can continue with the restored state.

toJson

  • The toJson method is responsible for converting the current state object into a JSON representation that can be easily stored in persistent storage.
  • Before the application is closed or paused, the HydratedBloc calls the toJson method to get the JSON representation of the current state.
  • The toJson method should be implemented in the custom HydratedBloc class and should return a JSON-serializable representation of the state.
  • This JSON representation is then saved to the persistent storage so that it can be retrieved later to restore the state.

Conclusion

Hydrated Bloc is a valuable extension of the traditional Bloc pattern in Flutter, offering developers the ability to persist and restore application state across app restarts. By adopting Hydrated Bloc, developers can enhance the user experience by providing continuity, maintaining user sessions, and preserving preferences or application settings. With its straightforward integration and efficiency, Hydrated Bloc has become a preferred choice for state management in complex Flutter applications.

As Flutter continues to evolve, it is essential for developers to stay up-to-date with the latest state management patterns and practices, making use of tools like Hydrated Bloc to create robust, performant, and user-friendly applications.

🎉 Hey there! Want to explore the full code? Just hop on over to our github.com/tejaswini-dev-techie/weather_app 🚀 If you find it helpful and awesome, don’t hold back — give it a star ⭐️ to show your love! And if this article makes your day brighter and more productive, let’s celebrate with a big round of applause 👏, and why not share it with your pals too? Sharing is caring! Let’s learn and grow together! 😊

Thanks for reading this article, Happy coding!

--

--

Tejaswini Dev

Passionate and dynamic individual with a deep interest in the world of technology