4

Flutter Bloc Library Tutorial (1.0.0 STABLE) – Reactive State Management

State management is needed by every app. No matter the size of your project, you need to store and do something with all the data present in your app. If you're building something small, you might be able to pull it off with StatefulWidgets. As the difficulty of the project starts to grow, you have to start looking for more maintainable solutions...

The flutter_bloc package is a reactive and predictable way to manage your app's state. This package takes everything that's awesome about the BLoC (business logic component) pattern and puts it into a simple-to-use library with amazing tooling. After many months of development, the Bloc package has arrived at its first stable version - 1.0.0.

The project we will build

Complex topics such as state management are best understood on real-ish projects. No, we're not going to build yet another counter app. Instead, we're going to create a weather app with master and detail screens, a fake repository to get the weather data from and the app will also have visually appealing error handling.

You came here to learn the Bloc library, so I definitely don't want to bore you with building mundane Flutter UIs. Get the starter project with the basic widgets and simple classes in place below:

When we implement the flutter_bloc library into this project, we will have an app where we can search for the current weather in a certain city. Then, we can choose to see the "details" which will take us to the detail page. To keep it simple, it will display just one additional field - temperature in Fahrenheit.​​​​

Starting out

First, we surely need to add all the dependencies needed for this project. Since we will work with a fake repository which will generate random weather data, we don't need to add any http package. All we need is flutter_bloc and equatable which are from the same author, Felix Angelov, and they work well together.​​​​

pubspec.yaml

dependencies:
  flutter:
    sdk: flutter
  flutter_bloc: ^1.0.0
  equatable: ^0.6.1

Using Equatable

The equatable package adds simple value equality to Dart classes which, by default, support only referential equality. The starter project already contains a Weather model class, so let's extend it with Equatable to make it into more of a data class. This requires us to return all the Weather class fields from the overridden props property.

data/model/weather.dart

import 'package:equatable/equatable.dart';
import 'package:meta/meta.dart';

class Weather extends Equatable {
  final String cityName;
  final double temperatureCelsius;
  final double temperatureFarenheit;

  Weather({
    @required this.cityName,
    @required this.temperatureCelsius,
    this.temperatureFarenheit,
  });

  @override
  List<Object> get props => [
        cityName,
        temperatureCelsius,
        temperatureFarenheit,
      ];
}

Close look at the repository

The starter project contains a fully implemented WeatherRepository. While it's definitely not bloc-specific (any state management pattern will benefit from a centralized data point), the repository is important to understand.

A repository is the single source of truth of data for the Bloc. While the app might have multiple data sources, such as network and cache, the Bloc always communicates with the repository. Then, it's the job of the repository to decide whether to fetch new data from the network, or to get it from local cache.

The WeatherRepository will fetch the "master" weather and also "detailed" weather. The only difference between them is that the latter one will have the temperatureFarenheit field populated.

data/weather_repository.dart

abstract class WeatherRepository {
  Future<Weather> fetchWeather(String cityName);
  Future<Weather> fetchDetailedWeather(String cityName);
}

If you want to see a fully-fledged app with repositories built with Clean Architecture and TDD, check out the tutorial series below:

Our FakeWeatherRepository implementation will simply generate random temperatures and simulate network errors and delays.

data/weather_repository.dart

class FakeWeatherRepository implements WeatherRepository {
  double cachedTempCelsius;

  @override
  Future<Weather> fetchWeather(String cityName) {
    // Simulate network delay
    return Future.delayed(
      Duration(seconds: 1),
      () {
        final random = Random();

        // Simulate some network error
        if (random.nextBool()) {
          throw NetworkError();
        }

        // Since we're inside a fake repository, we need to cache the temperature
        // in order to have the same one returned for the detailed weather
        cachedTempCelsius = 20 + random.nextInt(15) + random.nextDouble();

        // Return "fetched" weather
        return Weather(
          cityName: cityName,
          // Temperature between 20 and 35.99
          temperatureCelsius: cachedTempCelsius,
        );
      },
    );
  }

