Saltearse al contenido

Preguntas Frecuentes

Estado No Actualizado

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

my_state.dart
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

my_state.dart
sealed class MyState extends Equatable {
const MyState();
}
final class StateA extends MyState {
final String property;
const StateA(this.property);
@override
List<Object> get props => [];
}
my_state.dart
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

my_bloc.dart
MyBloc() {
on<MyEvent>((event, emit) {
// always create a new instance of the state you are going to yield
emit(state.copyWith(property: event.property));
});
}
my_bloc.dart
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

my_bloc.dart
MyBloc() {
on<MyEvent>((event, emit) {
// never modify/mutate state
state.property = event.property;
// never emit the same instance of state
emit(state);
});
}

Cuándo usar Equatable

Pregunta: ¿Cuándo debo usar Equatable?

💡Respuesta:

my_bloc.dart
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.

my_bloc_test.dart
blocTest(
'...',
build: () => MyBloc(),
act: (bloc) => bloc.add(MyEvent()),
expect: [
MyStateA(),
MyStateB(),
],
);

Sin Equatable, la prueba anterior fallaría y necesitaría ser reescrita así:

my_bloc_test.dart
blocTest(
'...',
build: () => MyBloc(),
act: (bloc) => bloc.add(MyEvent()),
expect: [
isA<MyStateA>(),
isA<MyStateB>(),
],
);

Manejo de Errores

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.

my_state.dart
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.

my_bloc.dart
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!'));
}
});

Bloc vs. Redux

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:

  1. La entrada y salida del BLoC son Streams y Sinks simples.
  2. Las dependencias deben ser inyectables y agnósticas de la plataforma.
  3. No se permite la bifurcación de la plataforma.
  4. La implementación puede ser lo que quieras siempre que sigas las reglas anteriores.

Las pautas de la interfaz de usuario son:

  1. Cada componente “lo suficientemente complejo” tiene un BLoC correspondiente.
  2. Los componentes deben enviar entradas “tal como están”.
  3. Los componentes deben mostrar salidas lo más cerca posible de “tal como están”.
  4. 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:

  1. Fuente única de verdad
  2. El estado es de solo lectura
  3. 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.

Bloc vs. Provider

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.

BlocProvider.of() No Encuentra el Bloc

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

my_widget.dart
@override
Widget 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);
...
},
)
...
}
}
my_widget.dart
@override
Widget build(BuildContext context) {
return BlocProvider(
create: (_) => BlocA(),
child: Builder(
builder: (context) => ElevatedButton(
onPressed: () {
final blocA = BlocProvider.of<BlocA>(context);
...
},
),
),
);
}

MALO

my_widget.dart
@override
Widget build(BuildContext context) {
return BlocProvider(
create: (_) => BlocA(),
child: ElevatedButton(
onPressed: () {
final blocA = BlocProvider.of<BlocA>(context);
...
}
)
);
}

Estructura del Proyecto

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.

Agregar Eventos dentro de un Bloc

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.

my_bloc.dart
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.

my_event.dart
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:

my_bloc.dart
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 o reanudar 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

Exponer Métodos Públicos

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.