Skip to content
mreysei · Michael Reyes
Theme

Understanding the Flutter BLoC Pattern

By Michael ReyesPublished on Updated on 5 min readCategory: Technology

The Flutter logo next to the text “BLoC Pattern Flutter” on a blue background.

In this article, I’ll explain how the BLoC Pattern in Flutter works.
I have a Flutter boilerplate in the LeanMind Github, which might help you follow along with this post.

But first, let’s make a small introduction.
This pattern was presented at the 2018 Dart Conference 📹, so it’s relatively new.
The goal of Paolo Soares and Cong Hu (both Google engineers) was to make code reusable between mobile applications built with Flutter and web applications built with Angular Dart.

The main concept is that there should be an intermediate layer between the views and the model.
This layer will manage states and handle data depending on the events received from the view.

I drew this diagram 🎨

Diagram of the BLoC pattern: the screen sends events to the BLoC and is rebuilt with each state; the BLoC reads and writes data through a repository or use case. LoadDataEvent leads to LoadingState and RecoveredDataState; UpdateDataEvent leads to LoadingState and then ErrorState or SuccessState.

From now on, all the code we’ll see will be directly related to the diagram, and we’ll call it “profile.”

We’ll create the minimal necessary code, divided into four files:

Code
// profile_bloc.dart
class ProfileBloc extends Bloc<ProfileEvent, ProfileState> {
  @override
  ProfileState get initialState => throw UnimplementedError();

  @override
  Stream<ProfileState> mapEventToState(ProfileEvent event) async* {
    throw UnimplementedError();
  }
}

// profile_screen.dart
class ProfileScreen extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    throw UnimplementedError();
  }
}

// profile_event.dart
abstract class ProfileEvent {}

// profile_state.dart
abstract class ProfileState {}

These are the packages we’ll use; add them to your pubspec.yaml:

  • flutter_bloc: to create the bloc and later use it in the view
  • built_value: responsible for generating immutable objects, among other things
  • build_runner: automatically generates the necessary code

Knowing this, let’s start by creating the initial state. We’ll call it InitialState, and it won’t have any properties.

Code
// profile_state.dart
abstract class InitialState extends ProfileState implements Built<InitialState, InitialStateBuilder> {
  InitialState._();
  factory InitialState([void Function(InitialStateBuilder) updates]) = _$InitialState;
}

Hmm… maybe this needs a bit of explanation:

💭 It’s an abstract class called InitialState that extends our previously created ProfileState and implements Built of itself. Inside, we create a private constructor to make it inaccessible from outside, and a factory that receives an optional function to build the state. Later, we’ll see how this works when parameters are needed.

After that, add the following line of code right after the imports:

Code
// profile_state.dart
part 'profile_state.g.dart';

Run the command flutter pub run build_runner watch --delete-conflicting-outputs, which executes the build_runner package and auto-generates the code required to make our InitialState work.

Perfect! 👏
We’ve created our first state — but it’s not used yet, so let’s tell our bloc to use it:

Code
// profile_bloc.dart
@override
ProfileState get initialState => InitialState();

Now that we have this state, we need to connect the view to the bloc. How? With the BlocBuilder (from flutter_bloc):

Code
// profile_screen.dart
@override
Widget build(BuildContext context) {
  return BlocBuilder<ProfileBloc, ProfileState>(
    builder: (BuildContext context, ProfileState state) {
      return Container(
        color: Colors.blue[900],
        child: Center(child: CircularProgressIndicator()),
      );
    },
  );
}

In this way, every time the state changes, the view will rebuild and display the appropriate content based on the state. For now, since we only have one state, it will show a spinner in the center of the screen.

Let’s create the event that will request data from the repository. We’ll call this event LoadDataEvent:

Code
// profile_event.dart
part 'profile_event.g.dart';

abstract class ProfileEvent {}

abstract class LoadDataEvent extends ProfileEvent implements Built<LoadDataEvent, LoadDataEventBuilder> {
  LoadDataEvent._();
  factory LoadDataEvent([void Function(LoadDataEventBuilder) updates]) = _$LoadDataEvent;
}

Now that we have the event, we’ll trigger it when the state is InitialState:

Code
// profile_screen.dart
class ProfileScreen extends StatefulWidget {
  @override
  _ProfileScreenState createState() => _ProfileScreenState();
}

class _ProfileScreenState extends State<ProfileScreen> {
  ProfileBloc bloc;

