nav font loadernav font loader
Go BackGo Back
Flutter

How to boost your Flutter’s Blocs with polymorphism

Both the BLoC library and Dart’s type system are extremely powerful. Check out this example of a BLoC with type polymorphism, which abstracts behavior in a comfortable and intuitive way!

We share our knowledge and experience in our blog | Somnio Software

by Mauricio Mordecki

6 min read · Aug 15, 2022

The BLoC paradigm has been the go-to solution for stream-based state management, with a plethora of mechanisms which abstract away the boilerplate of stream-based state management. But as we know, the more we understand the underlying mechanisms of our designs, the more meaningful our abstractions can be. In this post I will give one example of a parametric BLoC, which will help us centralize the business logic surrounding the download, upload, and validation of data. Hopefully this example will illuminate the mechanisms which allow us to do such type-based wizardry.

Introduction

The creation of abstractions is at the core of programming. These abstractions become our concepts, the objects we manipulate across our codebase, and even their own manipulations. The better the abstractions, the more elegant the solutions, and the less code we need overall. And as we know, less code means less bugs, less development time, and less maintenance costs.

Content

Our example situation

Let’s suppose we are working on an application for an animal shelter, which should manage the shelter’s animal registry. A user should be able to register animals to be adopted. Each different species has different attributes which are relevant, for example a snake’s length, a dog’s favorite food, or a cat’s number of legs. So, as expected, each different species will have a different form to fill, with different data.

So our design team comes to the conclusion that there should be a page tailored for each species that can be registered, with the possibility of new species being added in the future. These pages will be used to register new animals, and also to update already existing animals’ information.

The abstractions

So, some abstractions are pretty easy to spot. We will probably need a class for each animal species. We will also need a class for each species’ page’s BLoC. Maybe, if you’re cheeky, you have already spotted the possibility of an abstract class Animal. But the abstract BLoC will abstract away a core functionality of our application. The downloading, modification, and uploading of arbitrary types. Well, not arbitrary. Of species. So let’s get coding.

First the abstract class Animal

abstract class Animal {
const Animal();

bool get isValid;
}

We added a simple getter isValid to make sure we never try to upload a invalid animal to the database.

And now let’s check out our dog class.

class Dog extends Animal {
const Dog({
  this.height = 0,
  this.weight = 0,
  this.numberOfTails = 1,
});

final double height;
final double weight;
final int numberOfTails;

Dog copyWith({
  double? height,
  double? weight,
  int? numberOfTails,
}) {
  return Dog(
    height: height ?? this.height,
    weight: weight ?? this.weight,
    numberOfTails: numberOfTails ?? this.numberOfTails,
  );
}

@override
bool get isValid => height > 0 && weight > 0 && numberOfTails < 2;
}



As we can see, dogs have a height, a weight, and a number of tails. We set conditions on what constitutes a valid upload. For now, registration of mutant animals will not be done through the app, because special considerations need to be taken in such cases.

And now that we have all of our models in place, comes the time of the abstract BLoC.

The parametric AnimalBloc

So let’s think about this a little bit. Our BLoC will have to be able to download an animal from a database, upload it to a database, and change the animal it has saved. That translates to three events.

abstract class AnimalEvent {
const AnimalEvent();
}

class DownloadRequestedEvent extends AnimalEvent {}

class UploadRequestedEvent extends AnimalEvent {}

class DataChangedEvent<A extends Animal> extends AnimalEvent {
const DataChangedEvent(this.animal);

final A animal;
}

And our BLoC state should have an animal

class AnimalState<A extends Animal> {
const AnimalState({required this.animal});

final A animal;

AnimalState<A> copyWith({A? animal}) {
  return AnimalState(animal: animal ?? this.animal);
}
}

For now everything seems pretty typical, other than the type parameters, which are kind of off-putting, but later we’ll understand the role they play.

So now yes, here comes the time we have been waiting for. The actual parametric BLoC. First I’ll show you the code, then we can walk through it together.

