Bloc & Cubit #
Pustaka BLoC (Business Logic Component) adalah pola arsitektur manajemen status (state management) yang memisahkan logika bisnis dari lapisan tampilan antarmuka (UI) secara ketat menggunakan kekuatan streams. Diperkenalkan pertama kali oleh tim pengembang Google pada ajang Google I/O 2018, BLoC memaksa aplikasi untuk memiliki aliran data satu arah yang sangat terprediksi. Dalam ekosistem pustaka flutter_bloc, kita diberikan dua jenis abstraksi yang dapat disesuaikan dengan kompleksitas fitur: Cubit yang berbasis pemanggilan metode langsung (fungsi-ke-state), dan Bloc yang berbasis kejadian (event-driven) untuk ketertelusuran penuh (traceability). Kombinasi ini menjadikannya pilihan utama bagi proyek berskala korporasi (enterprise) dan kolaborasi tim besar.
Instalasi #
Untuk menggunakan BLoC di proyek kita, tambahkan pustaka berikut ke dalam berkas pubspec.yaml:
dependencies:
flutter:
sdk: flutter
# Paket integrasi BLoC untuk Flutter
flutter_bloc: ^8.1.6
# Digunakan untuk menyederhanakan perbandingan objek state
equatable: ^2.0.5
Cubit — BLoC yang Lebih Sederhana #
Cubit adalah versi penyederhanaan dari Bloc yang menghilangkan kebutuhan pendefinisian kelas Event. Alih-alih mengirim kejadian, kita memanggil metode fungsi biasa pada Cubit dari lapisan UI, lalu memancarkan status (state) baru ke pelanggan menggunakan metode internal emit().
flowchart TD
subgraph Alur_Cubit["Cubit (Alur Langsung)"]
UI_C["UI (Tombol Ditekan)"] -->|Panggil Fungsi / Method| Cubit["Cubit (Logika Bisnis)"]
Cubit -->|Pancarkan: emit(State)| UI_C_Rebuild["UI Rebuild / Render Ulang"]
end
subgraph Alur_Bloc["BLoC (Alur Berbasis Event)"]
UI_B["UI (Tombol Ditekan)"] -->|Kirim: add(Event)| Bloc["BLoC (Event Handler & Stream)"]
Bloc -->|Map Event ke State: emit(State)| UI_B_Rebuild["UI Rebuild / Render Ulang"]
end1. Mendefinisikan Objek State #
Kita disarankan menggunakan paket Equatable pada kelas status agar Flutter tidak me-rebuild UI jika nilai properti status baru sama dengan status lama.
import 'package:equatable/equatable.dart';
abstract class KonterState extends Equatable {
const KonterState();
@override
List<Object?> get props => [];
}
class KonterInitial extends KonterState {}
class KonterUpdated extends KonterState {
final int nilai;
const KonterUpdated(this.nilai);
@override
List<Object?> get props => [nilai]; // Perbandingan nilai terstruktur
}
2. Membuat Kelas Cubit #
Di dalam Cubit, kita menentukan fungsi mutator status secara langsung.
import 'package:flutter_bloc/flutter_bloc.dart';
class KonterCubit extends Cubit<KonterState> {
// Menetapkan status awal (initial state) ke KonterInitial
KonterCubit() : super(KonterInitial());
void tambah() {
final nilaiSekarang = state is KonterUpdated
? (state as KonterUpdated).nilai
: 0;
emit(KonterUpdated(nilaiSekarang + 1));
}
void kurang() {
final nilaiSekarang = state is KonterUpdated
? (state as KonterUpdated).nilai
: 0;
emit(KonterUpdated(nilaiSekarang - 1));
}
void reset() => emit(KonterInitial());
}
BLoC — Event-Driven Architecture #
Jika fitur aplikasi kita memiliki kompleksitas tinggi yang membutuhkan kontrol konkurensi (seperti menunda request berulang/debounce), pelacakan riwayat aktivitas pengguna (audit trail), atau integrasi analitik otomatis, menggunakan arsitektur BLoC penuh adalah pilihan terbaik.
Dalam BLoC, antarmuka UI sama sekali tidak diizinkan memanggil fungsi internal BLoC. UI harus memancarkan objek kejadian (Event) ke dalam sistem BLoC menggunakan metode add(Event).
1. Definisikan Event #
Event bertindak sebagai deskripsi formal mengenai interaksi apa yang sedang dilakukan oleh pengguna aplikasi kita.
abstract class AuthEvent extends Equatable {
const AuthEvent();
@override
List<Object?> get props => [];
}
class LoginSubmitted extends AuthEvent {
final String email;
final String sandi;
const LoginSubmitted({required this.email, required this.sandi});
@override
List<Object?> get props => [email, sandi];
}
class LogoutRequested extends AuthEvent {}
class SessionChecked extends AuthEvent {}
2. Definisikan State #
Status merepresentasikan respons yang akan dirender oleh antarmuka.
abstract class AuthState extends Equatable {
const AuthState();
@override
List<Object?> get props => [];
}
class AuthInitial extends AuthState {}
class AuthLoading extends AuthState {}
class AuthAuthenticated extends AuthState {
final String namaPengguna;
const AuthAuthenticated(this.namaPengguna);
@override
List<Object?> get props => [namaPengguna];
}
class AuthUnauthenticated extends AuthState {}
class AuthError extends AuthState {
final String pesanError;
const AuthError(this.pesanError);
@override
List<Object?> get props => [pesanError];
}
3. Implementasi Kelas BLoC #
Di dalam BLoC, kita mendaftarkan fungsi penangan (event handler) untuk memetakan setiap kejadian (Event) menjadi pancaran status (State) baru melalui metode on<Event>.
class AuthBloc extends Bloc<AuthEvent, AuthState> {
final AuthRepository _repository;
AuthBloc(this._repository) : super(AuthInitial()) {
// Mendaftarkan event handlers
on<LoginSubmitted>(_onLoginSubmitted);
on<LogoutRequested>(_onLogoutRequested);
on<SessionChecked>(_onSessionChecked);
}
Future<void> _onLoginSubmitted(
LoginSubmitted event,
Emitter<AuthState> emit,
) async {
emit(AuthLoading());
try {
final user = await _repository.login(event.email, event.sandi);
emit(AuthAuthenticated(user.nama));
} catch (e) {
emit(AuthError(e.toString()));
}
}
Future<void> _onLogoutRequested(
LogoutRequested event,
Emitter<AuthState> emit,
) async {
await _repository.logout();
emit(AuthUnauthenticated());
}
Future<void> _onSessionChecked(
SessionChecked event,
Emitter<AuthState> emit,
) async {
final user = await _repository.ambilUserTerkini();
if (user != null) {
emit(AuthAuthenticated(user.nama));
} else {
emit(AuthUnauthenticated());
}
}
}
BlocProvider — Injeksi dependensi ke Widget Tree #
Untuk menyediakan instance BLoC atau Cubit ke subtree widget kita, kita menggunakan BlocProvider. Kelas ini secara otomatis menangani daur hidup objek dan memanggil dispose() saat tidak digunakan lagi.
// Mendaftarkan satu BLoC dan langsung memicu inisialisasi pengecekan sesi login
BlocProvider(
create: (BuildContext context) => AuthBloc(context.read<AuthRepository>())
..add(SessionChecked()),
child: const LayarUtama(),
)
// Mendaftarkan banyak BLoC sekaligus menggunakan MultiBlocProvider
MultiBlocProvider(
providers: [
BlocProvider(create: (context) => AuthBloc(context.read<AuthRepository>())),
BlocProvider(create: (context) => KonterCubit()),
],
child: const LayarUtama(),
)
Widget Konsumen BLoC di Lapisan UI #
Pustaka flutter_bloc menyediakan sekumpulan widget khusus untuk berinteraksi dengan status data di dalam build method:
1. BlocBuilder (Render Ulang UI secara Reaktif) #
BlocBuilder bertugas mengevaluasi status saat ini dan merekonstruksi UI berdasarkan nilai tersebut. Kita dapat mengoptimalkan kinerjanya menggunakan callback buildWhen.
BlocBuilder<AuthBloc, AuthState>(
buildWhen: (previous, current) {
// Hanya lakukan rebuild jika transisi status memang berubah tipe data
return previous != current;
},
builder: (BuildContext context, AuthState state) {
if (state is AuthLoading) {
return const Center(child: CircularProgressIndicator());
}
if (state is AuthAuthenticated) {
return Text('Selamat datang, ${state.namaPengguna}!');
}
return const FormLoginWidget();
},
)
2. BlocListener (Menangani Efek Samping Sekali Jalan) #
BlocListener tidak bertugas me-rebuild UI secara fisik. Ia dipanggil tepat satu kali untuk setiap perubahan status.
- Kapan Digunakan: Untuk memicu dialog modal, meluncurkan snackbar peringatan, atau melakukan perpindahan navigasi rute halaman.
BlocListener<AuthBloc, AuthState>(
listener: (BuildContext context, AuthState state) {
if (state is AuthError) {
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(content: Text(state.pesanError)),
);
}
if (state is AuthAuthenticated) {
// Pindah ke halaman beranda
Navigator.of(context).pushReplacement(
MaterialPageRoute(builder: (_) => const LayarBeranda()),
);
}
},
child: const LayarLoginForm(),
)
3. BlocConsumer (Gabungan Builder & Listener) #
Ketika sebuah widget perlu me-rebuild dirinya sendiri sekaligus memicu efek samping navigasi secara bersamaan, kita menggunakan BlocConsumer untuk mencegah penumpukan widget (nesting hell).
flowchart TD
BlocEmit["BLoC Memancarkan State Baru"] --> BlocConsumer["BlocConsumer Widget"]
BlocConsumer --> CheckListener{"Apakah state memicu\nlistenWhen?"}
BlocConsumer --> CheckBuilder{"Apakah state memicu\nbuildWhen?"}
CheckListener -->|Ya| ListenerCallback["Jalankan listener() Callback\n(Side Effects: Navigasi, SnackBar)"]
CheckListener -->|Tidak| IgnoreListener["Abaikan Listener"]
CheckBuilder -->|Ya| BuilderCallback["Jalankan builder() Callback\n(Render Ulang Tampilan UI)"]
CheckBuilder -->|Tidak| IgnoreBuilder["Abaikan Rebuild (Gunakan Cache Frame)"]Berikut adalah contoh penggunaan BlocConsumer:
BlocConsumer<AuthBloc, AuthState>(
listener: (context, state) {
if (state is AuthError) {
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(content: Text(state.pesanError)),
);
}
},
builder: (context, state) {
if (state is AuthLoading) {
return const CircularProgressIndicator();
}
return TombolLogin(
onPressed: () {
context.read<AuthBloc>().add(
const LoginSubmitted(email: '[email protected]', sandi: 'password123'),
);
},
);
},
)
4. BlocSelector (Optimasi Berlangganan Parsial) #
BlocSelector membatasi rebuild hanya jika properti spesifik dari kelas status berubah nilai, mirip seperti context.select pada Provider.
BlocSelector<AuthBloc, AuthState, bool>(
selector: (state) => state is AuthLoading,
builder: (context, isLoading) {
// Hanya di-rebuild ketika status isLoading berubah (true/false)
return isLoading
? const CircularProgressIndicator()
: const Text('Kirim Data');
},
)
Pola Desain State di BLoC #
Dalam merancang struktur status data pada arsitektur BLoC, kita mengenal dua pola penulisan utama:
Pola 1: Pemisahan Kelas Terbuka (Sealed Class Pattern) #
Sejak hadirnya Dart 3, kita disarankan menggunakan modifier sealed class untuk menulis kelas-kelas status yang terpisah. Hal ini memudahkan kita melakukan pencocokan pola (exhaustive pattern matching) yang aman saat kompilasi di dalam UI.
sealed class DataState extends Equatable {
const DataState();
@override
List<Object?> get props => [];
}
class DataInitial extends DataState {}
class DataLoading extends DataState {}
class DataSuccess extends DataState {
final List<String> items;
const DataSuccess(this.items);
@override
List<Object?> get props => [items];
}
// Di UI, kita dapat menggunakan switch expression secara aman:
Widget build(BuildContext context) {
final state = context.watch<DataBloc>().state;
return Center(
child: switch (state) {
DataInitial() => const Text('Mulai memuat data...'),
DataLoading() => const CircularProgressIndicator(),
DataSuccess(:final items) => Text('Item: ${items.length}'),
},
);
}
Pola 2: Kelas Status Tunggal (Single State Class with copyWith) #
Pola ini menggunakan satu kelas status tunggal dengan banyak properti opsional dan metode copyWith. Pola ini sangat populer untuk menangani formulir pengisian data yang kompleks.
class FormPendaftaranState extends Equatable {
final String nama;
final String email;
final bool isSubmitting;
final String? error;
const FormPendaftaranState({
this.nama = '',
this.email = '',
this.isSubmitting = false,
this.error,
});
FormPendaftaranState copyWith({
String? nama,
String? email,
bool? isSubmitting,
String? error,
}) {
return FormPendaftaranState(
nama: nama ?? this.nama,
email: email ?? this.email,
isSubmitting: isSubmitting ?? this.isSubmitting,
error: error,
);
}
@override
List<Object?> get props => [nama, email, isSubmitting, error];
}
Kapan Memilih Cubit vs BLoC? #
Sebagai panduan arsitektur tim, berikut adalah indikator dalam menentukan kapan kita harus menggunakan Cubit dan kapan harus melangkah menggunakan BLoC:
- Gunakan Cubit jika:
- Alur status sangat sederhana (misal: toggle tema gelap, collapse panel, atau local counter).
- Tim pengembang ingin mengurangi kode boilerplate dan membutuhkan produktivitas tinggi.
- Kita tidak memerlukan kontrol konkurensi (seperti pembatasan request/debounce API).
- Gunakan BLoC jika:
- Alur bisnis logik sangat kompleks dan bertipe mesin status (state machine).
- Kita memerlukan fitur pelacakan kejadian pengguna secara terpusat (audit trail).
- Kita perlu menggunakan fitur transformasi kejadian (event transformer) seperti pembatalan otomatis (switchMap) atau pembatasan ketukan (throttle/debounce) pada pengiriman data.
Ringkasan #
- Cubit mengelola status dengan menerima instruksi fungsi langsung dari UI dan memancarkan status baru melalui metode
emit().- BLoC memaksakan penggunaan kelas Event sebagai jembatan kejadian antara UI dan logika bisnis untuk ketertelusuran yang lengkap.
- Equatable: Komponen wajib pada kelas status untuk memastikan perbandingan objek terstruktur berdasarkan nilai properti, menghindari jank visual akibat rebuild tak perlu.
- BlocConsumer: Widget gabungan efisien untuk memperbarui UI sekaligus memicu efek samping dialog/navigasi secara bersamaan.
- Sealed Class: Terapkan fitur ini (sejak Dart 3) untuk memaksakan validasi penanganan seluruh tipe status pada waktu kompilasi.