  @override
  Future<Weather> fetchDetailedWeather(String cityName) {
    return Future.delayed(
      Duration(seconds: 1),
      () {
        return Weather(
          cityName: cityName,
          temperatureCelsius: cachedTempCelsius,
          temperatureFarenheit: cachedTempCelsius * 1.8 + 32,
        );
      },
    );
  }
}

class NetworkError extends Error {}

The Bloc

You can think of a Bloc as if it was a pipe with one input and one output. That's what makes it so powerful and yet predictable. The stuff that goes into the pipe are events, the Bloc determines what to do based on the incoming event and outputs state.​​​​​​​​​​

Events come in, state comes out.

The UI layer (a.k.a. widgets), send an event, for example, when a button is clicked. The UI also receives states and rebuilds itself accordingly.

This means we need at least 3 classes to make the Bloc happen - weather_event​​, weather_state and, of course, weather_bloc. Thankfully, we don't need to create all of this manually because Bloc comes with an amazing tooling for developers. Just install the VS Code extension or the IntelliJ plugin and let's roll!

Creating files

At least in VS Code, right click on the lib folder and select the option below:

Bloc: New Bloc

You'll be presented with a popup. Input the name "weather" and choose ​"yes" to use the Equatable package. Equatable is very much needed, since certain features of Bloc depend on value equality.

Give the Bloc files and classes a name

Make sure to use Equatable

You should now have 4 files inside a folder called bloc. The top one is just a barrel file, exporting all of the other ones for easier imports.

Generated Bloc files

Events

Before writing any logic, you need to know about the use cases which the Bloc will support. There are two such use cases for our weather app - get the "master" weather and get the detailed weather . If we were managing state with a ChangeNotifier, we'd represent these with methods. However, since we're using Bloc, the use cases will be represented as event classes.​​

bloc/weather_event.dart

import 'package:equatable/equatable.dart';

abstract class WeatherEvent extends Equatable {
  const WeatherEvent();
}

class GetWeather extends WeatherEvent {
  final String cityName;

  const GetWeather(this.cityName);

  @override
  List<Object> get props => [cityName];
}

class GetDetailedWeather extends WeatherEvent {
  final String cityName;

  const GetDetailedWeather(this.cityName);

  @override
  List<Object> get props => [cityName];
}

When these events are added into the Bloc​​ from the UI, we will run logic to fetch appropriate weather data. Holding true to the spirit of the Bloc pattern, this data will be outputted through the other end of the proverbial pipe in the form of states.

States

Reactive state management patterns, such as Bloc, can be daunting. They require you to change the way you think about the flow of data in your app. We're all used to getting a returned value from a method. It's very direct - you call a method and get the value in the same place.

As you could already see on the Bloc diagram, there are no direct return values in the Bloc pattern. Instead, there are states which have to hold everything needed to rebuild the UI. Also, the places of adding events and listening to states are separate.​​​​

The simplest way to create states is to ask the following: "In how many different states can the UI appear?". Surely, we initially want to display only the input TextField. When the user searches for a city, we want to show a loading indicator and then subsequently the actual weather. Also, should an error happen, we want to notify the user about it. Therefore, it seems that there are 4 distinct states in which the app can be. All of them will be represented by a class.

bloc/weather_state.dart

import 'package:equatable/equatable.dart';

import '../data/model/weather.dart';

abstract class WeatherState extends Equatable {
  const WeatherState();
}

class WeatherInitial extends WeatherState {
  const WeatherInitial();
  @override
  List<Object> get props => [];
}

class WeatherLoading extends WeatherState {
  const WeatherLoading();
  @override
  List<Object> get props => [];
}

class WeatherLoaded extends WeatherState {
  final Weather weather;
  const WeatherLoaded(this.weather);
  @override
  List<Object> get props => [weather];
}

