Saltearse al contenido

Guía de Migración

v10.0.0

package:bloc_test

❗✨ Desacoplar blocTest de BlocBase

Justificación

blocTest debería usar las interfaces principales de bloc cuando sea posible para una mayor flexibilidad y reutilización. Anteriormente esto no era posible porque BlocBase implementaba StateStreamableSource, lo cual no era suficiente para blocTest debido a la dependencia interna en la API emit.

package:hydrated_bloc

❗✨ Soporte para WebAssembly

Justificación

Anteriormente no era posible compilar aplicaciones a wasm cuando se usaba hydrated_bloc. En la versión v10.0.0, el paquete fue refactorizado para permitir la compilación a wasm.

v9.x.x

Future<void> main() async {
WidgetsFlutterBinding.ensureInitialized();
HydratedBloc.storage = await HydratedStorage.build(
storageDirectory: kIsWeb
? HydratedStorage.webStorageDirectory
: await getTemporaryDirectory(),
);
runApp(App());
}

v10.x.x

void main() async {
WidgetsFlutterBinding.ensureInitialized();
HydratedBloc.storage = await HydratedStorage.build(
storageDirectory: kIsWeb
? HydratedStorageDirectory.web
: HydratedStorageDirectory((await getTemporaryDirectory()).path),
);
runApp(const App());
}

v9.0.0

package:bloc

❗🧹 Eliminar APIs Obsoletas

Resumen
  • BlocOverrides eliminado en favor de Bloc.observer y Bloc.transformer

❗✨ Introducir nueva Interfaz EmittableStateStreamableSource

Justificación

package:bloc_test estaba previamente estrechamente acoplado a BlocBase. La interfaz EmittableStateStreamableSource se introdujo para permitir que blocTest se desacople de la implementación concreta de BlocBase.

package:hydrated_bloc

✨ Reintroducir la API HydratedBloc.storage

Justificación

Consulta la justificación para reintroducir las anulaciones de Bloc.observer y Bloc.transformer.

v8.x.x

Future<void> main() async {
final storage = await HydratedStorage.build(
storageDirectory: kIsWeb
? HydratedStorage.webStorageDirectory
: await getTemporaryDirectory(),
);
HydratedBlocOverrides.runZoned(
() => runApp(App()),
storage: storage,
);
}

v9.0.0

Future<void> main() async {
WidgetsFlutterBinding.ensureInitialized();
HydratedBloc.storage = await HydratedStorage.build(
storageDirectory: kIsWeb
? HydratedStorage.webStorageDirectory
: await getTemporaryDirectory(),
);
runApp(App());
}

v8.1.0

package:bloc

✨ Reintroducir las APIs Bloc.observer y Bloc.transformer

Justificación

La API BlocOverrides se introdujo en v8.0.0 en un intento de soportar configuraciones específicas de bloc como BlocObserver, EventTransformer y HydratedStorage. En aplicaciones puras de Dart, los cambios funcionaron bien; sin embargo, en aplicaciones Flutter la nueva API causó más problemas de los que resolvió.

La API BlocOverrides se inspiró en APIs similares en Flutter/Dart:

Problemas

Aunque no fue la razón principal para estos cambios, la API BlocOverrides introdujo complejidad adicional para los desarrolladores. Además de aumentar la cantidad de anidamiento y líneas de código necesarias para lograr el mismo efecto, la API BlocOverrides requería que los desarrolladores tuvieran un sólido entendimiento de Zones en Dart. Las Zones no son un concepto amigable para principiantes y el no entender cómo funcionan podría llevar a la introducción de errores (como observadores, transformadores o instancias de almacenamiento no inicializadas).

Por ejemplo, muchos desarrolladores tendrían algo como:

void main() {
WidgetsFlutterBinding.ensureInitialized();
BlocOverrides.runZoned(...);
}

El código anterior, aunque parece inofensivo, puede llevar a muchos errores difíciles de rastrear. La zona desde la cual se llama inicialmente a WidgetsFlutterBinding.ensureInitialized será la zona en la que se manejan los eventos de gestos (por ejemplo, callbacks onTap, onPressed) debido a GestureBinding.initInstances. Este es solo uno de los muchos problemas causados por el uso de zoneValues.

Además, Flutter hace muchas cosas detrás de escena que implican bifurcar/manipular Zonas (especialmente al ejecutar pruebas) lo que puede llevar a comportamientos inesperados (y en muchos casos comportamientos que están fuera del control del desarrollador — ver problemas a continuación).

Debido al uso de runZoned, la transición a la API BlocOverrides llevó al descubrimiento de varios errores/limitaciones en Flutter (específicamente alrededor de las Pruebas de Widgets e Integración):

lo cual afectó a muchos desarrolladores que usaban la biblioteca bloc:

v8.0.x

void main() {
BlocOverrides.runZoned(
() {
// ...
},
blocObserver: CustomBlocObserver(),
eventTransformer: customEventTransformer(),
);
}

