Bloc 8.1.1+ — Part 2: Refactoring Hydrated Bloc Code For Functionality Gains With Code Road Construction

The Code Breaker
6 min readJan 25, 2023

--

This article shows how effortless it is to use Code Road Construction with Hydrated Bloc. The last article gave an in-depth look at the reasons for enum to classes type Code Road Construction while working with Hydrated Bloc. In contrast, this article shows the steps to easily refactor the code to gain functionality using the Freezed package. There are benefits to using the Freezed package, and one of the big ones is that it allows for easily using immutable coding practices.

In this article you will learn:

  1. How to clean the code using the Freezed package.
  2. The proper steps for refactoring with Code Road Construction from an Enum to Classes**.

** The last article discussed using Code Road Construction with Hydrated Bloc. That article gave a deep dive into the code and offered a complete explanation, including all of the necessary boilerplate code. Surprise, all of that boilerplate code was optional! We stated numerous times in that article that you could follow this article to learn a better way to refactor the code. Well, here it is!

I hope that you enjoy this journey!

  • The link to the previous article is here.
  • We are starting fresh with a complete understanding of the Hydrated Bloc requirements! Please find the starting code here. This version is not the completed, refactored code but the original one with the enum.

Refactoring from Enums to Classes

Import The Packages

  1. Start by adding these packages to the pubspec.yaml dependencies:
build_runner: ^2.1.4
freezed: ^2.3.2
freezed_annotation: ^2.2.0
json_annotation: ^4.8.0

And this package to the pubspec.yaml dev_dependencies:

json_serializable: ^6.6.0

2. Refactor the code into different files. This refactoring cleans the code and makes dealing with the Freezed package much easier due to its specific formatting.

  1. item.dart
  2. main.dart
  3. subject_bloc.dart
  4. subject_event.dart
  5. subject_state.dart

item.dart

enum Item {
magical(7),
sharp(5);

const Item(this.damage);
final int damage;
}

main.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:hydrated_bloc_enum_to_interface_start_code/item.dart';
import 'package:hydrated_bloc_enum_to_interface_start_code/subject_bloc.dart';
import 'package:hydrated_bloc_enum_to_interface_start_code/subject_event.dart';
import 'package:hydrated_bloc_enum_to_interface_start_code/subject_state.dart';
import 'package:path_provider/path_provider.dart';

Future<void> main() async {
WidgetsFlutterBinding.ensureInitialized();

HydratedBloc.storage = await initializeHydratedStorage();
runApp(const DIProviderTree());
}

Future<HydratedStorage> initializeHydratedStorage() async {
return HydratedStorage.build(
storageDirectory: kIsWeb
? HydratedStorage.webStorageDirectory
: await getTemporaryDirectory(),
);
}

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

@override
Widget build(BuildContext context) {
return MultiBlocProvider(
providers: [
BlocProvider<SubjectBloc>(
create: (context) => SubjectBloc(),
),
],
child: const MyHomePage(),
);
}
}

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

@override
Widget build(BuildContext context) {
return MaterialApp(
home: Scaffold(
body: SizedBox(
width: double.infinity,
height: double.infinity,
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
crossAxisAlignment: CrossAxisAlignment.center,
children: const [
DisplayCircleAvatar(),
ChipsHolder(),
],
),
)),
);
}
}

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

@override
Widget build(BuildContext context) {
return Wrap(
children: Item.values.map((Item enumValue) {
return SubjectChip(item: enumValue);
}).toList());
}
}

class SubjectChip extends StatelessWidget {
const SubjectChip({super.key, required this.item});
final Item item;
@override
Widget build(BuildContext context) {
return BlocBuilder<SubjectBloc, SubjectState>(
builder: (context, state) {
return ActionChip(
backgroundColor:
state.selectedSubjects.contains(item) ? Colors.green : Colors.red,
label: Text(item.name),
onPressed: () {
context.read<SubjectBloc>().add(ModifySelectionSubjectEvent(item));
},
);
},
);
}
}

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

@override
Widget build(BuildContext context) {
return CircleAvatar(
radius: 100,
backgroundColor: Colors.blue,
child: Text(context
.watch<SubjectBloc>()
.state
.selectedSubjects
.fold(0, (sum, item) => sum + item.damage)
.toString()),
);
}
}

subject_bloc.dart

import 'package:hydrated_bloc/hydrated_bloc.dart';
import 'package:hydrated_bloc_enum_to_interface_start_code/subject_event.dart';
import 'package:hydrated_bloc_enum_to_interface_start_code/subject_state.dart';

class SubjectBloc extends HydratedBloc<SubjectEvent, SubjectState> {
SubjectBloc() : super(SubjectState.initial()) {
on<ModifySelectionSubjectEvent>((event, emit) {
if (state.selectedSubjects.contains(event.item)) {
return emit(SubjectState(
selectedSubjects: state.selectedSubjects..remove(event.item)));
} else {
return emit(SubjectState(
selectedSubjects: state.selectedSubjects..add(event.item)));
}
});
}

@override
SubjectState? fromJson(Map<String, dynamic> json) {
return SubjectState.fromJson(json);
}

@override
Map<String, dynamic>? toJson(SubjectState state) {
return state.toJson();
}
}

subject_event.dart

import 'package:hydrated_bloc_enum_to_interface_start_code/item.dart';

