Eventos, estados y decisión entre Cubit y Bloc
Decida entre Cubit y Bloc, y diseñe transformaciones limpias de eventos a estados.
Eventos, estados y decisión entre Cubit y Bloc es una lección gratuita de Flutter Mobile Development en CoddyKit. Esta es la lección 1 de 4. Puedes leer la lección completa abajo gratuitamente — luego la practicas en el navegador con un editor de código integrado y un tutor de IA 24/7. Forma parte de la ruta de aprendizaje de Flutter Mobile Development, y tu progreso se sincroniza en la web y la app de CoddyKit. El curso de Flutter Mobile Development incluye 4 lecciones en total.
Partes de esta lección aún no han sido traducidas y se muestran en inglés.
Two Tools, One Family
El paquete flutter_bloc incluye dos primitivas de gestión de estado: Cubit y Bloc. Ambos extienden la misma clase base y ambos emiten un flujo de estados a tu interfaz de usuario.
- Cubit expone métodos simples que llamas directamente (p. ej.,
increment()). - Bloc reacciona a eventos que añades (p. ej.,
add(IncrementPressed())) y los mapea a estados.
Esta lección te enseña cómo cada uno transforma la entrada en un State y cómo elegir el correcto para una funcionalidad determinada.
Cubit: Methods to States
Un Cubit es la primitiva más sencilla. Extiendes Cubit<T>, pasas un estado inicial a super(...) y llamas a emit(newState) dentro de los métodos para enviar un nuevo estado a los oyentes.
No hay un objeto de evento ni una capa de mapeo. El método es la superficie de la API.
import 'package:flutter_bloc/flutter_bloc.dart';
class CounterCubit extends Cubit<int> {
CounterCubit() : super(0);
void increment() => emit(state + 1);
void decrement() => emit(state - 1);
void reset() => emit(0);
}Bloc: Events to States
Un Bloc separa la intención (un evento) de la lógica (un controlador). Defines clases de eventos y luego registras los controladores con on<Event> en el constructor. La interfaz de usuario nunca llama a la lógica directamente; solo add (añade) eventos.
Esta indirección supone un mayor coste en código repetitivo (*boilerplate*), pero te proporciona un canal único y trazable para cada cambio de estado.
import 'package:flutter_bloc/flutter_bloc.dart';
sealed class CounterEvent {}
class IncrementPressed extends CounterEvent {}
class DecrementPressed extends CounterEvent {}
class CounterBloc extends Bloc<CounterEvent, int> {
CounterBloc() : super(0) {
on<IncrementPressed>((event, emit) => emit(state + 1));
on<DecrementPressed>((event, emit) => emit(state - 1));
}
}Modeling States Explicitly
Un int simple sirve para un contador, pero las funciones reales tienen múltiples formas: carga, éxito, error. Modélalas como una jerarquía de clases selladas (sealed class) para que el compilador obligue a tu interfaz de usuario a manejar cada caso.
- Las clases selladas permiten un
switchexhaustivo en Dart 3. - Cada estado solo contiene los datos válidos para esa fase.
sealed class ProfileState {
const ProfileState();
}
class ProfileLoading extends ProfileState {
const ProfileLoading();
}
class ProfileLoaded extends ProfileState {
final String name;
const ProfileLoaded(this.name);
}
class ProfileError extends ProfileState {
final String message;
const ProfileError(this.message);
}
void main() {
final ProfileState s = ProfileLoaded('Ada');
final label = switch (s) {
ProfileLoading() => 'Loading...',
ProfileLoaded(:final name) => 'Hello, $name',
ProfileError(:final message) => 'Error: $message',
};
print(label);
}Equatable: Avoiding Redundant Rebuilds
Bloc y Cubit solo notifican a los listeners cuando el nuevo estado no es igual al anterior. Por defecto, los objetos de Dart se comparan por identidad, por lo que dos instancias distintas con los mismos datos se tratan como diferentes y desencadenan una reconstrucción.
Sobrescribe la igualdad (comúnmente con el paquete equatable) para que los estados con el mismo valor se dedupliquen y tus widgets dejen de reconstruirse innecesariamente.
import 'package:equatable/equatable.dart';
class CartState extends Equatable {
final int itemCount;
final double total;
const CartState(this.itemCount, this.total);
@override
List<Object?> get props => [itemCount, total];
}
// emit(CartState(2, 19.98)) twice in a row notifies listeners only once.Async Work Inside a Handler
La mayoría de los manejadores reales son asíncronos: obtienen datos y luego emiten. Con un Bloc puedes emitir varias veces desde un solo manejador: primero un estado de carga, luego éxito o error.
La firma del manejador te proporciona una función de devolución de llamada emit en lugar de un valor de retorno precisamente para que puedas transmitir varios estados durante un mismo evento.
class ProfileBloc extends Bloc<ProfileEvent, ProfileState> {
final ProfileRepo repo;
ProfileBloc(this.repo) : super(const ProfileLoading()) {
on<ProfileRequested>((event, emit) async {
emit(const ProfileLoading());
try {
final name = await repo.fetchName(event.id);
emit(ProfileLoaded(name));
} catch (e) {
emit(ProfileError(e.toString()));
}
});
}
}Event Transformers: The Bloc-Only Superpower
This is the feature that most often decides the question. With a Bloc you control how concurrent events are processed by passing a transformer to on<Event> (from the bloc_concurrency package):
concurrent()— handle all events in parallel (default).sequential()— one at a time, in order.droppable()— ignore new events while one is running (great for buttons).restartable()— cancel the in-flight handler when a newer event arrives (great for search).
Cubit has no event stream, so it cannot do this without writing the debounce/throttle logic by hand.
import 'package:bloc_concurrency/bloc_concurrency.dart';
import 'package:stream_transform/stream_transform.dart';
EventTransformer<E> debounce<E>(Duration d) {
return (events, mapper) => events.debounce(d).switchMap(mapper);
}
class SearchBloc extends Bloc<SearchEvent, SearchState> {
SearchBloc() : super(const SearchInitial()) {
on<QueryChanged>(
_onQueryChanged,
transformer: debounce(const Duration(milliseconds: 300)),
);
}
Future<void> _onQueryChanged(QueryChanged e, Emitter emit) async {
// runs at most once per 300ms of typing
}
}Observing Transitions for Debugging
Because a Bloc funnels every change through events, it can report a full Transition: the current state, the triggering event, and the next state. Override onTransition (or use a global BlocObserver) to log this funnel.
A Cubit only sees Change (current and next state) — there is no event to log, because there is no event. This is why Bloc is favored for features that need an audit trail.
import 'package:flutter_bloc/flutter_bloc.dart';
class AppObserver extends BlocObserver {
@override
void onTransition(Bloc bloc, Transition transition) {
super.onTransition(bloc, transition);
print('${bloc.runtimeType}: ${transition.event} '
'=> ${transition.nextState}');
}
}
void main() {
Bloc.observer = AppObserver();
}The Decision Rule
A practical heuristic the Bloc maintainers themselves recommend: start with a Cubit and reach for a Bloc only when you need what a Bloc adds.
Choose Cubit when:
- The logic is simple direct method calls (toggles, counters, form fields).
- You do not need to debounce/throttle/drop concurrent inputs.
- You value less boilerplate and easier onboarding.
Choose Bloc when:
- You need event transformers (debounce search, droppable submit).
- You want a traceable event log for analytics or debugging.
- Many distinct inputs map to one feature and you want them documented as event types.
Same Feature, Both Ways
Compare a toggle written as a Cubit versus a Bloc. The Cubit is shorter and reads top-to-bottom; the Bloc adds an event type and a handler. For a pure toggle, the Cubit is the better choice — the Bloc machinery buys you nothing here.
// Cubit version
class ThemeCubit extends Cubit<bool> {
ThemeCubit() : super(false);
void toggle() => emit(!state);
}
// Bloc version (same behavior, more ceremony)
sealed class ThemeEvent {}
class ThemeToggled extends ThemeEvent {}
class ThemeBloc extends Bloc<ThemeEvent, bool> {
ThemeBloc() : super(false) {
on<ThemeToggled>((e, emit) => emit(!state));
}
}Designing Clean Event-to-State Transformations
Whichever you pick, keep transformations clean:
- States are immutable; use
copyWithto derive the next state instead of mutating. - One event (or method) should map to a coherent set of emitted states, never to UI navigation or side effects you cannot trace.
- Keep I/O in a repository; the Bloc/Cubit only orchestrates and emits.
class FormState {
final String email;
final bool submitting;
const FormState({this.email = '', this.submitting = false});
FormState copyWith({String? email, bool? submitting}) => FormState(
email: email ?? this.email,
submitting: submitting ?? this.submitting,
);
}
void main() {
const start = FormState();
final next = start.copyWith(submitting: true);
print('${next.email}|${next.submitting}'); // |true
}Quick Check
A search field must issue a network request as the user types, but only after they pause for 300ms, cancelling any in-flight request when a newer keystroke arrives. Which choice best fits and why?
Recap
You learned how each primitive turns input into state and how to choose:
- Cubit = methods call
emitdirectly. Less boilerplate; ideal for toggles, counters, and simple forms. - Bloc = events are
added and mapped viaon<Event>. Buys you event transformers (debounce/throttle/droppable/restartable) and a traceable Transition log. - Model states as sealed, immutable classes; use
Equatableto dedupe rebuilds andcopyWithto derive next states. - Rule of thumb: start with a Cubit; upgrade to a Bloc only when you need concurrency control or an event audit trail.
Preguntas frecuentes
¿La lección «Eventos, estados y decisión entre Cubit y Bloc» es gratis?
Sí — el texto completo de «Eventos, estados y decisión entre Cubit y Bloc» es gratis para leer aquí en la web. Para practicarla de forma interactiva (editor de código integrado y tutor de IA 24/7) y desbloquear el resto del curso de Flutter Mobile Development, actualiza a CoddyKit PRO. El curso de Flutter Mobile Development incluye 4 lecciones en total.
¿Qué aprenderé en «Eventos, estados y decisión entre Cubit y Bloc»?
Decida entre Cubit y Bloc, y diseñe transformaciones limpias de eventos a estados. Practicas Flutter Mobile Development con código real que ejecutas directamente en el navegador, y un tutor de IA 24/7 responde tus preguntas mientras trabajas en la lección.
¿Necesito experiencia previa para empezar Flutter Mobile Development?
No se requiere experiencia previa. Flutter Mobile Development en CoddyKit está estructurado para principiantes hasta estudiantes avanzados, así que puedes empezar aquí o desde el inicio y avanzar a tu ritmo. Esta es la lección 1 de 4.
¿Cuánto tiempo toma la lección «Eventos, estados y decisión entre Cubit y Bloc»?
La mayoría de las lecciones de CoddyKit toman alrededor de 5–10 minutos. Cada una es compacta e interactiva, así que avanzas constantemente y retomas exactamente por donde dejaste en la web y la app.
¿Puedo escribir y ejecutar código en esta lección de Flutter Mobile Development?
Sí. Cada lección de Flutter Mobile Development incluye un editor de código integrado, así que escribes y ejecutas código real directamente en tu navegador y obtienes retroalimentación instantánea de IA — sin configuración local necesaria.
Todas las lecciones de este curso
- Eventos, estados y decisión entre Cubit y Bloc
- Transformadores de streams y debounce de eventos en BLoC
- Persistencia del estado con HydratedBloc
- Pruebas de BLoC con bloc_test y Mocktail