v8.1.0

void main() {
Bloc.observer = CustomBlocObserver();
Bloc.transformer = customEventTransformer();
// ...
}

v8.0.0

package:bloc

❗✨ Introducir nueva API BlocOverrides

Justificación

La API anterior utilizada para sobrescribir el BlocObserver y EventTransformer predeterminados dependía de un singleton global tanto para el BlocObserver como para el EventTransformer.

Como resultado, no era posible:

  • Tener múltiples implementaciones de BlocObserver o EventTransformer limitadas a diferentes partes de la aplicación.
  • Tener sobrescrituras de BlocObserver o EventTransformer limitadas a un paquete.
    • Si un paquete dependía de package:bloc y registraba su propio BlocObserver, cualquier consumidor del paquete tendría que sobrescribir el BlocObserver del paquete o informar al BlocObserver del paquete.

También era más difícil de probar debido al estado global compartido entre las pruebas.

Bloc v8.0.0 introduce una clase BlocOverrides que permite a los desarrolladores sobrescribir BlocObserver y/o EventTransformer para una Zone específica en lugar de depender de un singleton global mutable.

v7.x.x

void main() {
Bloc.observer = CustomBlocObserver();
Bloc.transformer = customEventTransformer();
// ...
}

v8.0.0

void main() {
BlocOverrides.runZoned(
() {
// ...
},
blocObserver: CustomBlocObserver(),
eventTransformer: customEventTransformer(),
);
}

Las instancias de Bloc usarán el BlocObserver y/o EventTransformer para la Zone actual a través de BlocOverrides.current. Si no hay BlocOverrides para la zona, usarán los valores predeterminados internos existentes (sin cambio en comportamiento/funcionalidad).

Esto permite que cada Zone funcione de manera independiente con sus propios BlocOverrides.

BlocOverrides.runZoned(
() {
// BlocObserverA y eventTransformerA
final overrides = BlocOverrides.current;
// Los Blocs en esta zona reportan a BlocObserverA
// y usan eventTransformerA como el transformador predeterminado.
// ...
// Más tarde...
BlocOverrides.runZoned(
() {
// BlocObserverB y eventTransformerB
final overrides = BlocOverrides.current;
// Los Blocs en esta zona reportan a BlocObserverB
// y usan eventTransformerB como el transformador predeterminado.
// ...
},
blocObserver: BlocObserverB(),
eventTransformer: eventTransformerB(),
);
},
blocObserver: BlocObserverA(),
eventTransformer: eventTransformerA(),
);

❗✨ Mejorar el Manejo y Reporte de Errores

Justificación

El objetivo de estos cambios es:

  • hacer que las excepciones internas no manejadas sean extremadamente obvias mientras se preserva la funcionalidad del bloc
  • soportar addError sin interrumpir el flujo de control

Anteriormente, el manejo y reporte de errores variaba dependiendo de si la aplicación se ejecutaba en modo de depuración o lanzamiento. Además, los errores reportados a través de addError se trataban como excepciones no capturadas en modo de depuración, lo que llevaba a una mala experiencia de desarrollador al usar la API addError (específicamente al escribir pruebas unitarias).

En v8.0.0, addError se puede usar de manera segura para reportar errores y blocTest se puede usar para verificar que los errores se reporten. Todos los errores aún se reportan a onError, sin embargo, solo las excepciones no capturadas se vuelven a lanzar (independientemente del modo de depuración o lanzamiento).

❗🧹 Hacer BlocObserver abstracto

Justificación

BlocObserver estaba destinado a ser una interfaz. Dado que la implementación predeterminada de la API son operaciones nulas, BlocObserver es ahora una clase abstract para comunicar claramente que la clase está destinada a ser extendida y no instanciada directamente.

v7.x.x

void main() {
// Era posible crear una instancia de la clase base.
final observer = BlocObserver();
}

v8.0.0

class MyBlocObserver extends BlocObserver {...}
void main() {
// No se puede instanciar la clase base.
final observer = BlocObserver(); // ERROR
// Extiende `BlocObserver` en su lugar.
final observer = MyBlocObserver(); // OK
}

❗✨ add lanza StateError si el Bloc está cerrado

Justificación

Anteriormente, era posible llamar a add en un bloc cerrado y el error interno se tragaba, lo que dificultaba depurar por qué el evento añadido no se estaba procesando. Para hacer este escenario más visible, en v8.0.0, llamar a add en un bloc cerrado lanzará un StateError que se informará como una excepción no capturada y se propagará a onError.

❗✨ emit lanza StateError si el Bloc está cerrado

Justificación

Anteriormente, era posible llamar a emit dentro de un bloc cerrado y no ocurría ningún cambio de estado, pero tampoco había una indicación de lo que salió mal, lo que dificultaba la depuración. Para hacer este escenario más visible, en v8.0.0, llamar a emit dentro de un bloc cerrado lanzará un StateError que se informará como una excepción no capturada y se propagará a onError.

