How to Use Extensions to Elevate Your Bloc State Functionality

Harnessing Extensions for Cleaner Flutter Code

·

5 min read

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 the ListBloc using BlocProvider.

  • The page displays a ListView with items obtained from the ListBloc.

  • 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.

Did you find this article valuable?

Support Henry's blog by becoming a sponsor. Any amount is appreciated!