8

Bloc Library (UPDATED) – Painless State Management for Flutter

Business Logic Component, otherwise known as Bloc, is arguably the best way to do state management on Flutter. Events come in, state comes out, and the flow of data simply has to be predictable this way.

Writing your apps using the Bloc pattern from scratch creates a lot of boilerplate code. That's why there is an amazing library which spares you from dealing with the intricacies of Bloc, like Streams and Sinks...

I've already covered the Bloc library a while ago. Back then it was just a cool library to use for enhancing your codebase, today, it will change the way you develop Flutter apps for the better.

Project & code editor setup

To get started with the Bloc library, you technically only need to add it to the pubspec.yaml file. There is another Dart package which fits just perfectly with Bloc, and that is Equatable (from the author of the Bloc library). Its role is to simplify value comparisons in Dart, since originally, Dart supports only referential equality.

pubspec.yaml

...
dependencies:
  flutter:
    sdk: flutter
  flutter_bloc: ^0.15.1
  equatable: ^0.2.6
...
This tutorial works with flutter_bloc version 0.15.1.
The latest update 0.17.0 changed one minor UI related thing (BlocProvider). The updated code is available from GitHub.

The Bloc library, however, also comes with extensions for both VS Code and IntelliJ! They provide code snippets and quick ways to create Bloc classes. Make sure you install them to get the best experience working with Bloc. This tutorial will be done with VS Code, but IntelliJ's plugin works in a similar way.

Starter UI

You cannot really have a Flutter app without a user interface. The app in this tutorial will pretend to be a simple weather forecast app which will just generate random temperatures.

Since this is a Bloc tutorial and not a tutorial on making UIs, you can copy the starter main.dart file from below. It may seem long, but most of the code is just making the widgets look pretty.

main.dart

import 'package:flutter/material.dart';

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

class MyApp extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      title: 'Flutter Demo',
      theme: ThemeData(
        primarySwatch: Colors.blue,
      ),
      home: WeatherPage(),
    );
  }
}

class WeatherPage extends StatefulWidget {
  WeatherPage({Key key}) : super(key: key);

  _WeatherPageState createState() => _WeatherPageState();
}

class _WeatherPageState extends State<WeatherPage> {
  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: Text("Fake Weather App"),
      ),
      body: Container(
        padding: EdgeInsets.symmetric(vertical: 16),
        alignment: Alignment.center,
        child: Column(
          mainAxisAlignment: MainAxisAlignment.spaceEvenly,
          children: <Widget>[
            Text(
              "City Name",
              style: TextStyle(
                fontSize: 40,
                fontWeight: FontWeight.w700,
              ),
            ),
            Text(
              "35 °C",
              style: TextStyle(fontSize: 80),
            ),
            CityInputField(),
          ],
        ),
      ),
    );
  }
}

class CityInputField extends StatefulWidget {
  const CityInputField({
    Key key,
  }) : super(key: key);

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

class _CityInputFieldState extends State<CityInputField> {
  @override
  Widget build(BuildContext context) {
    return Padding(
      padding: const EdgeInsets.symmetric(horizontal: 50),
      child: TextField(
        onSubmitted: submitCityName,
        textInputAction: TextInputAction.search,
        decoration: InputDecoration(
          hintText: "Enter a city",
          border: OutlineInputBorder(borderRadius: BorderRadius.circular(12)),
          suffixIcon: Icon(Icons.search),
        ),
      ),
    );
  }