  @override
  Widget build(BuildContext context) {
    return BlocBuilder<ProfileBloc, ProfileState>(
      builder: (BuildContext context, ProfileState state) {
        if (state is InitialState) {
          bloc = BlocProvider.of<ProfileBloc>(context);
          bloc.add(LoadDataEvent());
        }
        return buildSpinner();
      },
    );
  }

  Widget buildSpinner() {
    return Container(
      color: Colors.blue[900],
      child: Center(child: CircularProgressIndicator()),
    );
  }

  @override
  void dispose() {
    bloc?.close();
    super.dispose();
  }
}

Here are some key points:

  • First, we created a conditional so that when the state is InitialState, it triggers the LoadDataEvent.
  • Second, we instantiated the bloc using BlocProvider.of<ProfileBloc>(context).
  • Finally, we changed from StatelessWidget to StatefulWidget, since the bloc instance should persist throughout the widget, and must be closed when the view is disposed.

Now, in the bloc, we’ll add our event in the mapEventToState function and request the data from the repository like this:

Code
// profile_bloc.dart
@override
Stream<ProfileState> mapEventToState(ProfileEvent event) async* {
  if (event is LoadDataEvent){
    String name = await getName();
  }
}

// This simulates the repository
Future<String> getName() async {
  await Future.delayed(Duration(seconds: 2));
  return "Any Name";
}

Okay… we have the data, but what now?
We’ll have to create the next states. According to our diagram, when LoadDataEvent is triggered, it should transition to LoadingState, request data from the repository, and then move to RecoveredDataState. Let’s create these two new states!

Code
// profile_state.dart
abstract class LoadingState extends ProfileState implements Built<LoadingState, LoadingStateBuilder> {
  LoadingState._();
  factory LoadingState([void Function(LoadingStateBuilder) updates]) = _$LoadingState;
}

Now, let’s create RecoveredDataState, which will have properties:

Code
// profile_state.dart
abstract class RecoveredDataState extends ProfileState implements Built<RecoveredDataState, RecoveredDataStateBuilder> {
  String get name;

  RecoveredDataState._();
  factory RecoveredDataState([void Function(RecoveredDataStateBuilder) updates]) = _$RecoveredDataState;
}

Nice, we now have three states! 😄
Now let’s go back to the bloc and finish the LoadDataEvent:

Code
// profile_bloc.dart
@override
Stream<ProfileState> mapEventToState(ProfileEvent event) async* {
  if (event is LoadDataEvent) {
    yield LoadingState();
    String name = await getName();
    yield RecoveredDataState((builder) => builder..name = name);
  }
}

yield? async*? You probably noticed them earlier, but what are they? I won’t go into detail now — I recommend reading this article, which explains it clearly.

Now, about RecoveredDataState: we pass the name using the builder that was auto-generated in the state. Suppose we also have a surname — how would we pass it? Like this:

Code
yield RecoveredDataState((builder) => builder
  ..name = name
  ..surname = surname
);

Or equivalently:

Code
yield RecoveredDataState((builder) {
  builder.name = name;
  builder.surname = surname;
  return builder;
});

Let’s move on to the view. We’re sending two new states, but the view won’t react yet because we haven’t handled them:

Code
// profile_screen.dart
@override
Widget build(BuildContext context) {
  return BlocBuilder<ProfileBloc, ProfileState>(
    builder: (BuildContext context, ProfileState state) {
      if (state is InitialState) {
        bloc = BlocProvider.of<ProfileBloc>(context);
        bloc.add(LoadDataEvent());
      }
      if (state is RecoveredDataState) {
        return buildName(state.name);
      }
      return buildSpinner();
    },
  );
}

Widget buildName(String name) {
  return Container(
    color: Colors.grey[300],
    child: Center(
      child: Text(name, style: TextStyle(fontSize: 16)),
    ),
  );
}

Now we can see that in any state a spinner will appear, except in RecoveredDataState, which means the name (state.name) will be displayed.

To complete the diagram, we would change what’s shown in RecoveredDataState to a TextField with the name and a RaisedButton (or similar). When pressed, it would trigger a new event with the updated TextField value, and the bloc would handle this event, transitioning through ErrorState ❌ or SuccessState ✔. However, with what we’ve seen so far, it’s enough to understand that the view is built based on the states, and the states change in response to the events.

I hope reading this post helped you understand it better, and I encourage you to complete the diagram.

Thanks a lot for reading! 📖

This article was first published in Spanish on the Lean Mind blog: BLoC Pattern con Flutter. This version has been translated using AI, so please keep in mind that coherence may be lost or there may be some unusual phrasing. Thank you very much for your understanding.