Pengenalan BLoC Pattern
Pahami arsitektur BLoC: pisahkan event, state, dan logic bisnis dengan Cubit yang simpel lalu Bloc penuh.
Filosofi BLoC
BLoC (Business Logic Component) memisahkan tiga hal dengan tegas: Event (apa yang terjadi), State (kondisi saat ini), dan Bloc (aturan yang mengubah event menjadi state baru). UI hanya mengirim event dan me-render state. Logic bisnis tidak bocor ke widget.
Mulai dari Cubit: BLoC yang simpel
Cubit adalah versi sederhana tanpa event class. Cocok untuk dipelajari dulu sebelum Bloc penuh.
dependencies:
flutter_bloc: ^8.1.0import 'package:flutter_bloc/flutter_bloc.dart';
// 1. Cubit: menyimpan state int
class CounterCubit extends Cubit<int> {
CounterCubit() : super(0);
void tambah() => emit(state + 1);
void kurang() => emit(state - 1);
void reset() => emit(0);
}
// 2. Sediakan ke widget tree
class MyApp extends StatelessWidget {
const MyApp({super.key});
@override
Widget build(BuildContext context) {
return BlocProvider(
create: (_) => CounterCubit(),
child: const MaterialApp(home: HalamanCounter()),
);
}
}
// 3. Konsumsi di UI
class HalamanCounter extends StatelessWidget {
const HalamanCounter({super.key});
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(title: const Text('Cubit Counter')),
body: Center(
child: BlocBuilder<CounterCubit, int>(
builder: (context, angka) => Text(
'$angka',
style: const TextStyle(fontSize: 48),
),
),
),
floatingActionButton: Column(
mainAxisSize: MainAxisSize.min,
children: [
FloatingActionButton(
heroTag: 'tambah',
onPressed: () => context.read<CounterCubit>().tambah(),
child: const Icon(Icons.add),
),
const SizedBox(height: 8),
FloatingActionButton(
heroTag: 'kurang',
onPressed: () => context.read<CounterCubit>().kurang(),
child: const Icon(Icons.remove),
),
],
),
);
}
}BlocBuilder rebuild saat state berubah, context.read memanggil method tanpa berlangganan, mirip ref.read di Riverpod.
Naik ke Bloc: dengan Event
Untuk logic kompleks, Bloc memakai event class agar setiap aksi terdokumentasi:
// Event
sealed class KasirEvent {}
class TambahItem extends KasirEvent {
final String nama;
TambahItem(this.nama);
}
class HapusItem extends KasirEvent {
final String nama;
HapusItem(this.nama);
}
// Bloc
class KasirBloc extends Bloc<KasirEvent, List<String>> {
KasirBloc() : super([]) {
on<TambahItem>((event, emit) => emit([...state, event.nama]));
on<HapusItem>((event, emit) {
emit(state.where((p) => p != event.nama).toList());
});
}
}
// Mengirim event dari UI
context.read<KasirBloc>().add(TambahItem('Kopi Tubruk'));Event class membuat alur data eksplisit dan mudah di-test: beri event X, harapkan state Y.
BlocListener untuk efek samping
Navigasi, snackbar, atau dialog tidak boleh di BlocBuilder (yang bisa rebuild berkali-kali). Pakai BlocListener:
BlocListener<AuthBloc, AuthState>(
listener: (context, state) {
if (state is AuthSukses) {
Navigator.pushReplacementNamed(context, '/');
} else if (state is AuthGagal) {
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(content: Text(state.pesan)),
);
}
},
child: const FormLogin(),
)Catatan teknis: Jangan membuat Bloc/Cubit di dalam method
buildkarena build bisa dipanggil berkali-kali dan kamu akan membuat instance baru sambil membuang yang lama (beserta state-nya). Selalu buat diBlocProvider(create: ...)di atas widget yang memakainya. Dan tutup stream manual kalau Bloc memakai subscription sendiri, lewat overrideclose().