❗🧹 Eliminar APIs Obsoletas

Resumen
  • mapEventToState eliminado en favor de on<Event>
  • transformEvents eliminado en favor de la API EventTransformer
  • TransitionFunction typedef eliminado en favor de la API EventTransformer
  • listen eliminado en favor de stream.listen

package:bloc_test

MockBloc y MockCubit ya no requieren registerFallbackValue

Resumen

registerFallbackValue solo es necesario cuando se usa el matcher any() de package:mocktail para un tipo personalizado. Anteriormente, registerFallbackValue era necesario para cada Event y State al usar MockBloc o MockCubit.

v8.x.x

class FakeMyEvent extends Fake implements MyEvent {}
class FakeMyState extends Fake implements MyState {}
class MyMockBloc extends MockBloc<MyEvent, MyState> implements MyBloc {}
void main() {
setUpAll(() {
registerFallbackValue(FakeMyEvent());
registerFallbackValue(FakeMyState());
});
// Tests...
}

v9.0.0

class MyMockBloc extends MockBloc<MyEvent, MyState> implements MyBloc {}
void main() {
// Tests...
}

package:hydrated_bloc

❗✨ Introducir nueva API HydratedBlocOverrides

Justificación

Anteriormente, se utilizaba un singleton global para sobrescribir la implementación de Storage.

Como resultado, no era posible tener múltiples implementaciones de Storage limitadas a diferentes partes de la aplicación. También era más difícil de probar debido al estado global compartido entre las pruebas.

HydratedBloc v8.0.0 introduce una clase HydratedBlocOverrides que permite a los desarrolladores sobrescribir Storage para una Zone específica en lugar de depender de un singleton global mutable.

v7.x.x

void main() async {
HydratedBloc.storage = await HydratedStorage.build(
storageDirectory: await getApplicationSupportDirectory(),
);
// ...
}

v8.0.0

void main() {
final storage = await HydratedStorage.build(
storageDirectory: await getApplicationSupportDirectory(),
);
HydratedBlocOverrides.runZoned(
() {
// ...
},
storage: storage,
);
}

HydratedBloc usará el Storage para la Zone actual a través de HydratedBlocOverrides.current.

Esto permite que cada Zone funcione de manera independiente con sus propios BlocOverrides.

v7.2.0

package:bloc

✨ Introducir nueva API on<Event>

Justificación

La API on<Event> se introdujo como parte de [Propuesta] Reemplazar mapEventToState con on<Event> en Bloc. Debido a un problema en Dart no siempre es obvio cuál será el valor de state cuando se trata de generadores asincrónicos anidados (async*). Aunque hay formas de solucionar el problema, uno de los principios fundamentales de la biblioteca bloc es ser predecible. La API on<Event> se creó para hacer que la biblioteca sea lo más segura posible de usar y para eliminar cualquier incertidumbre en lo que respecta a los cambios de estado.

Resumen

on<E> te permite registrar un manejador de eventos para todos los eventos del tipo E. Por defecto, los eventos se procesarán concurrentemente cuando se use on<E> en lugar de mapEventToState, que procesa los eventos secuencialmente.

v7.1.0

abstract class CounterEvent {}
class Increment extends CounterEvent {}
class CounterBloc extends Bloc<CounterEvent, int> {
CounterBloc() : super(0);
@override
Stream<int> mapEventToState(CounterEvent event) async* {
if (event is Increment) {
yield state + 1;
}
}
}

v7.2.0

abstract class CounterEvent {}
class Increment extends CounterEvent {}
class CounterBloc extends Bloc<CounterEvent, int> {
CounterBloc() : super(0) {
on<Increment>((event, emit) => emit(state + 1));
}
}

Si deseas mantener el mismo comportamiento exacto que en la versión v7.1.0, puedes registrar un solo manejador de eventos para todos los eventos y aplicar un transformador sequential:

import 'package:bloc/bloc.dart';
import 'package:bloc_concurrency/bloc_concurrency.dart';
class MyBloc extends Bloc<MyEvent, MyState> {
MyBloc() : super(MyState()) {
on<MyEvent>(_onEvent, transformer: sequential())
}
FutureOr<void> _onEvent(MyEvent event, Emitter<MyState> emit) async {
// TODO: logic goes here...
}
}

También puedes sobrescribir el EventTransformer predeterminado para todos los blocs en tu aplicación:

import 'package:bloc/bloc.dart';
import 'package:bloc_concurrency/bloc_concurrency.dart';
void main() {
Bloc.transformer = sequential<dynamic>();
...
}

✨ Introducir nueva API EventTransformer

Justificación

