- Hiểu mô hình BLoC: Event in, State out qua Stream.
- Phân biệt Cubit vs Bloc — khi nào dùng cái nào.
- Sử dụng
BlocProvider,BlocBuilder,BlocListener,BlocConsumer. - Thiết kế State class theo pattern: sealed class với multiple state types.
- Test Bloc/Cubit độc lập với widget.
MultiBlocProvidercho nested provider.BlocSelectorcho rebuild optimization.- Pattern repository → bloc → view.
1. Pattern BLoC là gì?
Business Logic Component. Tách logic ra khỏi UI:
- Input: Event (Stream input).
- Output: State (Stream output).
- UI: subscribe state stream + dispatch event.
2. Cubit vs Bloc
| Cubit | Bloc | |
|---|---|---|
| Input | Method call trực tiếp (cubit.increment()) | Event class (add(IncrementPressed())) |
| Output | emit(newState) | emit(newState) trong handler on<E> |
| Verbose? | Ít hơn | Nhiều hơn (event class) |
| Trace history | Không event log | Có (Bloc observer log event/state) |
| Khi dùng | 90% case | Chain event phức tạp, analytics, time-travel debug |
3. Setup
flutter pub add flutter_bloc
flutter pub add --dev bloc_test # cho test chương 14
4. Counter Cubit — đơn giản nhất
import 'package:flutter_bloc/flutter_bloc.dart';
class CounterCubit extends Cubit<int> {
CounterCubit() : super(0); // initial state = 0
void increment() => emit(state + 1);
void decrement() => emit(state - 1);
void reset() => emit(0);
}
Wire vào UI:
BlocProvider(
create: (_) => CounterCubit(),
child: CounterPage(),
)
class CounterPage extends StatelessWidget {
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(title: const Text('Counter')),
body: Center(
child: BlocBuilder<CounterCubit, int>(
builder: (context, count) => Text('$count', style: Theme.of(context).textTheme.displayLarge),
),
),
floatingActionButton: FloatingActionButton(
onPressed: () => context.read<CounterCubit>().increment(),
child: const Icon(Icons.add),
),
);
}
}
5. Bloc — variant đầy đủ với Event
// Events
sealed class CounterEvent {}
class IncrementPressed extends CounterEvent {}
class DecrementPressed extends CounterEvent {}
class ResetPressed extends CounterEvent {}
// Bloc
class CounterBloc extends Bloc<CounterEvent, int> {
CounterBloc() : super(0) {
on<IncrementPressed>((event, emit) => emit(state + 1));
on<DecrementPressed>((event, emit) => emit(state - 1));
on<ResetPressed>((event, emit) => emit(0));
}
}
// Dispatch
context.read<CounterBloc>().add(IncrementPressed());
- Cubit — simple action: counter, toggle, form field. Method clear hơn event.
- Bloc — complex flow: auth (LoginPressed, LogoutPressed, TokenExpired, BiometricUnlocked), shopping (AddItem, RemoveItem, ApplyCoupon, Checkout).
- Bloc cho time-travel debug + analytics — log mọi event.
6. BlocBuilder
BlocBuilder<CounterCubit, int>(
builder: (context, count) => Text('$count'),
)
// Với buildWhen — control khi nào rebuild
BlocBuilder<CounterCubit, int>(
buildWhen: (prev, curr) => curr % 10 == 0, // chỉ rebuild khi % 10
builder: (context, count) => Text('$count'),
)
7. BlocListener — không rebuild, chỉ side effect
BlocListener<AuthCubit, AuthState>(
listenWhen: (prev, curr) => curr is Authenticated && prev is! Authenticated,
listener: (context, state) {
context.go('/home'); // navigate
},
child: LoginForm(),
)
Khác BlocBuilder: không rebuild UI, chỉ trigger callback. Dùng cho:
- Navigate khi state đổi.
- Show snackbar / dialog.
- Trigger analytics event.
8. BlocConsumer — combine Builder + Listener
BlocConsumer<LoginCubit, LoginState>(
listener: (context, state) {
if (state is LoginError) {
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(content: Text(state.message)),
);
}
if (state is LoginSuccess) context.go('/home');
},
builder: (context, state) => switch (state) {
LoginInitial() => const LoginForm(),
LoginLoading() => const CircularProgressIndicator(),
LoginError() => LoginForm(error: state.message),
LoginSuccess() => const SizedBox(),
},
)
9. BlocSelector — optimization
Rebuild chỉ khi subset state đổi:
// Cart có 10 field. UI chỉ care totalItems.
BlocSelector<CartBloc, CartState, int>(
selector: (state) => state.totalItems,
builder: (context, totalItems) => Badge(label: Text('$totalItems')),
)
// Rebuild chỉ khi totalItems thay đổi, không khi item detail / total price đổi.
10. read vs watch
// read — lấy reference, KHÔNG subscribe rebuild
ElevatedButton(
onPressed: () => context.read<CounterCubit>().increment(),
child: const Text('+1'),
)
// watch — subscribe rebuild khi state đổi
@override
Widget build(BuildContext context) {
final count = context.watch<CounterCubit>().state;
return Text('$count');
}
- onPressed callback:
read— không cần rebuild khi user tap. - Trong build():
watchhoặcBlocBuilder— cần rebuild.
11. Sealed state class — pattern chuẩn
sealed class AuthState {
const AuthState();
}
class AuthInitial extends AuthState {
const AuthInitial();
}
class AuthLoading extends AuthState {
const AuthLoading();
}
class Authenticated extends AuthState {
final User user;
const Authenticated(this.user);
}
class Unauthenticated extends AuthState {
final String? error;
const Unauthenticated({this.error});
}
// Trong BlocBuilder — switch exhaustive
BlocBuilder<AuthCubit, AuthState>(
builder: (context, state) => switch (state) {
AuthInitial() => const SplashScreen(),
AuthLoading() => const LoadingScreen(),
Authenticated(:var user) => HomeScreen(user: user),
Unauthenticated(:var error) => LoginScreen(error: error),
},
)
Sealed class (Dart 3) + exhaustive switch → compiler ép cover hết case. Thêm state mới → quên handle UI → compile fail. Đây là type-safety mạnh nhất bạn có cho state machine.
12. Async event trong Bloc
class UserBloc extends Bloc<UserEvent, UserState> {
final UserRepository _repo;
UserBloc(this._repo) : super(UserInitial()) {
on<LoadUser>(_onLoad);
}
Future<void> _onLoad(LoadUser event, Emitter<UserState> emit) async {
emit(UserLoading());
try {
final user = await _repo.fetchUser(event.id);
emit(UserLoaded(user));
} on ApiException catch (e) {
emit(UserError(e.message));
}
}
}
13. MultiBlocProvider — nested
MultiBlocProvider(
providers: [
BlocProvider(create: (_) => AuthCubit(AuthRepository())),
BlocProvider(create: (_) => CartBloc()),
BlocProvider(create: (_) => ThemeCubit()),
],
child: MyApp(),
)
14. Repository pattern
// Interface
abstract class UserRepository {
Future<User> fetchUser(int id);
}
// Concrete impl
class UserRepositoryImpl implements UserRepository {
final Dio _dio;
UserRepositoryImpl(this._dio);
@override
Future<User> fetchUser(int id) async {
final r = await _dio.get('/users/$id');
return User.fromJson(r.data);
}
}
// Inject vào Bloc
BlocProvider<UserBloc>(
create: (ctx) => UserBloc(ctx.read<UserRepository>()),
child: UserPage(),
)
// Wrap với RepositoryProvider ở root
RepositoryProvider<UserRepository>(
create: (_) => UserRepositoryImpl(Dio()),
child: MyApp(),
)
15. Test Bloc — basic
import 'package:test/test.dart';
import 'package:bloc_test/bloc_test.dart';
void main() {
group('CounterCubit', () {
test('initial state is 0', () {
expect(CounterCubit().state, 0);
});
blocTest<CounterCubit, int>(
'emits [1] when increment is called',
build: () => CounterCubit(),
act: (cubit) => cubit.increment(),
expect: () => [1],
);
blocTest<CounterCubit, int>(
'emits [1, 2] when increment called twice',
build: () => CounterCubit(),
act: (cubit) {
cubit.increment();
cubit.increment();
},
expect: () => [1, 2],
);
});
}
Chương 14 (Testing) sẽ deep dive thêm.
16. Pitfall: closure capture stale state
// ❌ Sai
on<IncrementPressed>((event, emit) async {
final current = state; // capture tại đây
await Future.delayed(Duration(seconds: 1));
emit(current + 1); // có thể stale nếu state đã đổi trong khi await
});
// ✅ Đúng — luôn read state mới qua getter
on<IncrementPressed>((event, emit) async {
await Future.delayed(Duration(seconds: 1));
emit(state + 1); // state là getter, luôn current
});
17. Bài tập
CounterCubit + reset + test
Counter app dùng Cubit. Add reset button. Test với bloc_test (3 case: initial, increment, reset).
💡 Gợi ý đáp án
Tham khảo section 4 (Counter Cubit) + section 15 (test). Test reset: act: (c) { c.increment(); c.reset(); }, expect: () => [1, 0].
LoginCubit flow
Login form 2 field (email, password) → Cubit emit Loading → mock delay 1s → emit Success hoặc Error. UI show loading spinner, error snackbar, navigate khi success.
💡 Gợi ý đáp án
sealed class LoginState { const LoginState(); }
class LoginInitial extends LoginState { const LoginInitial(); }
class LoginLoading extends LoginState { const LoginLoading(); }
class LoginSuccess extends LoginState { const LoginSuccess(); }
class LoginError extends LoginState {
final String message;
const LoginError(this.message);
}
class LoginCubit extends Cubit<LoginState> {
LoginCubit() : super(const LoginInitial());
Future<void> submit(String email, String password) async {
emit(const LoginLoading());
await Future.delayed(const Duration(seconds: 1));
if (email == 'admin' && password == '1234') {
emit(const LoginSuccess());
} else {
emit(const LoginError('Sai email/password'));
}
}
}
Refactor Cubit → Bloc
Refactor bài 2 từ Cubit sang Bloc (explicit Event class). So sánh code length + clarity.
💡 Gợi ý đáp án
sealed class LoginEvent {}
class LoginSubmitted extends LoginEvent {
final String email;
final String password;
LoginSubmitted(this.email, this.password);
}
class LoginBloc extends Bloc<LoginEvent, LoginState> {
LoginBloc() : super(const LoginInitial()) {
on<LoginSubmitted>((e, emit) async {
emit(const LoginLoading());
await Future.delayed(const Duration(seconds: 1));
if (e.email == 'admin' && e.password == '1234') {
emit(const LoginSuccess());
} else {
emit(const LoginError('Sai'));
}
});
}
}
Bloc dài hơn nhưng explicit. Cubit ngắn hơn — recommend cho most case.
CartBloc với 5 scenarios
Cart Bloc với event AddItem/RemoveItem/Clear. State có items list + total. Test với 5 scenario.
💡 Gợi ý đáp án
State class với List<Item> + computed total getter. Test: add 1 item, remove, clear, add same item twice, add+remove combo.
AuthBloc với MultiBlocProvider
Auth global: MultiBlocProvider wrap app. AuthBloc emit Authenticated/Unauthenticated. BlocListener navigate khi state đổi. Logout button qua bất kỳ widget.
💡 Gợi ý đáp án
Tham khảo section 11 (sealed AuthState) + section 7 (BlocListener navigate) + section 13 (MultiBlocProvider).
18. Quiz
Cubit và Bloc khác chính ở điểm?
Xem đáp án
Đáp án: Cubit emit trực tiếp qua method. Bloc qua event class + on<E> handler. Bloc verbose hơn nhưng explicit cho event chain.
context.read rebuild widget không?
Xem đáp án
Đáp án: Không. Chỉ lấy reference. watch mới subscribe rebuild.
BlocSelector vs BlocBuilder?
Xem đáp án
Đáp án: Selector rebuild chỉ khi selector output (subset state) đổi. Builder rebuild mỗi state change. Selector cho perf optimization khi state class lớn.
State class nên là immutable hay mutable?
Xem đáp án
Đáp án: Immutable. Bloc compare oldState vs newState qua == để decide rebuild. Mutable thì == always true → không rebuild.
emit(state) (same instance) có rebuild không?
Xem đáp án
Đáp án: Không, nếu == trả true. Bloc skip emit cùng giá trị (performance optimization).
Sealed state + switch exhaustive — analyzer guarantee gì?
Xem đáp án
Đáp án: Mọi state case có UI handle. Thêm state mới mà quên handle ở UI → compile lỗi. Type-safety mạnh nhất cho state machine.
Bloc nên dispose không?
Xem đáp án
Đáp án: BlocProvider tự close() Bloc khi unmount. Tự tạo qua BlocProvider.value phải close thủ công.
Test Bloc cần Flutter context không?
Xem đáp án
Đáp án: Không. Bloc/Cubit pure Dart, test trong unit test thuần. Không cần widget tree, không cần WidgetTester.
19. Tổng kết
- ✅ Pattern BLoC: Event in → Bloc process → State out.
- ✅ Cubit (simple) vs Bloc (event class).
- ✅
BlocProvider,BlocBuilder,BlocListener,BlocConsumer,BlocSelector. - ✅
context.read(no rebuild) vswatch(rebuild). - ✅ Sealed state + exhaustive switch = robust UI.
- ✅ Async event với
on<E>((e, emit) async {...}). - ✅ MultiBlocProvider, RepositoryProvider.
- ✅ Repository pattern: tách HTTP/DB khỏi Bloc.
- ✅
bloc_test— pure Dart test, không cần Flutter. - ✅ Pitfall: state getter luôn current, không capture stale.
20. Kết nối
- Chương 8: so sánh Bloc vs Provider/Riverpod/GetX.
- Chương 10 — Network: Bloc gọi repository → HTTP.
- Chương 14 — Testing: test Bloc + widget test.