  void submitCityName(String cityName) {
    // We will use the city name to search for the fake forecast
  }
}

We will work with the two Text widgets which display the searched for city and its temperature. Finally, we will use the CityInputField's TextField to obtain the city name from the user, for which we will perform the fake "search" - just randomly generate a temperature.

Business Logic Component

If you're not entirely familiar with the Bloc pattern, just realize it's nothing mysterious. All you have to remember is that the Bloc (Business Logic Component) is like a pipe.

Events (e.g user input values) come from one side, then the Bloc (the pipe) runs some code in reaction to the event coming in (retrieve weather data, in this case). Finally, the Bloc outputs State (package containing data). 

UI widgets then listen to new states outputted by the Bloc, and they get data from the State to display something visible to the user (in this case, the city name and the temperature for that city).

Creating a Bloc using the editor extension

Every Bloc needs multiple classes to function - Event, State, and the Bloc itself. With the VS Code extension, creating these classes & files is extremely quick.

  1. Right click on the lib folder
  2. Select Bloc: New Bloc from the menu.
  3. Give it a name "weather"
  4. Select "Yes" in the dialog to use the Equatable package.

Right-click menu in VS Code

This creates a new folder called bloc which contains a "barrel file" called bloc.dart which simply exports all of the other files. Then there is weather_bloc.dart, weather_event.dart and weather_state.dart.

Events - input for the Bloc

The first question you should to ask is "Which actions (a.k.a. events) from the UI will need to trigger some logic?"

In this app, there is only one such event - getting weather data for a certain city. Therefore,  you want to create a new class GetWeather for the event.

weather_event.dart

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

@immutable
abstract class WeatherEvent extends Equatable {
  WeatherEvent([List props = const []]) : super(props);
}

// The only event in this app is for getting the weather
class GetWeather extends WeatherEvent {
  final String cityName;

  // Equatable allows for a simple value equality in Dart.
  // All you need to do is to pass the class fields to the super constructor.
  GetWeather(this.cityName) : super([cityName]);
}

The file is generated with an abstract class WeatherEvent which extends Equatable for making value equality possible without all the hassle. All of the app-specific events have to extend this abstract base class.

States - output of the Bloc

The next step in making a Bloc is to ask "What will the Bloc output in reaction to the incoming events, so that the UI can react to the output?"

The first state is simply an "initial state" which is outputted at the start, before the user tries to get the weather for a particular city. 

Then, whenever the GetWeather event is dispatched the UI has to be notified of two things:

  1. The weather is being loaded (although only from a fake API)
  2. After some time, the weather is successfully loaded

weather_state.dart

import 'package:bloc_updated_prep/model/weather.dart';
import 'package:equatable/equatable.dart';
import 'package:meta/meta.dart';

@immutable
abstract class WeatherState extends Equatable {
  WeatherState([List props = const []]) : super(props);
}

class WeatherInitial extends WeatherState {}

class WeatherLoading extends WeatherState {}

// Only the WeatherLoaded event needs to contain data
class WeatherLoaded extends WeatherState {
  final Weather weather;

  WeatherLoaded(this.weather) : super([weather]);
}

Again, all of the weather states will inherit from the abstract base class WeatherState.

Weather is only a simple data class holding the city name and the temperature.

model/weather.dart

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

class Weather extends Equatable {
  final String cityName;
  final double temperature;

  Weather({
    @required this.cityName,
    @required this.temperature,
  }) : super([cityName, temperature]);
}

Bloc - where the logic happens

After you've carefully thought out what can come into the proverbial pipe (events), and what will come out (states), you can finally implement the "magical pipe" itself. Actually, instead of mysterious magic, there's only straightforward logic.

The WeatherBloc generated by using the VS Code extension comes preconfigured with the initial state getter and then a method mapEventToState. This method's name speaks for itself and this is the place for the logic.

weather_bloc.dart

import 'dart:async';
import 'dart:math';
import 'package:bloc/bloc.dart';
import 'package:bloc_updated_prep/model/weather.dart';
import './bloc.dart';

class WeatherBloc extends Bloc<WeatherEvent, WeatherState> {
  @override
  WeatherState get initialState => WeatherInitial();