class WeatherError extends WeatherState {
  final String message;
  const WeatherError(this.message);
  @override
  List<Object> get props => [message];
}

Business Logic Component

Having the inputs and outputs of the "pipe" in place, let's finally implement the stuff that's going on in the pipe. Formally, this part is known as the business logic. In the case of our weather app, we will simply fetch data from the abstract WeatherRepository - using an abstract class instead of the FakeWeatherRepository implementation directly allows us to seamlessly swap between multiple different implementations. This comes in handy for testing purposes.

Every Bloc must override at least two members - the initialState property and the mapEventToState method, which is an asynchronous generator. It's no surprise that Bloc works with Streams under the hood and inside the method, we will literally map events to states by emitting them to the Stream<WeatherState> using the yield keyword.

bloc/weather_bloc.dart

import 'dart:async';

import 'package:bloc/bloc.dart';

import './bloc.dart';
import '../data/weather_repository.dart';

class WeatherBloc extends Bloc<WeatherEvent, WeatherState> {
  final WeatherRepository repository;

  WeatherBloc(this.repository);

  @override
  WeatherState get initialState => WeatherInitial();

  @override
  Stream<WeatherState> mapEventToState(
    WeatherEvent event,
  ) async* {
    // Emitting a state from the asynchronous generator
    yield WeatherLoading();
    // Branching the executed logic by checking the event type
    if (event is GetWeather) {
      // Emit either Loaded or Error
      try {
        final weather = await repository.fetchWeather(event.cityName);
        yield WeatherLoaded(weather);
      } on NetworkError {
        yield WeatherError("Couldn't fetch weather. Is the device online?");
      }
    } else if (event is GetDetailedWeather) {
      // Code duplication 😢 to keep the code simple for the tutorial...
      try {
        final weather = await repository.fetchDetailedWeather(event.cityName);
        yield WeatherLoaded(weather);
      } on NetworkError {
        yield WeatherError("Couldn't fetch weather. Is the device online?");
      }
    }
  }
}

Using the Bloc from the UI

The UI of our weather app consists of two pages - search and detail. The starter project contains all the widget code needed to build the UI. Our task is to make the widgets do something useful by adding events to the Bloc and by reacting and rebuilding according to states emitted from the Bloc.

First, we're going to need to get the WeatherBloc instance to the WeatherSearchPage. There are multiple ways to do this and we're going to take the best one by providing the bloc down the widget tree with a BlocProvider.

This is the same approach as if you were using a ChangeNotifier together with the provider package. In fact, the BlocProvider class is built on top of the regular Provider.

main.dart

void main() => runApp(MyApp());

class MyApp extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      title: 'Weather App',
      home: BlocProvider(
        builder: (context) => WeatherBloc(FakeWeatherRepository()),
        child: WeatherSearchPage(),
      ),
    );
  }
}

WeatherSearchPage

The starter project's WeatherSearchPage has all of the widget building methods already prepared. Notice that they kind of correspond with the states which the Bloc can output, hence the method names buildInitialInput or buildLoading.

BlocBuilder

As you could already see when we were implementing the WeatherBloc, the states are outputted through a Stream. Sure, we could react to those states using a regular old StreamBuilder, but the flutter_bloc library has a better tool for the job - a BlocBuilder. Let's place it into the Scaffold's body and call appropriate sub-build methods based on the emitted state.

pages/weather_search_page.dart

class WeatherSearchPage extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: Text("Weather Search"),
      ),
      body: Container(
        padding: EdgeInsets.symmetric(vertical: 16),
        alignment: Alignment.center,
        child: BlocBuilder<WeatherBloc, WeatherState>(
          builder: (context, state) {
            if (state is WeatherInitial) {
              return buildInitialInput();
            } else if (state is WeatherLoading) {
              return buildLoading();
            } else if (state is WeatherLoaded) {
              return buildColumnWithData(context, state.weather);
            } else if (state is WeatherError) {
              return buildInitialInput();
            }
          },
        ),
      ),
    );
  }