La API on<Event> abrió la puerta para poder proporcionar un transformador de eventos personalizado por manejador de eventos. Se introdujo un nuevo typedef EventTransformer que permite a los desarrolladores transformar el flujo de eventos entrantes para cada manejador de eventos en lugar de tener que especificar un único transformador de eventos para todos los eventos.

Resumen

Un EventTransformer es responsable de tomar el flujo entrante de eventos junto con un EventMapper (tu manejador de eventos) y devolver un nuevo flujo de eventos.

typedef EventTransformer<Event> = Stream<Event> Function(Stream<Event> events, EventMapper<Event> mapper)

El EventTransformer predeterminado procesa todos los eventos concurrentemente y se ve algo así:

EventTransformer<E> concurrent<E>() {
return (events, mapper) => events.flatMap(mapper);
}

v7.1.0

@override
Stream<Transition<MyEvent, MyState>> transformEvents(events, transitionFn) {
return events
.debounceTime(const Duration(milliseconds: 300))
.flatMap(transitionFn);
}

v7.2.0

/// Define un `EventTransformer` personalizado
EventTransformer<MyEvent> debounce<MyEvent>(Duration duration) {
return (events, mapper) => events.debounceTime(duration).flatMap(mapper);
}
MyBloc() : super(MyState()) {
/// Aplica el `EventTransformer` personalizado al `EventHandler`
on<MyEvent>(_onEvent, transformer: debounce(const Duration(milliseconds: 300)))
}

⚠️ Marcar como obsoleta la API transformTransitions

Justificación

El getter stream en Bloc facilita la sobrescritura del flujo de estados salientes, por lo tanto, ya no es valioso mantener una API transformTransitions separada.

Resumen

v7.1.0

@override
Stream<Transition<Event, State>> transformTransitions(
Stream<Transition<Event, State>> transitions,
) {
return transitions.debounceTime(const Duration(milliseconds: 42));
}

v7.2.0

@override
Stream<State> get stream => super.stream.debounceTime(const Duration(milliseconds: 42));

v7.0.0

package:bloc

❗ Bloc y Cubit extienden BlocBase

Justificación

