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 StatefulWidget
s. 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 a state.
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:
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.
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.
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 Stream
s 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
.
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...
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.
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.
There are two ways to do this:
- Inside main.dart, wrap the whole
MaterialApp
in aBlocProvider
. This will make the provided Bloc available globally across all routes. - 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.
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.
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.
Why add extends Equitable if it is not used anywhere else?
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?
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.
Hi Reso, thumbs up for your tutorials, I really appreciate the work you do here!
Looking at this and the Sailor tutorial, I tried to combine them. But I got stuck and filed a ticked at Sailor project. Maybe I did something wrong and you could provide some assistance: https://github.com/gurleensethi/sailor/issues/18
Thanks, Michael
Hi Reso,
Do you think it is good practice to use “Block Provider Tree” like what is been described in this Gitter discussion? https://gitter.im/flutter/flutter/archives/2019/03/02
hi ,thank you for the tutorial.
I kind of can’t understand why you use fakeWeatherRepository when you are creating the bloc instance. (in main.dart)
what’s the reason to ‘fakeWeatherRepository implements weatherRepository’ .
Why not just do things in weatherRepository ?
if you have found out why please let me know. Thanks.
The importance of interfaces, the abstract classes in the case of Dart, such as weatherRepository is to loose couple the dependency (fakeWeatherRepository) from the dependant (WeatherBloc) so the dependency can be easily replaced later from maybe a realWeatherRepository. See https://resocoder.com/category/tutorials/flutter/tdd-clean-architecture/
Hi, the code in GitHub seems to be the final code. Is it possible to have the initial code (the one at the beginning of the tutorial)? Great content!
Hey, if you clone or fork the repository, the starter code is available if you `git checkout starter-commit-id`. https://github.com/ResoCoder/flutter-bloc-library-v1-tutorial/tree/3fb5a904ae84163c78f25d7b6011218eabc6cbf1
Hi Reso
You’ve used the BlocProvider.value with Navigator.of(context).push(MaterialPageRoute())
Can we use BlocProvider.value with named routes also ??
Navigator.pushNamed(context,’secondScreen’);
routes: {
‘secondScreen’: (context) => BlocProvider.value(
value: BlocProvider.of(context),
child: SecondScreen(),
),
},
BlocComp => MyBlocComponent class
Thanks!!
After searching a city name how to get back to the initial page ?
can we also use initState instead of didChangeDependencie?
You can actually use WidgetsBinding.instance.addPostFrameCallback((_) {}) to access context inside initState.
thx for this tutorial.
i don’t get why search page is stateless widget and detail page is statfull? both of them are changing state. so?