Saltearse al contenido

Conceptos de Flutter Bloc

Widgets de Bloc

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

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

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

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

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

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

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

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

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

Veamos cómo usar BlocProvider para proporcionar un CounterBloc a una CounterPage y reaccionar a los cambios de estado con 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()),
),
),
],
),
);
}
}

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

Vamos a ver cómo usar RepositoryProvider en el contexto del ejemplo flutter_weather.

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

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.

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()));
}

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.

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(),
),
);
}
}

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.

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(),
);
}
}

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

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

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.

Uso

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.

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

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.

Uso

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.

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

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

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.

Uso

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.

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

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