Como desarrollador, la relación entre blocs y cubits era un poco incómoda. Cuando se introdujo cubit por primera vez, comenzó como la clase base para blocs, lo cual tenía sentido porque tenía un subconjunto de la funcionalidad y los blocs simplemente extenderían Cubit y definirían APIs adicionales. Esto tenía algunos inconvenientes:

  • Todas las APIs tendrían que ser renombradas para aceptar un cubit por precisión o tendrían que mantenerse como bloc por consistencia, aunque jerárquicamente no fuera preciso (#1708, #1560).

  • Cubit tendría que extender Stream e implementar EventSink para tener una base común sobre la cual se puedan implementar widgets como BlocBuilder, BlocListener, etc. (#1429).

Más tarde, experimentamos con invertir la relación y hacer que bloc fuera la clase base, lo que resolvió parcialmente el primer punto anterior pero introdujo otros problemas:

  • La API de cubit está sobrecargada debido a las APIs subyacentes de bloc como mapEventToState, add, etc. (#2228)
    • Los desarrolladores técnicamente pueden invocar estas APIs y romper cosas.
  • Todavía tenemos el mismo problema de cubit exponiendo toda la API de stream como antes (#1429)

Para abordar estos problemas, introdujimos una clase base tanto para Bloc como para Cubit llamada BlocBase para que los componentes upstream puedan seguir interoperando con instancias de bloc y cubit sin exponer toda la API de Stream y EventSink directamente.

Resumen

BlocObserver

v6.1.x

class SimpleBlocObserver extends BlocObserver {
@override
void onCreate(Cubit cubit) {...}
@override
void onEvent(Bloc bloc, Object event) {...}
@override
void onChange(Cubit cubit, Object event) {...}
@override
void onTransition(Bloc bloc, Transition transition) {...}
@override
void onError(Cubit cubit, Object error, StackTrace stackTrace) {...}
@override
void onClose(Cubit cubit) {...}
}

v7.0.0

class SimpleBlocObserver extends BlocObserver {
@override
void onCreate(BlocBase bloc) {...}
@override
void onEvent(Bloc bloc, Object event) {...}
@override
void onChange(BlocBase bloc, Object? event) {...}
@override
void onTransition(Bloc bloc, Transition transition) {...}
@override
void onError(BlocBase bloc, Object error, StackTrace stackTrace) {...}
@override
void onClose(BlocBase bloc) {...}
}

Bloc/Cubit

v6.1.x

final bloc = MyBloc();
bloc.listen((state) {...});
final cubit = MyCubit();
cubit.listen((state) {...});

v7.0.0

final bloc = MyBloc();
bloc.stream.listen((state) {...});
final cubit = MyCubit();
cubit.stream.listen((state) {...});

package:bloc_test

❗seed devuelve una función para soportar valores dinámicos

Justificación

Para soportar tener un valor de semilla mutable que se pueda actualizar dinámicamente en setUp, seed devuelve una función.

Resumen

v7.x.x

blocTest(
'...',
seed: MyState(),
...
);

v8.0.0

blocTest(
'...',
seed: () => MyState(),
...
);

❗expect devuelve una función para soportar valores dinámicos y soporte de matchers

Justificación

Para soportar tener una expectativa mutable que se pueda actualizar dinámicamente en setUp, expect devuelve una función. expect también soporta Matchers.

Resumen

v7.x.x

blocTest(
'...',
expect: [MyStateA(), MyStateB()],
...
);

v8.0.0

blocTest(
'...',
expect: () => [MyStateA(), MyStateB()],
...
);
// It can also be a `Matcher`
blocTest(
'...',
expect: () => contains(MyStateA()),
...
);

❗errors devuelve una función para soportar valores dinámicos y soporte de matchers

Justificación

Para soportar tener un valor de errores mutable que se pueda actualizar dinámicamente en setUp, errors devuelve una función. errors también soporta Matchers.

Resumen

v7.x.x

blocTest(
'...',
errors: [MyError()],
...
);

v8.0.0

blocTest(
'...',
errors: () => [MyError()],
...
);
// It can also be a `Matcher`
blocTest(
'...',
errors: () => contains(MyError()),
...
);

❗MockBloc y MockCubit

Justificación

Para soportar la simulación de varias APIs centrales, MockBloc y MockCubit se exportan como parte del paquete bloc_test. Anteriormente, MockBloc tenía que ser utilizado tanto para instancias de Bloc como de Cubit, lo cual no era intuitivo.

Resumen

v7.x.x

class MockMyBloc extends MockBloc<MyState> implements MyBloc {}
class MockMyCubit extends MockBloc<MyState> implements MyBloc {}

v8.0.0

class MockMyBloc extends MockBloc<MyEvent, MyState> implements MyBloc {}
class MockMyCubit extends MockCubit<MyState> implements MyCubit {}

❗Integración con Mocktail

Justificación

Debido a varias limitaciones de la versión null-safe del paquete package:mockito descritas aquí, el paquete package:mocktail es utilizado por MockBloc y MockCubit. Esto permite a los desarrolladores continuar usando una API de simulación familiar sin la necesidad de escribir stubs manualmente o depender de la generación de código.

Resumen

v7.x.x

import 'package:mockito/mockito.dart';
...
when(bloc.state).thenReturn(MyState());
verify(bloc.add(any)).called(1);

v8.0.0

import 'package:mocktail/mocktail.dart';
...
when(() => bloc.state).thenReturn(MyState());
verify(() => bloc.add(any())).called(1);

Please refer to #347 as well as the mocktail documentation for more information.

package:flutter_bloc

❗ renombrar el parámetro cubit a bloc

Justificación

Como resultado de la refactorización en package:bloc para introducir BlocBase, que extiende Bloc y Cubit, los parámetros de BlocBuilder, BlocConsumer y BlocListener se renombraron de cubit a bloc porque los widgets operan sobre el tipo BlocBase. Esto también se alinea aún más con el nombre de la biblioteca y, con suerte, mejora la legibilidad.

Resumen

v6.1.x

BlocBuilder(
cubit: myBloc,
...
)
BlocListener(
cubit: myBloc,
...
)
BlocConsumer(
cubit: myBloc,
...
)

v7.0.0

BlocBuilder(
bloc: myBloc,
...
)
BlocListener(
bloc: myBloc,
...
)
BlocConsumer(
bloc: myBloc,
...
)

package:hydrated_bloc

❗storageDirectory es requerido al llamar a HydratedStorage.build

Justificación

Para hacer que package:hydrated_bloc sea un paquete puro de Dart, se eliminó la dependencia de package:path_provider y el parámetro storageDirectory al llamar a HydratedStorage.build es requerido y ya no tiene como valor predeterminado getTemporaryDirectory.

Resumen

v6.x.x

HydratedBloc.storage = await HydratedStorage.build();

v7.0.0

import 'package:path_provider/path_provider.dart';
...
HydratedBloc.storage = await HydratedStorage.build(
storageDirectory: await getTemporaryDirectory(),
);

v6.1.0

package:flutter_bloc

❗context.bloc y context.repository están obsoletos en favor de context.read y context.watch

Justificación

context.read, context.watch y context.select se añadieron para alinearse con la API existente de provider con la que muchos desarrolladores están familiarizados y para abordar problemas planteados por la comunidad. Para mejorar la seguridad del código y mantener la consistencia, context.bloc se deprecó porque puede ser reemplazado por context.read o context.watch dependiendo de si se usa directamente dentro de build.

context.watch

context.watch aborda la solicitud de tener un MultiBlocBuilder porque podemos observar varios blocs dentro de un solo Builder para renderizar la UI basada en múltiples estados:

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

context.select

context.select permite a los desarrolladores renderizar/actualizar la UI basada en una parte del estado de un bloc y aborda la solicitud de tener un buildWhen más simple.

final name = context.select((UserBloc bloc) => bloc.state.user.name);

El fragmento anterior nos permite acceder y reconstruir el widget solo cuando cambia el nombre del usuario actual.

context.read

Aunque parece que context.read es idéntico a context.bloc, hay algunas diferencias sutiles pero significativas. Ambos permiten acceder a un bloc con un BuildContext y no resultan en reconstrucciones; sin embargo, context.read no se puede llamar directamente dentro de un método build. Hay dos razones principales para usar context.bloc dentro de build:

  1. Para acceder al estado del bloc
@override
Widget build(BuildContext context) {
final state = context.bloc<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. En este escenario, se debe usar un BlocBuilder o context.watch.

@override
Widget build(BuildContext context) {
final state = context.watch<MyBloc>().state;
return Text('$state');
}

or

@override
Widget build(BuildContext context) {
return BlocBuilder<MyBloc, MyState>(
builder: (context, state) => Text('$state'),
);
}
  1. Para acceder al bloc y poder agregar un evento
@override
Widget build(BuildContext context) {
final bloc = context.bloc<MyBloc>();
return ElevatedButton(
onPressed: () => bloc.add(MyEvent()),
...
)
}

El uso anterior es ineficiente porque resulta en una búsqueda del bloc en cada reconstrucción cuando el bloc solo es necesario cuando el usuario toca el ElevatedButton. En este escenario, es preferible usar context.read para acceder al bloc directamente donde se necesita (en este caso, en el callback onPressed).

@override
Widget build(BuildContext context) {
return ElevatedButton(
onPressed: () => context.read<MyBloc>().add(MyEvent()),
...
)
}

Resumen

v6.0.x

@override
Widget build(BuildContext context) {
final bloc = context.bloc<MyBloc>();
return ElevatedButton(
onPressed: () => bloc.add(MyEvent()),
...
)
}

v6.1.x

@override
Widget build(BuildContext context) {
return ElevatedButton(
onPressed: () => context.read<MyBloc>().add(MyEvent()),
...
)
}

?> Si accedes a un bloc para agregar un evento, realiza el acceso al bloc usando context.read en el callback donde se necesita.

v6.0.x

@override
Widget build(BuildContext context) {
final state = context.bloc<MyBloc>().state;
return Text('$state');
}

v6.1.x

@override
Widget build(BuildContext context) {
final state = context.watch<MyBloc>().state;
return Text('$state');
}

?> Usa context.watch cuando accedas al estado del bloc para asegurar que el widget se reconstruya cuando el estado cambie.

v6.0.0

package:bloc

❗BlocObserver onError toma Cubit

Justificación

Debido a la integración de Cubit, onError ahora se comparte entre las instancias de Bloc y Cubit. Dado que Cubit es la base, BlocObserver aceptará un tipo Cubit en lugar de un tipo Bloc en la sobrescritura de onError.

v5.x.x

class MyBlocObserver extends BlocObserver {
@override
void onError(Bloc bloc, Object error, StackTrace stackTrace) {
super.onError(bloc, error, stackTrace);
}
}

v6.0.0

class MyBlocObserver extends BlocObserver {
@override
void onError(Cubit cubit, Object error, StackTrace stackTrace) {
super.onError(cubit, error, stackTrace);
}
}

❗Bloc no emite el último estado en la suscripción

Justificación

Este cambio se realizó para alinear Bloc y Cubit con el comportamiento incorporado de Stream en Dart. Además, conformar este comportamiento antiguo en el contexto de Cubit llevó a muchos efectos secundarios no deseados y, en general, complicó innecesariamente las implementaciones internas de otros paquetes como flutter_bloc y bloc_test (requiriendo skip(1), etc…).

v5.x.x

final bloc = MyBloc();
bloc.listen(print);

Anteriormente, el fragmento anterior mostraría el estado inicial del bloc seguido de los cambios de estado posteriores.

v6.x.x

En v6.0.0, el fragmento anterior no muestra el estado inicial y solo muestra los cambios de estado posteriores. El comportamiento anterior se puede lograr con lo siguiente:

final bloc = MyBloc();
print(bloc.state);
bloc.listen(print);

?> Nota: Este cambio solo afectará al código que dependa de suscripciones directas a blocs. Al usar BlocBuilder, BlocListener o BlocConsumer no habrá ningún cambio notable en el comportamiento.

package:bloc_test

❗MockBloc solo requiere el tipo de Estado

Justificación

No es necesario y elimina código adicional, además de hacer que MockBloc sea compatible con Cubit.

v5.x.x

class MockCounterBloc extends MockBloc<CounterEvent, int> implements CounterBloc {}

v6.0.0

class MockCounterBloc extends MockBloc<int> implements CounterBloc {}

❗whenListen solo requiere el tipo de Estado

Justificación

No es necesario y elimina código adicional, además de hacer que whenListen sea compatible con Cubit.

v5.x.x

whenListen<CounterEvent,int>(bloc, Stream.fromIterable([0, 1, 2, 3]));

v6.0.0

whenListen<int>(bloc, Stream.fromIterable([0, 1, 2, 3]));

❗blocTest does not require Event type

Justificación

No es necesario y elimina código adicional, además de hacer que blocTest sea compatible con Cubit.

v5.x.x

blocTest<CounterBloc, CounterEvent, int>(
'emits [1] when increment is called',
build: () async => CounterBloc(),
act: (bloc) => bloc.add(CounterEvent.increment),
expect: const <int>[1],
);

v6.0.0

blocTest<CounterBloc, int>(
'emits [1] when increment is called',
build: () => CounterBloc(),
act: (bloc) => bloc.add(CounterEvent.increment),
expect: const <int>[1],
);

❗blocTest skip por defecto es 0

Justificación

Dado que las instancias de bloc y cubit ya no emitirán el último estado para nuevas suscripciones, ya no era necesario que skip tuviera un valor predeterminado de 1.

v5.x.x

blocTest<CounterBloc, CounterEvent, int>(
'emits [0] when skip is 0',
build: () async => CounterBloc(),
skip: 0,
expect: const <int>[0],
);

v6.0.0

blocTest<CounterBloc, int>(
'emits [] when skip is 0',
build: () => CounterBloc(),
skip: 0,
expect: const <int>[],
);

El estado inicial de un bloc o cubit se puede probar con lo siguiente:

test('initial state is correct', () {
expect(MyBloc().state, InitialState());
});

❗blocTest hacer que build sea síncrono

Justificación

Anteriormente, build se hizo async para que se pudieran realizar varias preparaciones para poner el bloc bajo prueba en un estado específico. Ya no es necesario y también resuelve varios problemas debido a la latencia añadida entre la construcción y la suscripción internamente. En lugar de hacer una preparación asincrónica para poner un bloc en un estado deseado, ahora podemos establecer el estado del bloc encadenando emit con el estado deseado.

v5.x.x

blocTest<CounterBloc, CounterEvent, int>(
'emits [2] when increment is added',
build: () async {
final bloc = CounterBloc();
bloc.add(CounterEvent.increment);
await bloc.take(2);
return bloc;
}
act: (bloc) => bloc.add(CounterEvent.increment),
expect: const <int>[2],
);

v6.0.0

blocTest<CounterBloc, int>(
'emits [2] when increment is added',
build: () => CounterBloc()..emit(1),
act: (bloc) => bloc.add(CounterEvent.increment),
expect: const <int>[2],
);

package:flutter_bloc

❗El parámetro bloc de BlocBuilder se renombró a cubit

Justificación

Para que BlocBuilder pueda interoperar con instancias de bloc y cubit, el parámetro bloc se renombró a cubit (ya que Cubit es la clase base).

v5.x.x

BlocBuilder(
bloc: myBloc,
builder: (context, state) {...}
)

v6.0.0

BlocBuilder(
cubit: myBloc,
builder: (context, state) {...}
)

❗BlocListener parámetro bloc renombrado a cubit

Justificación

Para que BlocListener pueda interoperar con instancias de bloc y cubit, el parámetro bloc se renombró a cubit (ya que Cubit es la clase base).

v5.x.x

BlocListener(
bloc: myBloc,
listener: (context, state) {...}
)

v6.0.0

BlocListener(
cubit: myBloc,
listener: (context, state) {...}
)

❗BlocConsumer parámetro bloc renombrado a cubit

Justificación

Para que BlocConsumer pueda interoperar con instancias de bloc y cubit, el parámetro bloc se renombró a cubit (ya que Cubit es la clase base).

v5.x.x

BlocConsumer(
bloc: myBloc,
listener: (context, state) {...},
builder: (context, state) {...}
)

v6.0.0

BlocConsumer(
cubit: myBloc,
listener: (context, state) {...},
builder: (context, state) {...}
)

v5.0.0

package:bloc

❗initialState ha sido eliminado

Justificación

Como desarrollador, tener que sobrescribir initialState al crear un bloc presenta dos problemas principales:

  • El initialState del bloc puede ser dinámico y también puede ser referenciado en un momento posterior (incluso fuera del propio bloc). De alguna manera, esto puede verse como una filtración de información interna del bloc a la capa de UI.
  • Es verboso.

v4.x.x

class CounterBloc extends Bloc<CounterEvent, int> {
@override
int get initialState => 0;
...
}

v5.0.0

class CounterBloc extends Bloc<CounterEvent, int> {
CounterBloc() : super(0);
...
}

?> Para más información, consulta #1304

❗BlocDelegate renombrado a BlocObserver

Justificación

El nombre BlocDelegate no era una descripción precisa del papel que desempeñaba la clase. BlocDelegate sugiere que la clase juega un papel activo, mientras que en realidad el papel previsto del BlocDelegate era ser un componente pasivo que simplemente observa todos los blocs en una aplicación.

v4.x.x

class MyBlocDelegate extends BlocDelegate {
...
}

v5.0.0

class MyBlocObserver extends BlocObserver {
...
}

❗BlocSupervisor ha sido eliminado

Justificación

BlocSupervisor era otro componente que los desarrolladores debían conocer e interactuar con el único propósito de especificar un BlocDelegate personalizado. Con el cambio a BlocObserver, sentimos que mejoraba la experiencia del desarrollador al establecer el observador directamente en el propio bloc.

?> Este cambio también nos permitió desacoplar otros complementos de bloc como HydratedStorage del BlocObserver.

v4.x.x

BlocSupervisor.delegate = MyBlocDelegate();

v5.0.0

Bloc.observer = MyBlocObserver();

package:flutter_bloc

❗BlocBuilder condición renombrada a buildWhen

Justificación

Cuando se usa BlocBuilder, anteriormente podíamos especificar una condición para determinar si el builder debería reconstruirse.

BlocBuilder<MyBloc, MyState>(
buildWhen: (anterior, actual) {
// devuelve true/false para determinar si se debe llamar al builder
},
builder: (context, state) {...}
)

El nombre condition no es muy autoexplicativo u obvio y, más importante aún, cuando se interactúa con un BlocConsumer, la API se vuelve inconsistente porque los desarrolladores pueden proporcionar dos condiciones (una para builder y otra para listener). Como resultado, la API de BlocConsumer expone un buildWhen y listenWhen.

BlocConsumer<MyBloc, MyState>(
listenWhen: (anterior, actual) {
// devuelve true/false para determinar si se debe llamar al listener
},
listener: (context, state) {...},
buildWhen: (anterior, actual) {
// devuelve true/false para determinar si se debe llamar al builder
},
builder: (context, state) {...},
)

Para alinear la API y proporcionar una experiencia de desarrollador más consistente, condition fue renombrado a buildWhen.

v4.x.x

BlocBuilder<MyBloc, MyState>(
buildWhen: (anterior, actual) {
// devuelve true/false para determinar si se debe llamar al builder
},
builder: (context, state) {...}
)

v5.0.0

BlocBuilder<MyBloc, MyState>(
buildWhen: (anterior, actual) {
// devuelve true/false para determinar si se debe llamar al builder
},
builder: (context, state) {...}
)

❗BlocListener condición renombrada a listenWhen

Justificación

Por las mismas razones descritas anteriormente, la condición de BlocListener también fue renombrada.

v4.x.x

BlocListener<MyBloc, MyState>(
listenWhen: (anterior, actual) {
// devuelve true/false para determinar si se debe llamar al listener
},
listener: (context, state) {...}
)

v5.0.0

BlocListener<MyBloc, MyState>(
listenWhen: (anterior, actual) {
// devuelve true/false para determinar si se debe llamar al listener
},
listener: (context, state) {...}
)

package:hydrated_bloc

❗HydratedStorage y HydratedBlocStorage renombrados

Justificación

Para mejorar la reutilización del código entre hydrated_bloc y hydrated_cubit, la implementación concreta predeterminada de almacenamiento se renombró de HydratedBlocStorage a HydratedStorage. Además, la interfaz HydratedStorage se renombró de HydratedStorage a Storage.

v4.0.0

class MyHydratedStorage implements HydratedStorage {
...
}

v5.0.0

class MyHydratedStorage implements Storage {
...
}

❗HydratedStorage desacoplado de BlocDelegate

Justificación

Como se mencionó anteriormente, BlocDelegate fue renombrado a BlocObserver y se estableció directamente como parte del bloc a través de:

Bloc.observer = MyBlocObserver();

El siguiente cambio se realizó para:

  • Mantener la consistencia con la nueva API de observador de bloc
  • Mantener el almacenamiento limitado solo a HydratedBloc
  • Desacoplar el BlocObserver del Storage

v4.0.0

BlocSupervisor.delegate = await HydratedBlocDelegate.build();

v5.0.0

HydratedBloc.storage = await HydratedStorage.build();

❗Inicialización Simplificada

Justificación

Anteriormente, los desarrolladores tenían que llamar manualmente a super.initialState ?? DefaultInitialState() para configurar sus instancias de HydratedBloc. Esto es torpe y verboso y también incompatible con los cambios importantes en initialState en bloc. Como resultado, en la versión v5.0.0, la inicialización de HydratedBloc es idéntica a la inicialización normal de Bloc.

v4.0.0

class CounterBloc extends HydratedBloc<CounterEvent, int> {
@override
int get initialState => super.initialState ?? 0;
}

v5.0.0

class CounterBloc extends HydratedBloc<CounterEvent, int> {
CounterBloc() : super(0);
...
}