...

So far so good. The builder method is just stepping through all the possible states and returning appropriate UI. However, notice the last else if clause. It doesn't seem right, does it? Sure, we can show the initial input when an error occurs, but the WeatherError state also contains an error message and it would be good to display it to the user using a snackbar... 

Showing a snackbar is a "side effect" and those have nothing to do in a builder...

Builder is the place for rebuilding the UI and it has to be a "pure function". That is, it only returns a Widget and doesn't do anything else.

BlocListener

Performing one time actions like showing a snackbar is the domain of the BlocListener. Unlike the builder, it isn't a pure function. In fact, it returns void. The reason for this separation of responsibilities between the builder (builds UI) and listener (performs actions) is that while the listener is guaranteed to run only once per state change. You surely wouldn't want to show the same snackbar multiple times to the user, after all.

Listener is the place for logging, showing Snackbars, navigating, etc. It is guaranteed to run only once per state change.

Let's wrap the builder inside a BlocListener widget and show a SnackBar on WeatherError.

pages/weather_search_page.dart

...
child: BlocListener<WeatherBloc, WeatherState>(
  listener: (context, state) {
    if (state is WeatherError) {
      Scaffold.of(context).showSnackBar(
        SnackBar(
          content: Text(state.message),
        ),
      );
    }
  },
  child: BlocBuilder<WeatherBloc, WeatherState>(
    builder: (context, state) {
      ...
    },
  ),
),
...

Great! We can now build the UI in reaction to the incoming states. As you know though, executing the logic present inside the Bloc requires a "trigger" in the form of events.

Adding an Event to the Bloc

The starter project comes with a CityInputField widget which has a submitCityName method. It takes in a cityName parameter from the TextField. Adding (or triggering) events couldn't be more simple:

pages/weather_search_page.dart

class CityInputField extends StatelessWidget {
  ...
  void submitCityName(BuildContext context, String cityName) {
    // Get the Bloc using the BlocProvider
    // False positive lint warning, safe to ignore until it gets fixed...
    final weatherBloc = BlocProvider.of<WeatherBloc>(context);
    // Initiate getting the weather
    weatherBloc.add(GetWeather(cityName));
  }
}

Now we're officially done with the WeatherSearchPage. We still have the WeatherDetailPage to implement though and there are some tricky parts when it comes to navigating between routes using Bloc.

WeatherDetailPage

We navigate to the WeatherDetailPage upon tapping the "see details" button. Now we have to figure out how to get the already existing WeatherBloc instance to the new route since as you already know, this Bloc is responsible both for getting the "master" and "detail" data.

The "details" button

There are two ways to do this:

  1. Inside main.dartwrap the whole MaterialApp in a BlocProvider. This will make the provided Bloc available globally across all routes.
  2. Inside weather_search_page.dart, "re-provide" the Bloc to WeatherDetailPage when building a new route.

We're going to choose the second option because the WeatherBloc isn't meant to be available globally. Sure, we have only 2 pages, but imagine you're building a complex app with 15 pages. Having a bunch of unnecessary global Blocs isn't going to be cool then, is it?

BlocProvider.value

To provide the same instance of the Bloc, we're going to use a special constructor BlocProvider.value. Unlike the default constructor which has a builder method, this one isn't going to automatically dispose and close the Stream present inside the Bloc.

Use the BlocProvider.value constructor only to provide Blocs already instantiated inside the regular constructor with a builder.

Inside the WeatherSearchPage's buildColumnWithData change the usual navigation code...

pages/weather_search_page.dart

RaisedButton(
  child: Text('See Details'),
  color: Colors.lightBlue[100],
  onPressed: () {
    Navigator.of(context).push(MaterialPageRoute(
      builder: (_) => WeatherDetailPage(
        masterWeather: weather,
      ),
    ));
  },
),

