Flutter Bloc Concepts
Bloc Widgets
Section titled “Bloc Widgets”BlocBuilder
Section titled “BlocBuilder”BlocBuilder is a Flutter widget which requires a Bloc
and a builder
function. BlocBuilder
handles building the widget in response to new states.
BlocBuilder
is very similar to StreamBuilder
but has a more simple API to
reduce the amount of boilerplate code needed. The builder
function will
potentially be called many times and should be a
pure function that returns a
widget in response to the state.
See BlocListener
if you want to “do” anything in response to state changes
such as navigation, showing a dialog, etc…
If the bloc
parameter is omitted, BlocBuilder
will automatically perform a
lookup using BlocProvider
and the current BuildContext
.
BlocBuilder<BlocA, BlocAState>( builder: (context, state) { // return widget here based on BlocA's state },);
Only specify the bloc if you wish to provide a bloc that will be scoped to a
single widget and isn’t accessible via a parent BlocProvider
and the current
BuildContext
.
BlocBuilder<BlocA, BlocAState>( bloc: blocA, // provide the local bloc instance builder: (context, state) { // return widget here based on BlocA's state },);
For fine-grained control over when the builder
function is called an optional
buildWhen
can be provided. buildWhen
takes the previous bloc state and
current bloc state and returns a boolean. If buildWhen
returns true, builder
will be called with state
and the widget will rebuild. If buildWhen
returns
false, builder
will not be called with state
and no rebuild will occur.
BlocBuilder<BlocA, BlocAState>( buildWhen: (previousState, state) { // return true/false to determine whether or not // to rebuild the widget with state }, builder: (context, state) { // return widget here based on BlocA's state },);
BlocSelector
Section titled “BlocSelector”BlocSelector is a Flutter widget which is analogous to BlocBuilder
but
allows developers to filter updates by selecting a new value based on the
current bloc state. Unnecessary builds are prevented if the selected value does
not change. The selected value must be immutable in order for BlocSelector
to
accurately determine whether builder
should be called again.
If the bloc
parameter is omitted, BlocSelector
will automatically perform a
lookup using BlocProvider
and the current BuildContext
.
BlocSelector<BlocA, BlocAState, SelectedState>( selector: (state) { // return selected state based on the provided state. }, builder: (context, state) { // return widget here based on the selected state. },);
BlocProvider
Section titled “BlocProvider”BlocProvider is a Flutter widget which provides a bloc to its children via
BlocProvider.of<T>(context)
. It is used as a dependency injection (DI) widget
so that a single instance of a bloc can be provided to multiple widgets within a
subtree.
In most cases, BlocProvider
should be used to create new blocs which will be
made available to the rest of the subtree. In this case, since BlocProvider
is
responsible for creating the bloc, it will automatically handle closing the
bloc.
BlocProvider( create: (BuildContext context) => BlocA(), child: ChildA(),);
By default, BlocProvider
will create the bloc lazily, meaning create
will
get executed when the bloc is looked up via BlocProvider.of<BlocA>(context)
.
To override this behavior and force create
to be run immediately, lazy
can
be set to false
.
BlocProvider( lazy: false, create: (BuildContext context) => BlocA(), child: ChildA(),);
In some cases, BlocProvider
can be used to provide an existing bloc to a new
portion of the widget tree. This will be most commonly used when an existing
bloc needs to be made available to a new route. In this case, BlocProvider
will not automatically close the bloc since it did not create it.
BlocProvider.value( value: BlocProvider.of<BlocA>(context), child: ScreenA(),);
then from either ChildA
, or ScreenA
we can retrieve BlocA
with:
// with extensionscontext.read<BlocA>();
// without extensionsBlocProvider.of<BlocA>(context);
MultiBlocProvider
Section titled “MultiBlocProvider”MultiBlocProvider is a Flutter widget that merges multiple BlocProvider
widgets into one. MultiBlocProvider
improves the readability and eliminates
the need to nest multiple BlocProviders
. By using MultiBlocProvider
we can
go from:
BlocProvider<BlocA>( create: (BuildContext context) => BlocA(), child: BlocProvider<BlocB>( create: (BuildContext context) => BlocB(), child: BlocProvider<BlocC>( create: (BuildContext context) => BlocC(), child: ChildA(), ), ),);
to:
MultiBlocProvider( providers: [ BlocProvider<BlocA>( create: (BuildContext context) => BlocA(), ), BlocProvider<BlocB>( create: (BuildContext context) => BlocB(), ), BlocProvider<BlocC>( create: (BuildContext context) => BlocC(), ), ], child: ChildA(),);
BlocListener
Section titled “BlocListener”BlocListener is a Flutter widget which takes a BlocWidgetListener
and an
optional Bloc
and invokes the listener
in response to state changes in the
bloc. It should be used for functionality that needs to occur once per state
change such as navigation, showing a SnackBar
, showing a Dialog
, etc…
listener
is only called once for each state change (NOT including the
initial state) unlike builder
in BlocBuilder
and is a void
function.
If the bloc
parameter is omitted, BlocListener
will automatically perform a
lookup using BlocProvider
and the current BuildContext
.
BlocListener<BlocA, BlocAState>( listener: (context, state) { // do stuff here based on BlocA's state }, child: const SizedBox(),);
Only specify the bloc if you wish to provide a bloc that is otherwise not
accessible via BlocProvider
and the current BuildContext
.
BlocListener<BlocA, BlocAState>( bloc: blocA, listener: (context, state) { // do stuff here based on BlocA's state }, child: const SizedBox(),);
For fine-grained control over when the listener
function is called an optional
listenWhen
can be provided. listenWhen
takes the previous bloc state and
current bloc state and returns a boolean. If listenWhen
returns true,
listener
will be called with state
. If listenWhen
returns false,
listener
will not be called with state
.
BlocListener<BlocA, BlocAState>( listenWhen: (previousState, state) { // return true/false to determine whether or not // to call listener with state }, listener: (context, state) { // do stuff here based on BlocA's state }, child: const SizedBox(),);
MultiBlocListener
Section titled “MultiBlocListener”MultiBlocListener is a Flutter widget that merges multiple BlocListener
widgets into one. MultiBlocListener
improves the readability and eliminates
the need to nest multiple BlocListeners
. By using MultiBlocListener
we can
go from:
BlocListener<BlocA, BlocAState>( listener: (context, state) {}, child: BlocListener<BlocB, BlocBState>( listener: (context, state) {}, child: BlocListener<BlocC, BlocCState>( listener: (context, state) {}, child: ChildA(), ), ),);
to:
MultiBlocListener( listeners: [ BlocListener<BlocA, BlocAState>( listener: (context, state) {}, ), BlocListener<BlocB, BlocBState>( listener: (context, state) {}, ), BlocListener<BlocC, BlocCState>( listener: (context, state) {}, ), ], child: ChildA(),);
BlocConsumer
Section titled “BlocConsumer”BlocConsumer exposes a builder
and listener
in order to react to new
states. BlocConsumer
is analogous to a nested BlocListener
and BlocBuilder
but reduces the amount of boilerplate needed. BlocConsumer
should only be used
when it is necessary to both rebuild UI and execute other reactions to state
changes in the bloc
. BlocConsumer
takes a required BlocWidgetBuilder
and
BlocWidgetListener
and an optional bloc
, BlocBuilderCondition
, and
BlocListenerCondition
.
If the bloc
parameter is omitted, BlocConsumer
will automatically perform a
lookup using BlocProvider
and the current BuildContext
.
BlocConsumer<BlocA, BlocAState>( listener: (context, state) { // do stuff here based on BlocA's state }, builder: (context, state) { // return widget here based on BlocA's state },);
An optional listenWhen
and buildWhen
can be implemented for more granular
control over when listener
and builder
are called. The listenWhen
and
buildWhen
will be invoked on each bloc
state
change. They each take the
previous state
and current state
and must return a bool
which determines
whether or not the builder
and/or listener
function will be invoked. The
previous state
will be initialized to the state
of the bloc
when the
BlocConsumer
is initialized. listenWhen
and buildWhen
are optional and if
they aren’t implemented, they will default to true
.
BlocConsumer<BlocA, BlocAState>( listenWhen: (previous, current) { // return true/false to determine whether or not // to invoke listener with state }, listener: (context, state) { // do stuff here based on BlocA's state }, buildWhen: (previous, current) { // return true/false to determine whether or not // to rebuild the widget with state }, builder: (context, state) { // return widget here based on BlocA's state },);
RepositoryProvider
Section titled “RepositoryProvider”RepositoryProvider is a Flutter widget which provides a repository to its
children via RepositoryProvider.of<T>(context)
. It is used as a dependency
injection (DI) widget so that a single instance of a repository can be provided
to multiple widgets within a subtree. BlocProvider
should be used to provide
blocs whereas RepositoryProvider
should only be used for repositories.
RepositoryProvider( create: (context) => RepositoryA(), child: ChildA(),);
then from ChildA
we can retrieve the Repository
instance with:
// with extensionscontext.read<RepositoryA>();
// without extensionsRepositoryProvider.of<RepositoryA>(context)
Repositories that manage resources which must be disposed can do so via the
dispose
callback:
RepositoryProvider<RepositoryA>( create: (context) => RepositoryA(), dispose: (repository) => repository.dispose(), child: ChildA(),);
MultiRepositoryProvider
Section titled “MultiRepositoryProvider”MultiRepositoryProvider is a Flutter widget that merges multiple
RepositoryProvider
widgets into one. MultiRepositoryProvider
improves the
readability and eliminates the need to nest multiple RepositoryProvider
. By
using MultiRepositoryProvider
we can go from:
RepositoryProvider<RepositoryA>( create: (context) => RepositoryA(), child: RepositoryProvider<RepositoryB>( create: (context) => RepositoryB(), child: RepositoryProvider<RepositoryC>( create: (context) => RepositoryC(), child: ChildA(), ), ),);
to:
MultiRepositoryProvider( providers: [ RepositoryProvider<RepositoryA>( create: (context) => RepositoryA(), ), RepositoryProvider<RepositoryB>( create: (context) => RepositoryB(), ), RepositoryProvider<RepositoryC>( create: (context) => RepositoryC(), ), ], child: ChildA(),);
BlocProvider Usage
Section titled “BlocProvider Usage”Lets take a look at how to use BlocProvider
to provide a CounterBloc
to a
CounterPage
and react to state changes with BlocBuilder
.
sealed class CounterEvent {}final class CounterIncrementPressed extends CounterEvent {}final class CounterDecrementPressed extends CounterEvent {}
class CounterBloc extends Bloc<CounterEvent, int> { CounterBloc() : super(0) { on<CounterIncrementPressed>((event, emit) => emit(state + 1)); on<CounterDecrementPressed>((event, emit) => emit(state - 1)); }}
void main() => runApp(CounterApp());
class CounterApp extends StatelessWidget { @override Widget build(BuildContext context) { return MaterialApp( home: BlocProvider( create: (_) => CounterBloc(), child: CounterPage(), ), ); }}
class CounterPage extends StatelessWidget { @override Widget build(BuildContext context) { return Scaffold( appBar: AppBar(title: Text('Counter')), body: BlocBuilder<CounterBloc, int>( builder: (context, count) { return Center( child: Text( '$count', style: TextStyle(fontSize: 24.0), ), ); }, ), floatingActionButton: Column( crossAxisAlignment: CrossAxisAlignment.end, mainAxisAlignment: MainAxisAlignment.end, children: <Widget>[ Padding( padding: EdgeInsets.symmetric(vertical: 5.0), child: FloatingActionButton( child: Icon(Icons.add), onPressed: () => context.read<CounterBloc>().add(CounterIncrementPressed()), ), ), Padding( padding: EdgeInsets.symmetric(vertical: 5.0), child: FloatingActionButton( child: Icon(Icons.remove), onPressed: () => context.read<CounterBloc>().add(CounterDecrementPressed()), ), ), ], ), ); }}
At this point we have successfully separated our presentational layer from our
business logic layer. Notice that the CounterPage
widget knows nothing about
what happens when a user taps the buttons. The widget simply tells the
CounterBloc
that the user has pressed either the increment or decrement
button.
RepositoryProvider Usage
Section titled “RepositoryProvider Usage”We are going to take a look at how to use RepositoryProvider
within the
context of the flutter_weather
example.
class WeatherRepository { WeatherRepository({ WeatherApiClient? weatherApiClient }) : _weatherApiClient = weatherApiClient ?? WeatherApiClient();
final WeatherApiClient _weatherApiClient;
Future<Weather> getWeather(String city) async { final location = await _weatherApiClient.locationSearch(city); final woeid = location.woeid; final weather = await _weatherApiClient.getWeather(woeid); return Weather( temperature: weather.theTemp, location: location.title, condition: weather.weatherStateAbbr.toCondition, ); }
void dispose() => _weatherApiClient.close();}
In our main.dart
, we call runApp
with our WeatherApp
widget.
import 'package:flutter/material.dart';import 'package:flutter_weather/app.dart';
void main() => runApp(const WeatherApp());
We will inject our WeatherRepository
instance into our widget tree via
RepositoryProvider
.
When instantiating a bloc, we can access the instance of a repository via
context.read
and inject the repository into the bloc via constructor.
import 'package:flutter/material.dart';import 'package:flutter_bloc/flutter_bloc.dart';import 'package:weather_repository/weather_repository.dart';
class WeatherApp extends StatelessWidget { const WeatherApp({super.key});
@override Widget build(BuildContext context) { return RepositoryProvider( create: (_) => WeatherRepository(), dispose: (repository) => repository.dispose(), child: BlocProvider( create: (context) => WeatherCubit(context.read<WeatherRepository>()), child: const WeatherAppView(), ), ); }}
Extension Methods
Section titled “Extension Methods”Extension methods,
introduced in Dart 2.7, are a way to add functionality to existing libraries. In
this section, we’ll take a look at extension methods included in
package:flutter_bloc
and how they can be used.
flutter_bloc
has a dependency on
package:provider which simplifies the use
of
InheritedWidget
.
Internally, package:flutter_bloc
uses package:provider
to implement:
BlocProvider
, MultiBlocProvider
, RepositoryProvider
and
MultiRepositoryProvider
widgets. package:flutter_bloc
exports the
ReadContext
, WatchContext
and SelectContext
, extensions from
package:provider
.
context.read
Section titled “context.read”context.read<T>()
looks up the closest ancestor instance of type T
and is
functionally equivalent to BlocProvider.of<T>(context)
. context.read
is most
commonly used for retrieving a bloc instance in order to add an event within
onPressed
callbacks.
✅ DO use context.read
to add events in callbacks.
onPressed() { context.read<CounterBloc>().add(CounterIncrementPressed()),}
❌ AVOID using context.read
to retrieve state within a build
method.
@overrideWidget build(BuildContext context) { final state = context.read<MyBloc>().state; return Text('$state');}
The above usage is error prone because the Text
widget will not be rebuilt if
the state of the bloc changes.
context.watch
Section titled “context.watch”Like context.read<T>()
, context.watch<T>()
provides the closest ancestor
instance of type T
, however it also listens to changes on the instance. It is
functionally equivalent to BlocProvider.of<T>(context, listen: true)
.
If the provided Object
of type T
changes, context.watch
will trigger a
rebuild.
✅ DO use BlocBuilder
instead of context.watch
to explicitly scope
rebuilds.
Widget build(BuildContext context) { return MaterialApp( home: Scaffold( body: BlocBuilder<MyBloc, MyState>( builder: (context, state) { // Whenever the state changes, only the Text is rebuilt. return Text(state.value); }, ), ), );}
Alternatively, use a Builder
to scope rebuilds.
@overrideWidget build(BuildContext context) { return MaterialApp( home: Scaffold( body: Builder( builder: (context) { // Whenever the state changes, only the Text is rebuilt. final state = context.watch<MyBloc>().state; return Text(state.value); }, ), ), );}
✅ DO use Builder
and context.watch
as MultiBlocBuilder
.
Builder( builder: (context) { final stateA = context.watch<BlocA>().state; final stateB = context.watch<BlocB>().state; final stateC = context.watch<BlocC>().state;
// return a Widget which depends on the state of BlocA, BlocB, and BlocC });
❌ AVOID using context.watch
when the parent widget in the build
method
doesn’t depend on the state.
@overrideWidget build(BuildContext context) { // Whenever the state changes, the MaterialApp is rebuilt // even though it is only used in the Text widget. final state = context.watch<MyBloc>().state; return MaterialApp( home: Scaffold( body: Text(state.value), ), );}
context.select
Section titled “context.select”Just like context.watch<T>()
, context.select<T, R>(R function(T value))
provides the closest ancestor instance of type T
and listens to changes on
T
. Unlike context.watch
, context.select
allows you listen for changes in a
smaller part of a state.
Widget build(BuildContext context) { final name = context.select((ProfileBloc bloc) => bloc.state.name); return Text(name);}
The above will only rebuild the widget when the property name
of the
ProfileBloc
’s state changes.
✅ DO use BlocSelector
instead of context.select
to explicitly scope
rebuilds.
Widget build(BuildContext context) { return MaterialApp( home: Scaffold( body: BlocSelector<ProfileBloc, ProfileState, String>( selector: (state) => state.name, builder: (context, name) { // Whenever the state.name changes, only the Text is rebuilt. return Text(name); }, ), ), );}
Alternatively, use a Builder
to scope rebuilds.
@overrideWidget build(BuildContext context) { return MaterialApp( home: Scaffold( body: Builder( builder: (context) { // Whenever state.name changes, only the Text is rebuilt. final name = context.select((ProfileBloc bloc) => bloc.state.name); return Text(name); }, ), ), );}
❌ AVOID using context.select
when the parent widget in a build method
doesn’t depend on the state.
@overrideWidget build(BuildContext context) { // Whenever the state.value changes, the MaterialApp is rebuilt // even though it is only used in the Text widget. final name = context.select((ProfileBloc bloc) => bloc.state.name); return MaterialApp( home: Scaffold( body: Text(name), ), );}