  // Under the hood, the Bloc library works with regular Dart Streams.
  // The "async*" makes this method an async generator, which outputs a Stream
  @override
  Stream<WeatherState> mapEventToState(
    WeatherEvent event,
  ) async* {
    // Distinguish between different events, even though this app has only one
    if (event is GetWeather) {
      // Outputting a state from the asynchronous generator
      yield WeatherLoading();
      final weather = await _fetchWeatherFromFakeApi(event.cityName);
      yield WeatherLoaded(weather);
    }
  }

  Future<Weather> _fetchWeatherFromFakeApi(String cityName) {
    // Simulate network delay
    return Future.delayed(
      Duration(seconds: 1),
      () {
        return Weather(
          cityName: cityName,
          // Temperature between 20 and 35.99
          temperature: 20 + Random().nextInt(15) + Random().nextDouble(),
        );
      },
    );
  }
}

Using WeatherBloc in the UI

After implementing the Bloc itself, now it's time build the UI with it. There are 2 main things the UI can do with a Bloc:

  1. Dispatch events to the Bloc to initiate some logic
  2. React to states emitted from the Bloc to update the displayed UI

Updating the UI on state changes

Since we already know which states can come out of the Bloc, we can start off by changing the displayed widgets in reaction to the outputted state. We need to take care of the initial, loading, and loaded states.

This is possible with the BlocBuilder which rebuilds its widgets when a state is emitted.

main.dart

...
class _WeatherPageState extends State<WeatherPage> {
  // Instantiate the Bloc
  final weatherBloc = WeatherBloc();

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      ...
      body: Container(
        ...
        // BlocBuilder invokes the builder when new state is emitted.
        child: BlocBuilder(
          bloc: weatherBloc,
          // The builder function has to be a "pure function".
          // That is, it only returns a Widget and doesn't do anything else.
          builder: (BuildContext context, WeatherState state) {
            // Changing the UI based on the current state
            if (state is WeatherInitial) {
              return buildInitialInput();
            } else if (state is WeatherLoading) {
              return buildLoading();
            } else if (state is WeatherLoaded) {
              return buildColumnWithData(state.weather);
            }
          },
        ),
      ),
    );
  }

  Widget buildInitialInput() {
    return Center(
      child: CityInputField(),
    );
  }

  Widget buildLoading() {
    return Center(
      child: CircularProgressIndicator(),
    );
  }

  // Builds widgets from the starter UI with custom weather data
  Column buildColumnWithData(Weather weather) {
    return Column(
      mainAxisAlignment: MainAxisAlignment.spaceEvenly,
      children: <Widget>[
        Text(
          weather.cityName,
          style: TextStyle(
            fontSize: 40,
            fontWeight: FontWeight.w700,
          ),
        ),
        Text(
          // Display the temperature with 1 decimal place
          "${weather.temperature.toStringAsFixed(1)} °C",
          style: TextStyle(fontSize: 80),
        ),
        CityInputField(),
      ],
    );
  }

  @override
  void dispose() {
    super.dispose();
    // Don't forget to call dispose on the Bloc to close the Streams!
    weatherBloc.dispose();
  }
}
...

Initiate fetching the weather

Of course, updating the UI is not possible just by itself. The UI also needs to dispatch the GetWeather event, in order for the WeatherBloc to emit new states.

The problem is that the WeatherBloc instance is inside _WeatherPageState, but the TextField which handles the user input is inside its own separate widget CityInputFieldHow can we get the same Bloc instance over to another widget in the widget tree?

The answer is BlocProvider which is an InheritedWidget made specifically for passing Blocs down the widget tree. Since the CityInputField is nested inside the WeatherPage's Container, we need to wrap that Container with the BlocProvider.

main.dart

...
class _WeatherPageState extends State<WeatherPage> {
  // Instantiate the Bloc
  final weatherBloc = WeatherBloc();

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      ...
      // BlocProvider is an InheritedWidget for Blocs
      body: BlocProvider(
        // This bloc can now be accessed from CityInputField
        bloc: weatherBloc,
        child: Container(
          ...
        ),
      ),
    );
  }
  ...
}
...
class _CityInputFieldState extends State<CityInputField> {
  @override
  Widget build(BuildContext context) {
    ...
  }