... into the following, which makes sure that the already existing Bloc instance provided from main.dart will be available even inside the new route.

pages/weather_search_page.dart

RaisedButton(
  child: Text('See Details'),
  color: Colors.lightBlue[100],
  onPressed: () {
    Navigator.of(context).push(MaterialPageRoute(
      builder: (_) => BlocProvider.value(
        value: BlocProvider.of<WeatherBloc>(context),
        child: WeatherDetailPage(
          masterWeather: weather,
        ),
      ),
    ));
  },
),

Adding an event ASAP

In contrast with the search page, there is no user input in the detail page. This means that there isn't any button onPressed method from where we can add an event to the Bloc. Still, we want to trigger the GetDetailedWeather event and pass it the city name from the masterWeather field which is populated when navigating from the search page. How and when are we going to add this event to the Bloc then?

Obviously, it's best to do it as soon as possible and also, to do it only once per WeatherDetailPage lifetime. The build method is immediately out of play because it can possibly run many times over when rebuilding the UI. Instead, we're going to utilize the didChangeDependencies method of a State object.

didChangeDependencies runs before build and most importantly, doesn't run on rebuilds.

This requires us to change the WeatherDetailPage to be a StatefulWidget and the code will look like this:

pages/weather_detail_page.dart

class WeatherDetailPage extends StatefulWidget {
  final Weather masterWeather;

  const WeatherDetailPage({
    Key key,
    @required this.masterWeather,
  }) : super(key: key);

  @override
  _WeatherDetailPageState createState() => _WeatherDetailPageState();
}

class _WeatherDetailPageState extends State<WeatherDetailPage> {
  @override
  void didChangeDependencies() {
    super.didChangeDependencies();
    // Immediately trigger the event
    BlocProvider.of<WeatherBloc>(context)
      ..add(GetDetailedWeather(widget.masterWeather.cityName));
  }
...

With that, we can now finally add a BlocBuilder even to this page and the app will be complete!

pages/weather_detail_page.dart

@override
Widget build(BuildContext context) {
  return Scaffold(
    appBar: AppBar(
      title: Text("Weather Detail"),
    ),
    body: Container(
      padding: EdgeInsets.symmetric(vertical: 16),
      alignment: Alignment.center,
      child: BlocBuilder<WeatherBloc, WeatherState>(
        builder: (context, state) {
          if (state is WeatherLoading) {
            return buildLoading();
          } else if (state is WeatherLoaded) {
            return buildColumnWithData(context, state.weather);
          }
        },
      ),
    ),
  );
}

What you learned

You now know how to use the powerful flutter_bloc library in your projects. Unlike other state management patterns, Bloc forces you to do things just one way and the right way. With its one-way data flow using events and states, Bloc is a sure way to bring more structure, extensibility and, most importantly, maintainability to your apps.​​​​​​

Matej Rešetár
 

Matej is an app developer with a knack for teaching others. If he's not programming, making tutorials or doing other business, he's mostly working out, listening to audiobooks and taking cold showers.

  • Brian says:

    hey Reso, would you mind checking this state management solution package https://pub.dev/packages/states_rebuilder . I’m really interested in how you’d compare it to Bloc. Great content man…keep it up.

  • Владимир says:

    Why add extends Equitable if it is not used anywhere else?

  • Marno van Niekerk says:

    Hi. Thank you very much for the updated tutorial.
    I have just one question regarding the loadingState. I have a DropdownMenu which waits for the data, which have the initialLoading, but then I need to click a button, but If I click the button, the state change to the loadingState again to save to the DB, and then the DataLoaded state disappears and so does the DropdownMenu.

    How would you go about handling the states for this scenario, without letting the DropdownMenu disappear, while still have a loading circle when the data is loading?

  • Kaleem Ahmed says:

    It does not appear the started project git reference is right. If I follow the link on this page for starter project it points to the final project. Please check.

  • >