Guía de Migración
v10.0.0
Sección titulada «v10.0.0»package:bloc_test
Sección titulada «package:bloc_test»❗✨ Desacoplar blocTest
de BlocBase
Sección titulada «❗✨ Desacoplar blocTest de BlocBase»Justificación
Sección titulada «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
Sección titulada «package:hydrated_bloc»❗✨ Soporte para WebAssembly
Sección titulada «❗✨ Soporte para WebAssembly»Justificación
Sección titulada «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());}
package:bloc
Sección titulada «package:bloc»❗🧹 Eliminar APIs Obsoletas
Sección titulada «❗🧹 Eliminar APIs Obsoletas»Resumen
Sección titulada «Resumen»BlocOverrides
eliminado en favor deBloc.observer
yBloc.transformer
❗✨ Introducir nueva Interfaz EmittableStateStreamableSource
Sección titulada «❗✨ Introducir nueva Interfaz EmittableStateStreamableSource»Justificación
Sección titulada «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
Sección titulada «package:hydrated_bloc»✨ Reintroducir la API HydratedBloc.storage
Sección titulada «✨ Reintroducir la API HydratedBloc.storage»Justificación
Sección titulada «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());}
package:bloc
Sección titulada «package:bloc»✨ Reintroducir las APIs Bloc.observer
y Bloc.transformer
Sección titulada «✨ Reintroducir las APIs Bloc.observer y Bloc.transformer»Justificación
Sección titulada «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):
- 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();
// ...}
package:bloc
Sección titulada «package:bloc»❗✨ Introducir nueva API BlocOverrides
Sección titulada «❗✨ Introducir nueva API BlocOverrides»Justificación
Sección titulada «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
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(),);
❗✨ Mejorar el Manejo y Reporte de Errores
Sección titulada «❗✨ Mejorar el Manejo y Reporte de Errores»Justificación
Sección titulada «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
Sección titulada «❗🧹 Hacer BlocObserver abstracto»Justificación
Sección titulada «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
Sección titulada «❗✨ add lanza StateError si el Bloc está cerrado»Justificación
Sección titulada «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
Sección titulada «❗✨ emit lanza StateError si el Bloc está cerrado»Justificación
Sección titulada «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
Sección titulada «❗🧹 Eliminar APIs Obsoletas»Resumen
Sección titulada «Resumen»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
package:bloc_test
Sección titulada «package:bloc_test»✨ MockBloc
y MockCubit
ya no requieren registerFallbackValue
Sección titulada «✨ MockBloc y MockCubit ya no requieren registerFallbackValue»Resumen
Sección titulada «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
Sección titulada «package:hydrated_bloc»❗✨ Introducir nueva API HydratedBlocOverrides
Sección titulada «❗✨ Introducir nueva API HydratedBlocOverrides»Justificación
Sección titulada «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
.
package:bloc
Sección titulada «package:bloc»✨ Introducir nueva API on<Event>
Sección titulada «✨ Introducir nueva API on<Event>»Justificación
Sección titulada «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
Sección titulada «✨ Introducir nueva API EventTransformer»Justificación
Sección titulada «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
@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)))}
⚠️ Marcar como obsoleta la API transformTransitions
Sección titulada «⚠️ Marcar como obsoleta la API transformTransitions»Justificación
Sección titulada «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
@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));
package:bloc
Sección titulada «package:bloc»❗ Bloc y Cubit extienden BlocBase
Sección titulada «❗ Bloc y Cubit extienden BlocBase»Justificación
Sección titulada «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
Sección titulada «package:bloc_test»❗seed devuelve una función para soportar valores dinámicos
Sección titulada «❗seed devuelve una función para soportar valores dinámicos»Justificación
Sección titulada «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
Sección titulada «❗expect devuelve una función para soportar valores dinámicos y soporte de matchers»Justificación
Sección titulada «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
Sección titulada «❗errors devuelve una función para soportar valores dinámicos y soporte de matchers»Justificación
Sección titulada «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
Sección titulada «❗MockBloc y MockCubit»Justificación
Sección titulada «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
Sección titulada «❗Integración con Mocktail»Justificación
Sección titulada «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
Sección titulada «package:flutter_bloc»❗ renombrar el parámetro cubit
a bloc
Sección titulada «❗ renombrar el parámetro cubit a bloc»Justificación
Sección titulada «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
Sección titulada «package:hydrated_bloc»❗storageDirectory es requerido al llamar a HydratedStorage.build
Sección titulada «❗storageDirectory es requerido al llamar a HydratedStorage.build»Justificación
Sección titulada «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(),);
package:flutter_bloc
Sección titulada «package:flutter_bloc»❗context.bloc y context.repository están obsoletos en favor de context.read y context.watch
Sección titulada «❗context.bloc y context.repository están obsoletos en favor de context.read y context.watch»Justificación
Sección titulada «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
:
- 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.
package:bloc
Sección titulada «package:bloc»❗BlocObserver onError toma Cubit
Sección titulada «❗BlocObserver onError toma Cubit»Justificación
Sección titulada «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
Sección titulada «❗Bloc no emite el último estado en la suscripción»Justificación
Sección titulada «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
Sección titulada «package:bloc_test»❗MockBloc solo requiere el tipo de Estado
Sección titulada «❗MockBloc solo requiere el tipo de Estado»Justificación
Sección titulada «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
Sección titulada «❗whenListen solo requiere el tipo de Estado»Justificación
Sección titulada «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
Sección titulada «❗blocTest does not require Event type»Justificación
Sección titulada «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
Sección titulada «❗blocTest skip por defecto es 0»Justificación
Sección titulada «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
Sección titulada «❗blocTest hacer que build sea síncrono»Justificación
Sección titulada «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
Sección titulada «package:flutter_bloc»❗El parámetro bloc
de BlocBuilder se renombró a cubit
Sección titulada «❗El parámetro bloc de BlocBuilder se renombró a cubit»Justificación
Sección titulada «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
Sección titulada «❗BlocListener parámetro bloc renombrado a cubit»Justificación
Sección titulada «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
Sección titulada «❗BlocConsumer parámetro bloc renombrado a cubit»Justificación
Sección titulada «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) {...})
package:bloc
Sección titulada «package:bloc»❗initialState ha sido eliminado
Sección titulada «❗initialState ha sido eliminado»Justificación
Sección titulada «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
Sección titulada «❗BlocDelegate renombrado a BlocObserver»Justificación
Sección titulada «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
Sección titulada «❗BlocSupervisor ha sido eliminado»Justificación
Sección titulada «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
Sección titulada «package:flutter_bloc»❗BlocBuilder condición renombrada a buildWhen
Sección titulada «❗BlocBuilder condición renombrada a buildWhen»Justificación
Sección titulada «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
Sección titulada «❗BlocListener condición renombrada a listenWhen»Justificación
Sección titulada «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
Sección titulada «package:hydrated_bloc»❗HydratedStorage y HydratedBlocStorage renombrados
Sección titulada «❗HydratedStorage y HydratedBlocStorage renombrados»Justificación
Sección titulada «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
Sección titulada «❗HydratedStorage desacoplado de BlocDelegate»Justificación
Sección titulada «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
delStorage
v4.0.0
BlocSupervisor.delegate = await HydratedBlocDelegate.build();
v5.0.0
HydratedBloc.storage = await HydratedStorage.build();
❗Inicialización Simplificada
Sección titulada «❗Inicialización Simplificada»Justificación
Sección titulada «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);
...}