Bloc 8.0.0+ — Why I Love Hydrated Bloc and Why You Probably Should Too

The Code Breaker
8 min readSep 30, 2022

--

Read this if you are interested in quickly refactoring your code to persist state.

Who doesn’t like simple? After all, isn’t that one of programming’s most beloved mantras — Keep It Simple? The goodness of using flutter_bloc in your code for state management comes with many benefits including freedom from the technical debt associated with StatefulWidgets AND is the perfect opportunity for easily persisting state using hydrated_bloc. For me using the Hydrated Bloc package is almost as easy as falling off of a log and is a no-brainer. I hope to open your mind to using it too. First, I will show the steps that demonstrate how easy it is to refactor your code to use bloc, and then hydrate it. Persisting state data has never been easier!

Learning Intentions

  • You will learn about Bloc and Hydrated Bloc.
  • You will learn to refactor code to use Bloc.
  • You will learn to refactor Bloc code to use Hydrated Bloc.
  • You will learn to easily persist data.

The Beginning GitHub Code

GitHub — Goosebay111/app_enum_example

The Stock Standard Code (no BloC)

First, let us understand the app that we will convert to bloc.

An image of the working app

Let’s imagine that this app keeps track of an item for a video game. The item can give the user numerical bonuses depending on whether it is magical, sharp or heavy. The item can have any combination of the three (i.e. magical and/or sharp and/or heavy). By pressing one of the bonus options, a numerical value can be added or removed from the total which is displayed inside of the blue circle.

Let’s look at the code using StatefulWidget (w/o BLoC)

I present the code in different files. It might not make sense to break up such simple code, but it is better to have it compartmentalised before adding to and changing the code.

main.dart

void main() => runApp(const MyApp());
class MyApp extends StatelessWidget {
const MyApp({super.key});
@override
Widget build(BuildContext context) {
return const MaterialApp(
home: MyHomePage(),
);
}
}

home_page.dart

import 'package:app_enum_example/enums/item_enum.dart';
import 'package:flutter/material.dart';
class MyHomePage extends StatefulWidget {
const MyHomePage({super.key});
@override
State<MyHomePage> createState() => _MyHomePageState();
}
class _MyHomePageState extends State<MyHomePage> {
List<Item> items = [Item.magical, Item.sharp, Item.heavy];
List<Item> selected = [];
int _getSum(item) =>
item.fold(0, (previousValue, element) => previousValue + element.bonus());
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: const Text('No Bloc: Hydrating Your App'),
),
body: Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
CircleAvatar(
radius: 100,
child: Text(
'${_getSum(selected)}',
style: const TextStyle(fontSize: 50),
),
),
Wrap(
children: [
...items
.map((item) => ActionChip(
backgroundColor: selected.contains(item)
? Colors.blue
: Colors.grey,
label: Text(item.name.toString()),
onPressed: () {
setState(() {
selected.contains(item)
? selected.remove(item)
: selected.add(item);
});
},
))
.toList(),
],
),
],
),
),
);
}
}

item_enum.dart

enum Item {
magical(7),
sharp(5),
heavy(3);
const Item(this.damage);
final int damage;
int bonus() => damage;
}

For reference, a purple code diagram shows where the widgets appear in the app.

Purple diagram that displays widget functionality

For ease of understanding, the important state and state modifying code above is truncated and displayed below. The selected var is a List that contains any number of enum items. The selected var is the focal point of the state. The CircleAvatar widget has a Text widget that accesses the numerical values by passing the selected var to the _getSum method, which is a .fold method that tabulates and returns the total of the passed values. Lastly, the state can be modified by pressing an ActionChip widget in unison with setState. Pressing an ActionChip widget also toggles its colour.

Because we are not using bloc, we require using a StatefulWidget and its setState method to update the data when the state changes.

...
// state
List<Item> selected = [];
// adds all of the state values together and returns the sum
int _getSum(item) =>
item.fold(0, (previousValue, element) => previousValue + element.bonus());
...
...
// displays the sum total of the state.
CircleAvatar(
radius: 100,
child: Text(
'${_getSum(selected)}',
style: const TextStyle(fontSize: 50),
),
),
...
...
// modifies the state.
onPressed: () {
setState(() {
selected.contains(item)
? selected.remove(item)
: selected.add(item);
});
},
...

Part 1. Refactoring to BLoC State Management

Add the following libraries to the pub spec.yaml file.

pubspec.yaml