  void submitCityName(String cityName) {
    // Get the Bloc using the BlocProvider
    final weatherBloc = BlocProvider.of<WeatherBloc>(context);
    // Initiate getting the weather
    weatherBloc.dispatch(GetWeather(cityName));
  }
}

Awesome! The app is now fully functional. After inputting the city name, GetWeather event is dispatched, which in turn outputs the WeatherLoading state, and after a while, the WeatherLoaded state finally arrives.

Using the app

Adding a BlocListener

What if you want to, let's say, log the loaded city in reaction to an emitted state? Plug it to the BlocBuilder, right? 

One issue with the BlocBuilder is that its build function doesn't run only when the state changes. It will also be invoked whenever Flutter decides rebuild the UI (e.g. hot reload). This means, if you're trying to keep a statistic on how many times a particular city has been loaded, BlocBuilder is not a good place to put your logging code.

To execute code only once per state change, use the BlocListener widget. All  of this also applies to things like showing Snackbars, navigating to different routes and so on. In short, BlocBuilder should only build the UI widgets, all of the other functionality should go into the BlocListener.

Usually, you pair the BlocListener with the BlocBuilder in a way, that the builder is a child of the listener.

main.dart

...
// BlocListener invokes the listener when new state is emitted.
child: BlocListener(
  bloc: weatherBloc,
  // Listener is the place for logging, showing Snackbars, navigating, etc.
  // It is guaranteed to run only once per state change.
  listener: (BuildContext context, WeatherState state) {
    if (state is WeatherLoaded) {
      print("Loaded: ${state.weather.cityName}");
    }
  },
  // BlocBuilder invokes the builder when new state is emitted.
  child: BlocBuilder(
    bloc: weatherBloc,
    // The builder function has to be a "pure function".
    // That is, it only returns a Widget and doesn't do anything else.
    builder: (BuildContext context, WeatherState state) {
      // Changing the UI based on the current state
      if (state is WeatherInitial) {
        return buildInitialInput();
      } else if (state is WeatherLoading) {
        return buildLoading();
      } else if (state is WeatherLoaded) {
        return buildColumnWithData(state.weather);
      }
    },
  ),
),
...

Conclusion

The Bloc library is the perfect way to take the pain out of state management. While other state management options using Streams are also available, and you can even write with the Bloc pattern without a library, using this library alleviates a lot of the nitty-gritty details, making the development easier and much more high-level.

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.

  • Kongsun says:

    Hello. I really like your video.

    But I wonder about rxdart (https://github.com/ReactiveX/rxdart) and bloc (https://github.com/felangel/bloc). Is it the same? And which one is better?
    Thank you.

    • Hi! RxDart is more low level, using Observables. Bloc is an abstraction on top of RxDart, so that you don’t have to write as much boilerplate code.
      One isn’t better than the other. They just have different purposes. For state management though, I’d use the higher level Bloc library all the time.

  • Basel says:

    Hi there
    really great article, learned a lot from it, but I have one problem, even when the state changes inside the bloc builder then UI doesn’t change, any ideas ?

  • Robert Wildling says:

    Excellent tutorial! Thank you very much! – Is there any chance to motivate you to do a more complex tutorial on that topic? One that exemplifies the use of more then just one event that has to be managed, e.g. something that includes navigation (how about a “Game States” setup? Start page, Introduction page, PlayGame page, Highscore page…), notification…?

  • Juan Sebastian Avila says:

    Hi Matej.

    I have a StatelessWidget where I was using this Bloc pattern, but, when I was trying to implement the dispose method the VsCode marks an error because the Stateless Widget doesn’t have a dispose method. I just changed the Stateless Widget for a Stateful Widget and it fix the error.
    The question is: Is not posible to use the Bloc pattern in a Stateless Widget?

  • >