Pular para o conteúdo

Flutter Bloc Concepts

Este conteúdo não está disponível em sua língua ainda.

Bloc Widgets

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

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

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 extensions
context.read<BlocA>();
// without extensions
BlocProvider.of<BlocA>(context);

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

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

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

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

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 extensions
context.read<RepositoryA>();
// without extensions
RepositoryProvider.of<RepositoryA>(context)

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

Lets take a look at how to use BlocProvider to provide a CounterBloc to a CounterPage and react to state changes with BlocBuilder.

counter_bloc.dart
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));
}
}
main.dart
void main() => runApp(CounterApp());
class CounterApp extends StatelessWidget {
@override
Widget build(BuildContext context) {
return MaterialApp(
home: BlocProvider(
create: (_) => CounterBloc(),
child: CounterPage(),
),
);
}
}
counter_page.dart
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

We are going to take a look at how to use RepositoryProvider within the context of the flutter_weather example.

weather_repository.dart
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,
);
}
}

Since the app has an explicit dependency on the WeatherRepository we inject an instance via constructor. This allows us to inject different instances of WeatherRepository based on the build flavor or environment.

main.dart
import 'package:flutter/material.dart';
import 'package:flutter_weather/app.dart';
import 'package:weather_repository/weather_repository.dart';
void main() {
runApp(WeatherApp(weatherRepository: WeatherRepository()));
}

Since we only have one repository in our app, we will inject it into our widget tree via RepositoryProvider.value. If you have more than one repository, you can use MultiRepositoryProvider to provide multiple repository instances to the subtree.

app.dart
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({Key? key, required WeatherRepository weatherRepository})
: _weatherRepository = weatherRepository,
super(key: key);
final WeatherRepository _weatherRepository;
@override
Widget build(BuildContext context) {
return RepositoryProvider.value(
value: _weatherRepository,
child: BlocProvider(
create: (_) => ThemeCubit(),
child: WeatherAppView(),
),
);
}
}

In most cases, the root app widget will expose one or more repositories to the subtree via RepositoryProvider.

weather_page.dart
import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:flutter_weather/weather/weather.dart';
import 'package:weather_repository/weather_repository.dart';
class WeatherPage extends StatelessWidget {
@override
Widget build(BuildContext context) {
return BlocProvider(
create: (context) => WeatherCubit(context.read<WeatherRepository>()),
child: WeatherView(),
);
}
}

Now when instantiating a bloc, we can access the instance of a repository via context.read and inject the repository into the bloc via constructor.

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

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.

Usage

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.

@override
Widget 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

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.

Usage

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.

@override
Widget 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.

@override
Widget 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

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.

Usage

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.

@override
Widget 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.

@override
Widget 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),
),
);
}