dependencies:
flutter:
sdk: flutter
  flutter_bloc: ^8.1.1
equatable: ^2.0.5

STEP 1) Converting State Management to BLoC

Because we are working with flutter_bloc, it is best practice to work with events, states and blocs.

  1. Add these three files to the lib folder of your project:

bloc_events.dart

abstract class SelectedEvents {}
class AddItem extends SelectedEvents {
AddItem(this.item);
final Item item;
}
class RemoveItem extends SelectedEvents {
RemoveItem(this.item);
final Item item;
}

bloc_state.dart

class SelectedState {
SelectedState({required this.item, required this.selectedItems});
final List<Item> item;
final List<Item> selectedItems;
}
class InitialState extends SelectedState {
InitialState()
: super(item: [Item.magical, Item.sharp, Item.heavy], selectedItems: []);
}

Notice that we begin by implementing our initial state here, where a List of Items is passed to the superclass (SelectedState) that contains all of the elements that we want to be included in the Wrap widget, and an empty List of items which we use for our displayed value.

item_bloc.dart

class SelectedBloc extends Bloc<SelectedEvents, SelectedState> {
SelectedBloc() : super(InitialState()) {
on<AddItem>(((event, emit) => emit(SelectedState(
item: state.item,
selectedItems: state.selectedItems..add(event.item),
))));
on<RemoveItem>(((event, emit) => emit(SelectedState(
item: state.item,
selectedItems: state.selectedItems..remove(event.item),
))));
}
}

The emit method is used to change, monitor and update the state. Ultimately, the emit command indicates to the StatelessWidgets to redraw. Notice that ..add and ..remove have double dots. The double dots are necessary here.

STEP 2) Add BlocProvider to main.dart

main.dart

void main() 
=> runApp(const MyApp());
class MyApp extends StatelessWidget {
const MyApp({Key? key}) : super(key: key);
@override
Widget build(BuildContext context) {
// add bloc provider
return BlocProvider(
create: (context) => SelectedBloc(),
child: const MaterialApp(
home: MyHomePage(),
),
);
}
}

STEP 3) Modify the HomePage

  1. Change the StatefulWidget to StatelssWidget,
  2. Add a BlocBuilder
  3. Change code to work with the State with a BlocProvider in home_page.dart

home_page.dart

**// 1) Convert from StateFullWidget to StatelessWidget**
class MyHomePage extends StatelessWidget {
const MyHomePage({Key? key}) : super(key: key);
int _getSum(item) =>
item.fold(0, (previousValue, element) => previousValue + element.bonus());
@override
Widget build(BuildContext context) {
**// 2) Add bloc builder**
return BlocBuilder<SelectedBloc, SelectedState>(
builder: (context, state) {
return Scaffold(
appBar: AppBar(
title: const Text('App Enum with BLoC'),
),
body: Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: <Widget>[
CircleAvatar(
radius: 100,
child: Text(
_getSum(state.selectedItems).toString(),
style: const TextStyle(fontSize: 50),
),
),
Wrap(
children: [
...Item.values
.map((e) => Padding(
padding:
const EdgeInsets.symmetric(horizontal: 1),
child: ActionChip(
backgroundColor: state.selectedItems.contains(e)
? Colors.blue
: null,
label: Text(
e.name.toString(),
),
onPressed: () {
**// 3) Word with the state with BlocProvider**
state.selectedItems.contains(e)
? BlocProvider.of<SelectedBloc>(context)
.add(RemoveItem(e))
: BlocProvider.of<SelectedBloc>(context)
.add(AddItem(e));
},
),
))
.toList(),
],
),
],
),
),
);
},
);
}
}

STEP 4) Completely Stop and Restart Your App

Now the state will update without using a StatefulWidget.

The GitHub for the full code Part 1

GitHub — Goosebay111/app_enum_with_bloc

Part 2. Refactoring to Hydrated Bloc

Now let’s get on to the exciting bit! This is how easy it is to make your state persistent!

Obviously, this is where you would begin if you already used flutter_bloc in your apps. Making it even easier on you!

Add the following libraries to the pub spec.yaml file.

pubspec.yaml

hydrated_bloc: ^8.1.0
path_provider: ^2.0.11

STEP 1) Modify bloc_state.dart

