How to Use Extensions to Elevate Your Bloc State Functionality
Harnessing Extensions for Cleaner Flutter Code
Introduction
Extension methods add functionality to existing libraries and there're many examples about this in a lot of articles. Common uses are for shortening buildcontext methods.
Example, Theme.of(context).colorScheme
becomes context.theme.colorScheme
with the help of the BuildContext extension.
BuildContext Extension
extension BuildContextExtension on BuildContext {
ThemeData get theme => Theme.of(this);
}
This principle can also be applied to our blocs in order to encapsulate repeated functionality. For example;
ListState
@freezed
class ListState with _$ListState {
const factory ListState({
@Default([]) List<String> items,
}) = _ListState;
}
The above freezed
bloc state just contains a list of items that we're going to be working with, same as below.
ListEvent
@freezed
class ListEvent with _$ListEvent {
const factory ListEvent.itemAdded() = _ItemAdded;
const factory ListEvent.itemRemoved(int index) = _ItemRemoved;
}
And finally;
ListBloc
class ListBloc extends Bloc<ListEvent, ListState> {
ListBloc() : super(const _ListState()) {
on<_ItemAdded>(_onItemAdded);
on<_ItemRemoved>(_onItemRemoved);
}
FutureOr<void> _onItemAdded(_ItemAdded event, Emitter<ListState> emit) {
final item = "Item ${state.items.length + 1}";
emit(state.copyWith(items: [...state.items, item]));
}
FutureOr<void> _onItemRemoved(_ItemRemoved event, Emitter<ListState> emit) {
final items = [...state.items]..removeAt(event.index);
emit(state.copyWith(items: items));
}
}
I'm doing something very simple here;
When
_onItemAdded
is added, a new item is automatically generated and added to the list.When
_onItemRemoved
is called, the item at the specified index is removed.
ListPage
@RoutePage()
class ListPage extends StatelessWidget implements AutoRouteWrapper {
@override
Widget wrappedRoute(BuildContext context) {
return BlocProvider<ListBloc>(
create: (context) {
return ListBloc();
},
child: this,
);
}
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(title: const Text("List Page")),
body: SizedBox.expand(
child: BlocBuilder<ListBloc, ListState>(
builder: (context, listState) {
final ListState(:items) = listState;
return ListView.builder(
itemCount: items.length,
itemBuilder: (context, index) {
final item = items.elementAt(index);
return Dismissible(
key: Key(item),
onDismissed: (direction) {
context.read<ListBloc>().add(ListEvent.itemRemoved(index));
},
child: ListTile(title: Text(item)),
);
},
);
},
),
),
floatingActionButton: FloatingActionButton(
onPressed: () {
context.read<ListBloc>().add(const ListEvent.itemAdded());
},
backgroundColor: Colors.green,
child: const Icon(Icons.add),
),
);
}
}
Implementation Details
AutoRoute and
wrappedRoute
is used to provide theListBloc
usingBlocProvider
.The page displays a
ListView
with items obtained from theListBloc
.Each item is displayed in a
Dismissible
widget, allowing removal on swipe.At the bottom, a
FloatingActionButton
triggers the addition of a new item
Getting the mean
If tasked to get the mean of the items we would probably go about it this way.
First we add a new field to the ListState
@freezed
class ListState with _$ListState {
const factory ListState({
@Default([]) List<String> items,
@Default([]) List<String> oddItems,
@Default(0.0) double mean,
}) = _ListState;
}
FutureOr<void> _onItemAdded(_ItemAdded event, Emitter<ListState> emit) {
// Generate a new item with a label based on the current number of items.
final item = "Item ${state.items.length + 1}";
// Calculate the sum of numeric items in the current list state.
final sum = state.items.map(int.parse).fold(0, (a, b) => a + b);
// Calculate the mean value of numeric items or set to 0.0 if the list is empty.
final mean = state.items.isEmpty ? 0.0 : sum / state.items.length.toDouble();
// Emit the updated list state with the new item and calculated mean.
emit(state.copyWith(items: [...state.items, item], mean: mean));
}
The _onItemAdded
event now calculates the mean value of the numeric items in the current list state in addition to adding the new item. The mean is computed by converting each string to an integer, summing them up, and dividing by the total number of items.
The same would have to be repeated for the _onItemRemoved
event;
FutureOr<void> _onItemRemoved(_ItemRemoved event, Emitter<ListState> emit) {
// Create a copy of the current items list with the specified item removed.
final items = [...state.items]..removeAt(event.index);
// Calculate the sum of numeric items in the updated list state.
final sum = state.items.map(int.parse).fold(0, (a, b) => a + b);
// Calculate the mean value of numeric items or set to 0.0 if the list is empty.
final mean = state.items.isEmpty ? 0.0 : sum / state.items.length.toDouble();
// Emit the updated list state with the removed item and recalculated mean.
emit(state.copyWith(items: items, mean: mean));
}
The ListPage is then refactored to include this
return ListView.builder(
itemCount: items.length,
itemBuilder: (context, index) {
final item = items.elementAt(index);
return Column(
children: [
Text("Mean: ${listState.mean}"),
Dismissible(
key: Key(item),
onDismissed: (direction) {
context
.read<ListBloc>()
.add(ListEvent.itemRemoved(index));
},
child: ListTile(title: Text(item)),
),
],
);
},
);
The problem
Every future functionality that updates the items would also need to have an implementation to calculate the mean since we would need an up to date value on every state change.
ListExtension
extension ListExtension on ListState {
/// Gets the total number of items in the list.
int get itemCount => items.length;
/// Calculates the sum of numeric items in the list.
int get sum => items.map(int.parse).fold(0, (a, b) => a + b);
/// Calculates the mean value of numeric items in the list.
double get mean => items.isEmpty ? 0 : sum / itemCount.toDouble();
}
This extension on ListState
, is on the ListState
class and it's the solution . It provides convenient methods to help calculate the mean based on the items in the ListState
. This way repeated functionality is encapsulated and we have better code organization.
Now, the mean field @Default(0.0) double mean
can be removed and we can revert the changes done to the _onItemAdded
and _onItemRemoved
back to these.
FutureOr<void> _onItemAdded(_ItemAdded event, Emitter<ListState> emit) {
final item = "Item ${state.items.length + 1}";
// Emit a new state with the updated list
emit(state.copyWith(items: [...state.items, item]));
}
FutureOr<void> _onItemRemoved(_ItemRemoved event, Emitter<ListState> emit) {
final items = [...state.items]..removeAt(event.index);
emit(state.copyWith(items: items));
}
All that's left is to import the ListExtension
in the ListPage
to fix any errors.