เหตุการณ์ สถานะ และการตัดสินใจระหว่าง Cubit กับ Bloc
ตัดสินใจเลือกระหว่าง Cubit กับ Bloc และออกแบบการแปลงจากเหตุการณ์เป็นสถานะอย่างเป็นระเบียบ
เหตุการณ์ สถานะ และการตัดสินใจระหว่าง Cubit กับ Bloc เป็นบทเรียน Flutter Mobile Development ฟรีบน CoddyKit นี่คือบทเรียนที่ 1 จากทั้งหมด 4 บทเรียน คุณสามารถอ่านบทเรียนทั้งหมดด้านล่างฟรี — จากนั้นลองปฏิบัติด้วยตัวคุณเองในเบราว์เซอร์พร้อมตัวแก้ไขโค้ดในตัวและติวเตอร์ AI ตลอด 24/7 บทเรียนนี้เป็นส่วนหนึ่งของเส้นทางการเรียน Flutter Mobile Development และความก้าวหน้าของคุณจะซิงค์ข้ามเว็บและแอป CoddyKit คอร์ส Flutter Mobile Development มีบทเรียนทั้งหมด 4 บทเรียน
บางส่วนของบทเรียนนี้ยังไม่ได้รับการแปล และแสดงเป็นภาษาอังกฤษ
Two Tools, One Family
The flutter_bloc package ships two state-management primitives: Cubit and Bloc. Both extend the same base class and both emit a stream of states to your UI.
- Cubit exposes plain methods you call directly (e.g.
increment()). - Bloc reacts to events you add (e.g.
add(IncrementPressed())) and maps them to states.
This lesson teaches you how each one transforms input into State, and how to pick the right one for a given feature.
Cubit: Methods to States
A Cubit is the simpler primitive. You extend Cubit<T>, pass an initial state to super(...), and call emit(newState) inside methods to push a new state to listeners.
There is no event object and no mapping layer. The method is the API surface.
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
A Bloc separates the intent (an event) from the logic (a handler). You define event classes, then register handlers with on<Event> in the constructor. The UI never calls logic directly; it only adds events.
This indirection costs more boilerplate but gives you a single, traceable funnel for every state change.
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
A plain int works for a counter, but real features have multiple shapes: loading, success, failure. Model these as a sealed class hierarchy so the compiler forces your UI to handle every case.
- Sealed classes enable exhaustive
switchin Dart 3. - Each state carries only the data valid for that phase.
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 and Cubit only notify listeners when the new state is not equal to the previous one. By default Dart objects compare by identity, so two distinct instances with the same data are treated as different and trigger a rebuild.
Override equality (commonly with the equatable package) so value-equal states are deduplicated and your widgets stop rebuilding needlessly.
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
Most real handlers are asynchronous: fetch data, then emit. With a Bloc you can emit multiple times from one handler — first a loading state, then success or failure.
The handler signature gives you an emit callback rather than a return value precisely so you can stream several states during one event.
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.
คำถามที่พบบ่อย
บทเรียน “เหตุการณ์ สถานะ และการตัดสินใจระหว่าง Cubit กับ Bloc” ฟรีหรือไม่
ใช่ — ข้อความเต็มของ “เหตุการณ์ สถานะ และการตัดสินใจระหว่าง Cubit กับ Bloc” ฟรีให้อ่านที่นี่บนเว็บ เพื่อปฏิบัติแบบโต้ตอบ (ตัวแก้ไขโค้ดในตัวและติวเตอร์ AI ตลอด 24/7) และปลดล็อคส่วนที่เหลือของคอร์ส Flutter Mobile Development ให้อัปเกรดเป็น CoddyKit PRO คอร์ส Flutter Mobile Development มีบทเรียนทั้งหมด 4 บทเรียน
คุณจะเรียนรู้อะไรในบทเรียน “เหตุการณ์ สถานะ และการตัดสินใจระหว่าง Cubit กับ Bloc”
ตัดสินใจเลือกระหว่าง Cubit กับ Bloc และออกแบบการแปลงจากเหตุการณ์เป็นสถานะอย่างเป็นระเบียบ คุณปฏิบัติ Flutter Mobile Development ด้วยโค้ดที่ใช้งานได้จริงที่คุณเรียกใช้โดยตรงในเบราว์เซอร์ และติวเตอร์ AI ตลอด 24/7 ตอบคำถามของคุณขณะที่คุณไปผ่านบทเรียน
คุณต้องมีประสบการณ์ก่อนที่จะเริ่มเรียน Flutter Mobile Development หรือไม่
ไม่จำเป็นต้องมีประสบการณ์มาก่อน Flutter Mobile Development บน CoddyKit ออกแบบมาสำหรับผู้เริ่มต้นไปจนถึงผู้เรียนขั้นสูง คุณสามารถเริ่มต้นที่นี่หรือเริ่มจากตัวแรกและเรียนด้วยความเร็วของคุณเอง นี่คือบทเรียนที่ 1 จากทั้งหมด 4 บทเรียน
บทเรียน “เหตุการณ์ สถานะ และการตัดสินใจระหว่าง Cubit กับ Bloc” ใช้เวลานานแค่ไหน
บทเรียน CoddyKit ส่วนใหญ่ใช้เวลาประมาณ 5–10 นาที แต่ละบทเรียนจึงสั้นและเป็นแบบโต้ตอบ คุณสามารถก้าวหน้าอย่างต่อเนื่องและกลับมาเรียนต่อจากตรงที่เพิ่งหยุดบนเว็บและแอปได้เลย
ฉันเขียนและรันโค้ดในบทเรียน Flutter Mobile Development นี้ได้ไหม
ได้ บทเรียน Flutter Mobile Development ทุกบทมีตัวแก้ไขโค้ดในตัว คุณจึงเขียนและรันโค้ดจริงได้เลยในเบราว์เซอร์ และได้รับข้อเสนอแนะจาก AI ในทันที — ไม่ต้องติดตั้งในเครื่องของคุณ
บทเรียนทั้งหมดในหลักสูตรนี้
- เหตุการณ์ สถานะ และการตัดสินใจระหว่าง Cubit กับ Bloc
- ตัวแปลงสตรีมและการหน่วงเหตุการณ์ใน BLoC
- การคงอยู่ของสถานะด้วย HydratedBloc
- การทดสอบ BLoC ด้วย bloc_test และ Mocktail