We are now adding the elements needed for the Hydrated Bloc. In a nutshell, hydrated bloc uses toJson and fromJson to make state persistent. Usually, apps already use toJson and fromJson functionality for database interactions. Being so common means that hydrated bloc is even easier to use for most of us. Just a special note for the code below, I used a getItemType helper function to convert the returned string to an enum. Without converting to an enum in this way, I experienced a hard-to-detect bug.

In bloc_state.dart do the steps below. Steps 1–3 add code, while step 4 removes and consolidates code. Steps 3-4 are not always necessary depending on how you initialise your bloc state. I add them here because they clean up the code.

  1. Add toJson method.
  2. Add fromJson factory.
  3. Add an initial state factory.
  4. Remove the initial state class.

bloc_state.dart

import 'package:new_item_experiment/enums/item_enum.dart';
class SelectedState {
SelectedState({required this.item, required this.selectedItems});
final List<Item> item;
final List<Item> selectedItems;
);
// 1) add toJson method
Map<String, dynamic> toJson() => {
'items': item.map((e) => e.toString()).toList(),
'selectedItems': selectedItems.map((e) => e.toString()).toList(),
};
// 2) add fromJson factory
factory SelectedState.fromJson(Map<String, dynamic> json) => SelectedState(
item: (json['items'].map<Item>((e) => getItemType(e)).toList()),
selectedItems:
json['selectedItems'].map<Item>((e) => getItemType(e)).toList(),
);
// 3) add an initial state factory
factory SelectedState.initial() => SelectedState(
item: Item.values,
selectedItems: [],
);
}
// 4) remove the initial state class, now that it is handled in factory (step 3)
// class InitialState extends SelectedState {
// InitialState()
// : super(item: [Item.magical, Item.sharp, Item.heavy], selectedItems: []);
// }
Item getItemType(String item) {
switch (item) {
case 'Item.magical':
return Item.magical;
case 'Item.sharp':
return Item.sharp;
case 'Item.heavy':
return Item.heavy;
default:
return Item.heavy;
}
}

STEP 2) Modify the ItemBloc to Hydrated Bloc, Fix the Super injection and Add Overrides

The previous changes create an error because we have removed the InitialState class. Follow these steps in the item_bloc.dart file.

  1. Change Bloc to Hydrated Bloc.
  2. Change what is injected to the super class to SelectedState.initial( ),
  3. Add fromJson to the overrides.
  4. Add toJson to the overrides.

item_bloc.dart

// 1) Change Bloc to Hydrated Bloc
class SelectedBloc extends HydratedBloc<SelectedEvents, SelectedState> {
// 2) Change to use the new state source
SelectedBloc() : super(SelectedState.initial()) {
on<AddItem>(((event, emit) => emit(SelectedState(
item: state.item,
selectedItems: state.selectedItems..add(event.item),
))));
on<RemoveItem>(((event, emit) => emit(SelectedState(
item: state.item,
selectedItems: state.selectedItems..remove(event.item),
))));
}
// 3) add the fromJson override
@override
SelectedState? fromJson(Map<String, dynamic> json) {
return SelectedState.fromJson(json);
}
// 4) add the toJson override
@override
Map<String, dynamic>? toJson(SelectedState state) {
return state.toJson();
}
}

STEP 3) Set Up the main.dart File for the Hydrated Bloc Data Storage

  1. Update the entire main.dart file

main.dart

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

class DIMultiWidgetSubTree extends StatelessWidget {
const DIMultiWidgetSubTree({
super.key,
});

@override
Widget build(BuildContext context) {
return RepositoryProvider(
create: (context) => SelectedBloc(), child: const MyApp());
}
}

class MyApp extends StatelessWidget {
const MyApp({Key? key}) : super(key: key);

@override
Widget build(BuildContext context) {
return BlocProvider(
create: (context) => SelectedBloc(),
child: const MaterialApp(
home: MyHomePage(),
),
);
}
}

It is in this code that Hydrated Bloc does all of the heavy lifting. Whenever a state change is triggered, a storage variable is stored in persistent memory.

STEP 4) Completely Stop and Restart Your App

Now the state is persistent. Try clicking on the magical button to highlight it, then press the hot reload button. It remains highlighted!

Conclusion

That’s it, we now have fully functioning persistent memory that basically comes for free when you use BLoC. We have just seen the simple steps for converting from Stateful to BloC state management, and then we saw how to make it persistent with Hydrated BloC.

Final GitHub Code

GitHub — Goosebay111/new_item_experiment

Next in the Series

Find out how to get more functionality out of the code discussed in this article here.

--

--