abstract class SubjectEvent {}

class ModifySelectionSubjectEvent extends SubjectEvent {
ModifySelectionSubjectEvent(this.item);
final Item item;
}

subject_state.dart

import 'package:hydrated_bloc_enum_to_interface_start_code/item.dart';

abstract class SubjectEvent {}

class ModifySelectionSubjectEvent extends SubjectEvent {
ModifySelectionSubjectEvent(this.item);
final Item item;
}

3. Comment out ALL of the code in item.dart and then add the following IMPORTANT CODE.

items.dart

  • Add the import statements.
  • Add the Item._() private constructor to ensure that you can add the custom .fromJson method into this class. If you don’t, there will be a problem using Freezed to format the code.
  • For refactoring, name the class the same as the original enum.
  • Inject the values of the enum into the superclass of the factory constructor (i.e. String name, int damage).
  • Add a factory-named constructor for each item of your enum (i.e. magic and sharp).
import 'package:freezed_annotation/freezed_annotation.dart';
import 'package:flutter/foundation.dart';
part 'item.freezed.dart';
part 'item.g.dart';

@freezed
class Item with _$Item {
// the Item._() allows custom functions to be added without problems.
// This allows for the fromJson used in the g.dart files
// without it there will be problems next.
const Item._();
const factory Item({required String name, required int damage}) = _Magical;

factory Item.fromJson(Map<String, dynamic> json) => _$ItemFromJson(json);
factory Item.magic() => const Item(name: 'magic', damage: 10);
factory Item.sharp() => const Item(name: 'sharp', damage: 5);
}

Then, run these lines in your console terminal

flutter pub get
flutter pub upgrade --major-versions
flutter pub outdated
flutter pub run build_runner build --delete-conflicting-outputs

Running this should generate the g.dart files needed.
There is an alternative code that you can enter, which is shorter, but sometimes generating g.dart files doesn’t correctly work when running the shorter version. To fix this, I run this full version for updating files.

Generating the g.dart files makes the compiler show errors in the main.dart and subject_state.dart

4. Deal with the loss of the enum by changing the code.

In main.dart:

/* CHANGE FROM:
class ChipsHolder extends StatelessWidget {
const ChipsHolder({
super.key,
});
@override
Widget build(BuildContext context) {
return Wrap(
children: Item.values.map((Item enumValue) {
return SubjectChip(item: enumValue);
}).toList());
}
} */

// TO:
class ChipsHolder extends StatelessWidget {
const ChipsHolder({
super.key,
});
@override
Widget build(BuildContext context) {
return Wrap(
children: [Item.magic(), Item.sharp()].map((Item enumValue) {
return SubjectChip(item: enumValue);
}).toList());
}
}

In subject_state.dart

/* CHANGE FROM:
factory SubjectState.fromJson(Map<String, dynamic> json) {
return SubjectState(
selectedSubjects: json[_key]
.map<Item>(
(e) => Item.values.firstWhere((element) => element.name == e))
.toList(),
);
} */

// TO:
factory SubjectState.fromJson(Map<String, dynamic> json) {
return SubjectState(
selectedSubjects:
json[_key].map<Item>((e) => Item.fromJson(e)).toList(),
);
}

That’s it; these four steps finish what needs doing!

You Are Done; however, Let’s Not Forget The Purpose And Test The Code

We have refactored our code to gain functionality; we want to change the variables of the items. So, let’s ensure that we can do that.

  1. To the Scaffold of main.dart
floatingActionButton: FloatingActionButton(
onPressed: () {
context
.read<SubjectBloc>()
.add(AddOneToAllSelectedSubjectsEvent());
},
),

2. To subject_event.dart

// Add

class AddOneToAllSelectedSubjectsEvent extends SubjectEvent {}

3. To subject_bloc.dart

Notice that we use copyWith because using the Freezed package allows us to work with immutable code, which has enormous benefits!

// Add

on<AddOneToAllSelectedSubjectsEvent>((event, emit) {
return emit(SubjectState(
selectedSubjects: state.selectedSubjects
.map((e) => e.copyWith(damage: e.damage + 1))
.toList()));
});

4. Restart the app, press the magic and sharp chips and then press the FloatingActionButton to ensure that your variables change.

As a bonus, this code employs immutable coding practices, which sets up well for using the Functional Programming Paradigm and helps us prevent hard-to-catch bugs.

A Bit Of A Comparison

We shy away from complexity because it usually adds to the codebase we must maintain. Remember how we discussed in the last article that code is a liability?

How did we do with all of this refactoring compared to the starting code and completed code of the last article?

-The Original code with an enum and no functionality: 154 lines of code.

-This article with Freezed: 197 lines of code.

-The last article without Freezed: 246 lines of code.

Yes, judging work by lines of code usually is a bad policy, but there is a 20% difference, and it is much easier to read and follow. Plus, saving time by using the four simple steps is fantastic.

Conclusion

This was the next in a series of articles that I am writing about working with Hydrated Bloc and dealing with Code Road Construction. The last article gave an in-depth look at the reasons for Code Road Construction while working with Hydrated Bloc; this article shows the steps used to simplify and easily refactor code to get functionality gains using the Freezed package. There are benefits to using the Freezed package, and one of the bigger ones is that it allows using immutable coding practices.

find the complete code here.

--

--