Guía de Migració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
.
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());}
BlocOverrides
eliminado en favor deBloc.observer
yBloc.transformer
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
.
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());}
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):
- https://github.com/flutter/flutter/issues/96939
- https://github.com/flutter/flutter/issues/94123
- https://github.com/flutter/flutter/issues/93676
lo cual afectó a muchos desarrolladores que usaban la biblioteca bloc:
- https://github.com/felangel/bloc/issues/3394
- https://github.com/felangel/bloc/issues/3350
- https://github.com/felangel/bloc/issues/3319
v8.0.x
void main() { BlocOverrides.runZoned( () { // ... }, blocObserver: CustomBlocObserver(), eventTransformer: customEventTransformer(), );}
v8.1.0
void main() { Bloc.observer = CustomBlocObserver(); Bloc.transformer = customEventTransformer();
// ...}
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
oEventTransformer
limitadas a diferentes partes de la aplicación. - Tener sobrescrituras de
BlocObserver
oEventTransformer
limitadas a un paquete.- Si un paquete dependía de
package:bloc
y registraba su propioBlocObserver
, cualquier consumidor del paquete tendría que sobrescribir elBlocObserver
del paquete o informar alBlocObserver
del paquete.
- Si un paquete dependía de
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(),);
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).
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}
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
.
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
.
mapEventToState
eliminado en favor deon<Event>
transformEvents
eliminado en favor de la APIEventTransformer
TransitionFunction
typedef eliminado en favor de la APIEventTransformer
listen
eliminado en favor destream.listen
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...}
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
.
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>(); ...}
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
@overrideStream<Transition<MyEvent, MyState>> transformEvents(events, transitionFn) { return events .debounceTime(const Duration(milliseconds: 300)) .flatMap(transitionFn);}
v7.2.0
/// Define un `EventTransformer` personalizadoEventTransformer<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)))}
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
@overrideStream<Transition<Event, State>> transformTransitions( Stream<Transition<Event, State>> transitions,) { return transitions.debounceTime(const Duration(milliseconds: 42));}
v7.2.0
@overrideStream<State> get stream => super.stream.debounceTime(const Duration(milliseconds: 42));
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) {...});
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(), ...);
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()), ...);
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()), ...);
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 {}
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.
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, ...)
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(),);
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
:
- Para acceder al estado del bloc
@overrideWidget 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
.
@overrideWidget build(BuildContext context) { final state = context.watch<MyBloc>().state; return Text('$state');}
or
@overrideWidget build(BuildContext context) { return BlocBuilder<MyBloc, MyState>( builder: (context, state) => Text('$state'), );}
- Para acceder al bloc y poder agregar un evento
@overrideWidget 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
).
@overrideWidget build(BuildContext context) { return ElevatedButton( onPressed: () => context.read<MyBloc>().add(MyEvent()), ... )}
Resumen
v6.0.x
@overrideWidget build(BuildContext context) { final bloc = context.bloc<MyBloc>(); return ElevatedButton( onPressed: () => bloc.add(MyEvent()), ... )}
v6.1.x
@overrideWidget 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
@overrideWidget build(BuildContext context) { final state = context.bloc<MyBloc>().state; return Text('$state');}
v6.1.x
@overrideWidget 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.
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); }}
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.
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 {}
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]));
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],);
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());});
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],);
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) {...})
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) {...})
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) {...})
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
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
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();
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) {...})
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) {...})
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 { ...}
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
delStorage
v4.0.0
BlocSupervisor.delegate = await HydratedBlocDelegate.build();
v5.0.0
HydratedBloc.storage = await HydratedStorage.build();
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);
...}