Conceptos de Flutter Bloc
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 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 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 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 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 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 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 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 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(),);
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.
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, ); }}
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';import 'package:weather_repository/weather_repository.dart';
void main() { runApp(WeatherApp(weatherRepository: WeatherRepository()));}
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({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(), ), ); }}
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.
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<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.
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), ), );}
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), ), );}