Preguntas Frecuentes
❔ Pregunta: Estoy emitiendo un estado en mi bloc pero la interfaz de usuario no se actualiza. ¿Qué estoy haciendo mal?
💡 Respuesta: Si estás usando Equatable, asegúrate de pasar todas las propiedades al getter props.
✅ BUENO
sealed class MyState extends Equatable { const MyState();}
final class StateA extends MyState { final String property;
const StateA(this.property);
@override List<Object> get props => [property]; // pass all properties to props}
❌ MALO
sealed class MyState extends Equatable { const MyState();}
final class StateA extends MyState { final String property;
const StateA(this.property);
@override List<Object> get props => [];}
sealed class MyState extends Equatable { const MyState();}
final class StateA extends MyState { final String property;
const StateA(this.property);
@override List<Object> get props => null;}
Además, asegúrate de emitir una nueva instancia del estado en tu bloc.
✅ BUENO
MyBloc() { on<MyEvent>((event, emit) { // always create a new instance of the state you are going to yield emit(state.copyWith(property: event.property)); });}
MyBloc() { on<MyEvent>((event, emit) { final data = _getData(event.info); // always create a new instance of the state you are going to yield emit(MyState(data: data)); });}
❌ MALO
MyBloc() { on<MyEvent>((event, emit) { // never modify/mutate state state.property = event.property; // never emit the same instance of state emit(state); });}
❔Pregunta: ¿Cuándo debo usar Equatable?
💡Respuesta:
MyBloc() { on<MyEvent>((event, emit) { emit(StateA('hi')); emit(StateA('hi')); });}
En el escenario anterior, si StateA
extiende Equatable
, solo ocurrirá un cambio de estado (el segundo emit será ignorado). En general, debes usar Equatable
si deseas optimizar tu código para reducir el número de reconstrucciones. No debes usar Equatable
si deseas que el mismo estado consecutivo desencadene múltiples transiciones.
Además, usar Equatable
facilita mucho las pruebas de blocs, ya que podemos esperar instancias específicas de estados de bloc en lugar de usar Matchers
o Predicates
.
blocTest( '...', build: () => MyBloc(), act: (bloc) => bloc.add(MyEvent()), expect: [ MyStateA(), MyStateB(), ],);
Sin Equatable
, la prueba anterior fallaría y necesitaría ser reescrita así:
blocTest( '...', build: () => MyBloc(), act: (bloc) => bloc.add(MyEvent()), expect: [ isA<MyStateA>(), isA<MyStateB>(), ],);
❔ Pregunta: ¿Cómo puedo manejar un error mientras sigo mostrando datos anteriores?
💡 Respuesta:
Esto depende en gran medida de cómo se haya modelado el estado del bloc. En casos donde los datos deben mantenerse incluso en presencia de un error, considera usar una sola clase de estado.
enum Status { initial, loading, success, failure }
class MyState { const MyState({ this.data = Data.empty, this.error = '', this.status = Status.initial, });
final Data data; final String error; final Status status;
MyState copyWith({Data data, String error, Status status}) { return MyState( data: data ?? this.data, error: error ?? this.error, status: status ?? this.status, ); }}
Esto permitirá que los widgets tengan acceso a las propiedades data
y error
simultáneamente y el bloc puede usar state.copyWith
para mantener los datos antiguos incluso cuando ocurra un error.
on<DataRequested>((event, emit) { try { final data = await _repository.getData(); emit(state.copyWith(status: Status.success, data: data)); } catch(error) { emit(state.copyWith(status: Status.failure, error: 'Something went wrong!')); }});
❔ Pregunta: ¿Cuál es la diferencia entre Bloc y Redux?
💡 Respuesta:
BLoC es un patrón de diseño que se define por las siguientes reglas:
- La entrada y salida del BLoC son Streams y Sinks simples.
- Las dependencias deben ser inyectables y agnósticas de la plataforma.
- No se permite la bifurcación de la plataforma.
- La implementación puede ser lo que quieras siempre que sigas las reglas anteriores.
Las pautas de la interfaz de usuario son:
- Cada componente “lo suficientemente complejo” tiene un BLoC correspondiente.
- Los componentes deben enviar entradas “tal como están”.
- Los componentes deben mostrar salidas lo más cerca posible de “tal como están”.
- Toda la bifurcación debe basarse en salidas booleanas simples del BLoC.
La biblioteca Bloc implementa el patrón de diseño BLoC y tiene como objetivo abstraer RxDart para simplificar la experiencia del desarrollador.
Los tres principios de Redux son:
- Fuente única de verdad
- El estado es de solo lectura
- Los cambios se realizan con funciones puras
La biblioteca bloc viola el primer principio; con bloc, el estado se distribuye a través de múltiples blocs. Además, no hay concepto de middleware en bloc y bloc está diseñado para facilitar los cambios de estado asincrónicos, permitiéndote emitir múltiples estados para un solo evento.
❔ Pregunta: ¿Cuál es la diferencia entre Bloc y Provider?
💡 Respuesta: provider
está diseñado para la inyección de dependencias (envuelve
InheritedWidget
). Aún necesitas averiguar cómo gestionar tu estado (a través de
ChangeNotifier
, Bloc
, Mobx
, etc…). La biblioteca Bloc usa provider
internamente para facilitar la provisión y acceso a blocs a lo largo del árbol de widgets.
❔ Pregunta: Cuando uso BlocProvider.of(context)
no puede encontrar el bloc.
¿Cómo puedo solucionar esto?
💡 Respuesta: No puedes acceder a un bloc desde el mismo contexto en el que fue
proporcionado, por lo que debes asegurarte de que BlocProvider.of()
se llame dentro de un
BuildContext
hijo.
✅ BUENO
@overrideWidget build(BuildContext context) { return BlocProvider( create: (_) => BlocA(), child: MyChild(); );}
class MyChild extends StatelessWidget { @override Widget build(BuildContext context) { return ElevatedButton( onPressed: () { final blocA = BlocProvider.of<BlocA>(context); ... }, ) ... }}
@overrideWidget build(BuildContext context) { return BlocProvider( create: (_) => BlocA(), child: Builder( builder: (context) => ElevatedButton( onPressed: () { final blocA = BlocProvider.of<BlocA>(context); ... }, ), ), );}
❌ MALO
@overrideWidget build(BuildContext context) { return BlocProvider( create: (_) => BlocA(), child: ElevatedButton( onPressed: () { final blocA = BlocProvider.of<BlocA>(context); ... } ) );}
❔ Pregunta: ¿Cómo debo estructurar mi proyecto?
💡 Respuesta: Aunque realmente no hay una respuesta correcta/incorrecta a esta pregunta, algunas referencias recomendadas son:
Lo más importante es tener una estructura de proyecto consistente e intencional.
❔ Pregunta: ¿Está bien agregar eventos dentro de un bloc?
💡 Respuesta: En la mayoría de los casos, los eventos deben agregarse externamente, pero en algunos casos selectos puede tener sentido que los eventos se agreguen internamente.
La situación más común en la que se utilizan eventos internos es cuando los cambios de estado deben ocurrir en respuesta a actualizaciones en tiempo real desde un repositorio. En estas situaciones, el repositorio es el estímulo para el cambio de estado en lugar de un evento externo como un toque de botón.
En el siguiente ejemplo, el estado de MyBloc
depende del usuario actual
que se expone a través del Stream<User>
del UserRepository
. MyBloc
escucha los cambios en el usuario actual y agrega un evento interno _UserChanged
cada vez que se emite un usuario desde el flujo de usuarios.
class MyBloc extends Bloc<MyEvent, MyState> { MyBloc({required UserRepository userRepository}) : super(...) { on<_UserChanged>(_onUserChanged); _userSubscription = userRepository.user.listen( (user) => add(_UserChanged(user)), ); }}
Al agregar un evento interno, también podemos especificar un transformer
personalizado
para el evento para determinar cómo se procesarán múltiples eventos _UserChanged
— por defecto se procesarán concurrentemente.
Se recomienda encarecidamente que los eventos internos sean privados. Esta es una forma explícita de señalar que un evento específico se usa solo dentro del bloc y evita que los componentes externos conozcan el evento.
sealed class MyEvent {}
// `EventA` is an external event.final class EventA extends MyEvent {}
// `EventB` is an internal event.// We are explicitly making `EventB` private so that it can only be used// within the bloc.final class _EventB extends MyEvent {}
Alternativamente, podemos definir un evento externo Started
y usar la API
emit.forEach
para manejar la reacción a las actualizaciones de usuarios en tiempo real:
class MyBloc extends Bloc<MyEvent, MyState> { MyBloc({required UserRepository userRepository}) : _userRepository = userRepository, super(...) { on<Started>(_onStarted); }
Future<void> _onStarted(Started event, Emitter<MyState> emit) { return emit.forEach( _userRepository.user, onData: (user) => MyState(...) ); }}
Los beneficios del enfoque anterior son:
- No necesitamos un evento interno
_UserChanged
- No necesitamos gestionar manualmente la
StreamSubscription
- Tenemos control total sobre cuándo el bloc se suscribe al flujo de actualizaciones de usuarios
Las desventajas del enfoque anterior son:
- No podemos
pausar
oreanudar
fácilmente la suscripción - Necesitamos exponer un evento público
Started
que debe agregarse externamente - No podemos usar un
transformer
personalizado para ajustar cómo reaccionamos a las actualizaciones de usuarios
❔ Pregunta: ¿Está bien exponer métodos públicos en mis instancias de bloc y cubit?
💡 Respuesta
Al crear un cubit, se recomienda exponer solo métodos públicos con el propósito de desencadenar cambios de estado. Como resultado, generalmente todos los métodos públicos en una instancia de cubit deben devolver void
o Future<void>
.
Al crear un bloc, se recomienda evitar exponer cualquier método público personalizado y en su lugar notificar al bloc de eventos llamando a add
.