Conceptos de Flutter Bloc
Widgets de Bloc
Sección titulada «Widgets de Bloc»BlocBuilder
Sección titulada «BlocBuilder»BlocBuilder es un widget de Flutter que requiere un Bloc
y una función
builder
. BlocBuilder
maneja la construcción del widget en respuesta a nuevos
estados. BlocBuilder
es muy similar a StreamBuilder
pero tiene una API más
simple para reducir la cantidad de código boilerplate necesario. La función
builder
potencialmente será llamada muchas veces y debe ser una
función pura que devuelve un
widget en respuesta al estado.
Consulta BlocListener
si deseas “hacer” algo en respuesta a cambios de estado,
como navegación, mostrar un diálogo, etc.
Si se omite el parámetro bloc
, BlocBuilder
realizará automáticamente una
búsqueda usando BlocProvider
y el BuildContext
actual.
BlocBuilder<BlocA, BlocAState>( builder: (context, state) { // return widget here based on BlocA's state },);
Solo especifica el bloc si deseas proporcionar un bloc que estará limitado a un
solo widget y no es accesible a través de un BlocProvider
padre y el
BuildContext
actual.
BlocBuilder<BlocA, BlocAState>( bloc: blocA, // provide the local bloc instance builder: (context, state) { // return widget here based on BlocA's state },);
Para un control más detallado sobre cuándo se llama a la función builder
, se
puede proporcionar un buildWhen
opcional. buildWhen
toma el estado anterior
del bloc y el estado actual del bloc y devuelve un booleano. Si buildWhen
devuelve verdadero, se llamará a builder
con state
y el widget se
reconstruirá. Si buildWhen
devuelve falso, no se llamará a builder
con
state
y no ocurrirá ninguna reconstrucción.
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
Sección titulada «BlocSelector»BlocSelector es un widget de Flutter que es análogo a BlocBuilder
pero
permite a los desarrolladores filtrar actualizaciones seleccionando un nuevo
valor basado en el estado actual del bloc. Se previenen construcciones
innecesarias si el valor seleccionado no cambia. El valor seleccionado debe ser
inmutable para que BlocSelector
determine con precisión si se debe llamar
nuevamente a builder
.
Si se omite el parámetro bloc
, BlocSelector
realizará automáticamente una
búsqueda usando BlocProvider
y el BuildContext
actual.
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
Sección titulada «BlocProvider»BlocProvider es un widget de Flutter que proporciona un bloc a sus hijos a
través de BlocProvider.of<T>(context)
. Se utiliza como un widget de inyección
de dependencias (DI) para que una sola instancia de un bloc pueda ser
proporcionada a múltiples widgets dentro de un subárbol.
En la mayoría de los casos, BlocProvider
debe usarse para crear nuevos blocs
que estarán disponibles para el resto del subárbol. En este caso, dado que
BlocProvider
es responsable de crear el bloc, manejará automáticamente el
cierre del bloc.
BlocProvider( create: (BuildContext context) => BlocA(), child: ChildA(),);
Por defecto, BlocProvider
creará el bloc de manera perezosa, lo que significa
que create
se ejecutará cuando se busque el bloc a través de
BlocProvider.of<BlocA>(context)
.
Para anular este comportamiento y forzar que create
se ejecute inmediatamente,
lazy
se puede establecer en false
.
BlocProvider( lazy: false, create: (BuildContext context) => BlocA(), child: ChildA(),);
En algunos casos, BlocProvider
se puede usar para proporcionar un bloc
existente a una nueva porción del árbol de widgets. Esto se usará más comúnmente
cuando un bloc existente necesite estar disponible para una nueva ruta. En este
caso, BlocProvider
no cerrará automáticamente el bloc ya que no lo creó.
BlocProvider.value( value: BlocProvider.of<BlocA>(context), child: ScreenA(),);
entonces desde ChildA
o ScreenA
podemos recuperar BlocA
con:
// with extensionscontext.read<BlocA>();
// without extensionsBlocProvider.of<BlocA>(context);
MultiBlocProvider
Sección titulada «MultiBlocProvider»MultiBlocProvider es un widget de Flutter que fusiona múltiples widgets
BlocProvider
en uno solo. MultiBlocProvider
mejora la legibilidad y elimina
la necesidad de anidar múltiples BlocProviders
. Usando MultiBlocProvider
podemos pasar de:
BlocProvider<BlocA>( create: (BuildContext context) => BlocA(), child: BlocProvider<BlocB>( create: (BuildContext context) => BlocB(), child: BlocProvider<BlocC>( create: (BuildContext context) => BlocC(), child: ChildA(), ), ),);
a:
MultiBlocProvider( providers: [ BlocProvider<BlocA>( create: (BuildContext context) => BlocA(), ), BlocProvider<BlocB>( create: (BuildContext context) => BlocB(), ), BlocProvider<BlocC>( create: (BuildContext context) => BlocC(), ), ], child: ChildA(),);
BlocListener
Sección titulada «BlocListener»BlocListener es un widget de Flutter que toma un BlocWidgetListener
y un
Bloc
opcional e invoca el listener
en respuesta a cambios de estado en el
bloc. Debe usarse para funcionalidades que necesitan ocurrir una vez por cambio
de estado, como navegación, mostrar un SnackBar
, mostrar un Dialog
, etc.
listener
solo se llama una vez por cada cambio de estado (NO incluyendo el
estado inicial) a diferencia de builder
en BlocBuilder
y es una función
void
.
Si se omite el parámetro bloc
, BlocListener
realizará automáticamente una
búsqueda usando BlocProvider
y el BuildContext
actual.
BlocListener<BlocA, BlocAState>( listener: (context, state) { // do stuff here based on BlocA's state }, child: const SizedBox(),);
Solo especifica el bloc si deseas proporcionar un bloc que no es accesible a
través de BlocProvider
y el BuildContext
actual.
BlocListener<BlocA, BlocAState>( bloc: blocA, listener: (context, state) { // do stuff here based on BlocA's state }, child: const SizedBox(),);
Para un control más detallado sobre cuándo se llama a la función listener
, se
puede proporcionar un listenWhen
opcional. listenWhen
toma el estado
anterior del bloc y el estado actual del bloc y devuelve un booleano. Si
listenWhen
devuelve verdadero, se llamará a listener
con state
. Si
listenWhen
devuelve falso, no se llamará a listener
con 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
Sección titulada «MultiBlocListener»MultiBlocListener es un widget de Flutter que fusiona múltiples widgets
BlocListener
en uno solo. MultiBlocListener
mejora la legibilidad y elimina
la necesidad de anidar múltiples BlocListeners
. Usando MultiBlocListener
podemos pasar de:
BlocListener<BlocA, BlocAState>( listener: (context, state) {}, child: BlocListener<BlocB, BlocBState>( listener: (context, state) {}, child: BlocListener<BlocC, BlocCState>( listener: (context, state) {}, child: ChildA(), ), ),);
a:
MultiBlocListener( listeners: [ BlocListener<BlocA, BlocAState>( listener: (context, state) {}, ), BlocListener<BlocB, BlocBState>( listener: (context, state) {}, ), BlocListener<BlocC, BlocCState>( listener: (context, state) {}, ), ], child: ChildA(),);
BlocConsumer
Sección titulada «BlocConsumer»BlocConsumer expone un builder
y un listener
para reaccionar a nuevos
estados. BlocConsumer
es análogo a un BlocListener
y BlocBuilder
anidados,
pero reduce la cantidad de código boilerplate necesario. BlocConsumer
solo
debe usarse cuando es necesario tanto reconstruir la UI como ejecutar otras
reacciones a cambios de estado en el bloc
. BlocConsumer
toma un
BlocWidgetBuilder
y un BlocWidgetListener
requeridos y un bloc
,
BlocBuilderCondition
y BlocListenerCondition
opcionales.
Si se omite el parámetro bloc
, BlocConsumer
realizará automáticamente una
búsqueda usando BlocProvider
y el BuildContext
actual.
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 },);
Se pueden implementar opcionalmente listenWhen
y buildWhen
para un control
más granular sobre cuándo se llaman listener
y builder
. listenWhen
y
buildWhen
se invocarán en cada cambio de estado
del bloc
. Cada uno toma el
estado
anterior y el estado
actual y debe devolver un bool
que determina
si se invocará la función builder
y/o listener
. El estado
anterior se
inicializará al estado
del bloc
cuando se inicialice el BlocConsumer
.
listenWhen
y buildWhen
son opcionales y si no se implementan, su valor
predeterminado será 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
Sección titulada «RepositoryProvider»RepositoryProvider es un widget de Flutter que proporciona un repositorio a
sus hijos a través de RepositoryProvider.of<T>(context)
. Se utiliza como un
widget de inyección de dependencias (DI) para que una sola instancia de un
repositorio pueda ser proporcionada a múltiples widgets dentro de un subárbol.
BlocProvider
debe usarse para proporcionar blocs, mientras que
RepositoryProvider
solo debe usarse para repositorios.
RepositoryProvider( create: (context) => RepositoryA(), child: ChildA(),);
entonces desde ChildA
podemos recuperar la instancia del Repository
con:
// with extensionscontext.read<RepositoryA>();
// without extensionsRepositoryProvider.of<RepositoryA>(context)
MultiRepositoryProvider
Sección titulada «MultiRepositoryProvider»MultiRepositoryProvider es un widget de Flutter que fusiona múltiples
widgets RepositoryProvider
en uno solo. MultiRepositoryProvider
mejora la
legibilidad y elimina la necesidad de anidar múltiples RepositoryProvider
.
Usando MultiRepositoryProvider
podemos pasar de:
RepositoryProvider<RepositoryA>( create: (context) => RepositoryA(), child: RepositoryProvider<RepositoryB>( create: (context) => RepositoryB(), child: RepositoryProvider<RepositoryC>( create: (context) => RepositoryC(), child: ChildA(), ), ),);
a:
MultiRepositoryProvider( providers: [ RepositoryProvider<RepositoryA>( create: (context) => RepositoryA(), ), RepositoryProvider<RepositoryB>( create: (context) => RepositoryB(), ), RepositoryProvider<RepositoryC>( create: (context) => RepositoryC(), ), ], child: ChildA(),);
Uso de BlocProvider
Sección titulada «Uso de BlocProvider»Veamos cómo usar BlocProvider
para proporcionar un CounterBloc
a una
CounterPage
y reaccionar a los cambios de estado con 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()), ), ), ], ), ); }}
En este punto, hemos separado con éxito nuestra capa de presentación de nuestra
capa de lógica de negocio. Observa que el widget CounterPage
no sabe nada
sobre lo que sucede cuando un usuario toca los botones. El widget simplemente le
dice al CounterBloc
que el usuario ha presionado el botón de incremento o
decremento.
Uso de RepositoryProvider
Sección titulada «Uso de RepositoryProvider»Vamos a ver cómo usar RepositoryProvider
en el contexto del ejemplo
flutter_weather
.
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();}
Dado que la aplicación tiene una dependencia explícita del WeatherRepository
,
inyectamos una instancia a través del constructor. Esto nos permite inyectar
diferentes instancias de WeatherRepository
según el sabor de compilación o el
entorno.
import 'package:flutter/material.dart';import 'package:flutter_weather/app.dart';
void main() => runApp(const WeatherApp());
Dado que solo tenemos un repositorio en nuestra aplicación, lo inyectaremos en
nuestro árbol de widgets a través de RepositoryProvider.value
. Si tienes más
de un repositorio, puedes usar MultiRepositoryProvider
para proporcionar
múltiples instancias de repositorio al subárbol.
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(), ), ); }}
En la mayoría de los casos, el widget raíz de la aplicación expondrá uno o más
repositorios al subárbol a través de RepositoryProvider
.
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(), ); }}
Ahora, al instanciar un bloc, podemos acceder a la instancia de un repositorio a
través de context.read
e inyectar el repositorio en el bloc a través del
constructor.
Métodos de Extensión
Sección titulada «Métodos de Extensión»Los métodos de extensión,
introducidos en Dart 2.7, son una forma de agregar funcionalidad a las
bibliotecas existentes. En esta sección, veremos los métodos de extensión
incluidos en package:flutter_bloc
y cómo se pueden usar.
flutter_bloc
tiene una dependencia de
package:provider que simplifica el uso de
InheritedWidget
.
Internamente, package:flutter_bloc
usa package:provider
para implementar:
los widgets BlocProvider
, MultiBlocProvider
, RepositoryProvider
y
MultiRepositoryProvider
. package:flutter_bloc
exporta las extensiones
ReadContext
, WatchContext
y SelectContext
de package:provider
.
context.read
Sección titulada «context.read»context.read<T>()
busca la instancia de ancestro más cercana del tipo T
y es
funcionalmente equivalente a BlocProvider.of<T>(context)
. context.read
se
usa más comúnmente para recuperar una instancia de bloc con el fin de agregar un
evento dentro de las devoluciones de llamada onPressed
.
✅ USA context.read
para agregar eventos en callbacks.
onPressed() { context.read<CounterBloc>().add(CounterIncrementPressed()),}
❌ EVITA usar context.read
para recuperar el estado dentro de un método
build
.
@overrideWidget build(BuildContext context) { final state = context.read<MyBloc>().state; return Text('$state');}
El uso anterior es propenso a errores porque el widget Text
no se reconstruirá
si el estado del bloc cambia.
context.watch
Sección titulada «context.watch»Al igual que context.read<T>()
, context.watch<T>()
proporciona la instancia
de ancestro más cercana del tipo T
, sin embargo, también escucha los cambios
en la instancia. Es funcionalmente equivalente a
BlocProvider.of<T>(context, listen: true)
.
Si el Object
proporcionado del tipo T
cambia, context.watch
activará una
reconstrucción.
✅ USA BlocBuilder
en lugar de context.watch
para delimitar
explícitamente las reconstrucciones.
Widget build(BuildContext context) { return MaterialApp( home: Scaffold( body: BlocBuilder<MyBloc, MyState>( builder: (context, state) { // Siempre que el estado cambie, solo se reconstruirá el Text. return Text(state.value); }, ), ), );}
Alternativamente, usa un Builder
para delimitar las reconstrucciones.
@overrideWidget build(BuildContext context) { return MaterialApp( home: Scaffold( body: Builder( builder: (context) { // Siempre que el estado cambie, solo se reconstruirá el Text. final state = context.watch<MyBloc>().state; return Text(state.value); }, ), ), );}
✅ USA Builder
y context.watch
como MultiBlocBuilder
.
Builder( builder: (context) { final stateA = context.watch<BlocA>().state; final stateB = context.watch<BlocB>().state; final stateC = context.watch<BlocC>().state;
// devuelve un Widget que depende del estado de BlocA, BlocB y BlocC });
❌ EVITA usar context.watch
cuando el widget padre en el método build
no
depende del estado.
@overrideWidget build(BuildContext context) { // Siempre que el estado cambie, se reconstruirá el MaterialApp // aunque solo se use en el widget Text. final state = context.watch<MyBloc>().state; return MaterialApp( home: Scaffold( body: Text(state.value), ), );}
context.select
Sección titulada «context.select»Al igual que context.watch<T>()
, context.select<T, R>(R function(T value))
proporciona la instancia de ancestro más cercana del tipo T
y escucha los
cambios en T
. A diferencia de context.watch
, context.select
te permite
escuchar cambios en una parte más pequeña de un estado.
Widget build(BuildContext context) { final name = context.select((ProfileBloc bloc) => bloc.state.name); return Text(name);}
Lo anterior solo reconstruirá el widget cuando la propiedad name
del estado de
ProfileBloc
cambie.
✅ USA BlocSelector
en lugar de context.select
para delimitar
explícitamente las reconstrucciones.
Widget build(BuildContext context) { return MaterialApp( home: Scaffold( body: BlocSelector<ProfileBloc, ProfileState, String>( selector: (state) => state.name, builder: (context, name) { // Siempre que state.name cambie, solo se reconstruirá el Text. return Text(name); }, ), ), );}
Alternativamente, usa un Builder
para delimitar las reconstrucciones.
@overrideWidget build(BuildContext context) { return MaterialApp( home: Scaffold( body: Builder( builder: (context) { // Siempre que state.name cambie, solo se reconstruirá el Text. final name = context.select((ProfileBloc bloc) => bloc.state.name); return Text(name); }, ), ), );}
❌ EVITA usar context.select
cuando el widget padre en el método build
no depende del estado.
@overrideWidget build(BuildContext context) { // Siempre que state.value cambie, se reconstruirá el MaterialApp // aunque solo se use en el widget Text. final name = context.select((ProfileBloc bloc) => bloc.state.name); return MaterialApp( home: Scaffold( body: Text(name), ), );}