abstract class AnimalBloc<A extends Animal> extends Bloc<AnimalEvent, AnimalState<A>> {
AnimalBloc({
  required Downloader<A> downloader,
  required Uploader<A> uploader,
  required A initialAnimal,
})  : _downloader = downloader,
      _uploader = uploader,
      super(AnimalState<A>(animal: initialAnimal)) {
  on<DownloadRequestedEvent>((event, emit) async {
    final newAnimal = await _downloader();
    emit(state.copyWith(animal: newAnimal));
  });

  on<UploadRequestedEvent>((event, emit) async {
    if (state.animal.isValid) {
      await _uploader(state.animal);
    }
  });

  on<DataChangedEvent<A>>((event, emit) {
    emit(state.copyWith(animal: event.animal));
  });
}

final Downloader<A> _downloader;
final Uploader<A> _uploader;
}


typedef Downloader<A> = Future<A> Function();

typedef Uploader<A> = Future<void> Function(A);

Yep. That’s it. As you can see, we have three event handlers, one for each event we defined previously. One more thing to note are the typedefs at the bottom, they are just for more comfortable function types. _downlaoder and _uploader will be the repository functions to handle the data, and in the BLoC pattern they are part of the repository layer, so they are beyond our scope.

Now let’s look at how we could use this to implement the BLoC for registering a dog. First our page will have input fields for the dog's height, weight, and number of tails, so when those change we will let the BLoC know with these events

abstract class DogEvent extends AnimalEvent {
const DogEvent();
}

class DogHeightChangedEvent extends DogEvent {
const DogHeightChangedEvent(this.height);

final double height;
}

class DogWeightChangedEvent extends DogEvent {
const DogWeightChangedEvent(this.weight);

final double weight;
}

class DogTailsChangedEvent extends DogEvent {
const DogTailsChangedEvent(this.tails);

final int tails;
}

The DogBloc inheriting the AnimalBloc

And now we can build the DogBloc event handlers using the AnimalBloc events, and we can also create new handlers without littering the AnimalBloc with dog-specific events, and emit new AnimalState<Dog> which will blissfully interact with the AnimalBloc logic.

class DogBloc extends AnimalBloc<Dog> {
DogBloc({
  required Downloader<Dog> downloader,
  required Uploader<Dog> uploader,
}) : super(
        downloader: downloader,
        uploader: uploader,
        initialAnimal: const Dog.new(),
      ) {
  on<DogHeightChangedEvent>((event, emit) {
    add(DataChangedEvent(state.animal.copyWith(height: event.height)));
  });

  on<DogWeightChangedEvent>((event, emit) {
    add(DataChangedEvent(state.animal.copyWith(weight: event.weight)));
  });

  on<DogTailsChangedEvent>((event, emit) {
    add(DataChangedEvent(state.animal.copyWith(numberOfTails: event.tails)));
  });
}
}

As you can see, the DogBloc is fully implemented and ready to be plugged to the UI with very little code, and absolutely no uploading nor downloading logic. That is all handled either in the uploader or downloader functions for Dog specific code, or on the AnimalBloc for animal generic specifications.

Well, that was an abstract parametric BLoC. It might look like a lot of code, but the strength of this method and example is the work that’s to come when adding new species and new features. Adding a new species handling BLoC will be as simple as defining its class extending Animal, and making the needed animal manipulation events. But wait, there’s more…

Parametric Widgets

The last wisp of magic I have to share today is something that could only happen after all of this parametric work has been done. A reusable parametric widget.

class UploadAnimalButton<AB extends AnimalBloc> extends StatelessWidget {
const UploadAnimalButton({this.child, Key? key}) : super(key: key);

final Widget? child;

@override
Widget build(BuildContext context) {
  return GestureDetector(
    onTap: () => context.read<AB>().add(UploadRequestedEvent()),
    child: child,
  );
}
}

Any widget can be wrapped with this one and function as the upload animal button, for any animal. This way the actual BLoC functionality is in one place, while different UI widgets can be used for different animals.

Conclusion

I really do hope that this post has shed a light on the power of type parameters not only in Dart and Flutter but in all typed languages.

I’d love to hear your ideas on different responsibilities which can be abstracted away with a nice abstract BLoC. Maybe a REST BLoC whose type parametrization extends a Jsonable mixin? Blow me away with your ingeniousness, I’m sure you’ll have no problems with that.


by Mauricio Mordecki

6 min read · Aug 15, 2022

Mauricio Mordecki is a Software Developer at Somnio Software. He finds joy in programming with a team, developing a project and achieving an excelent product.

Mobile app development
Webdevelopement

Let’s create successful apps together!

ˑ
Select...