Bloc & Cubit #
Bloc (Business Logic Component) adalah pola arsitektur yang memisahkan logika bisnis dari UI secara ketat melalui stream event dan state. Flutter Bloc library menyediakan dua abstraksi: Cubit yang lebih sederhana (tanpa event), dan Bloc yang event-driven dan memberikan traceability penuh. Keduanya ideal untuk tim besar dan aplikasi enterprise.
Instalasi #
# pubspec.yaml
dependencies:
flutter_bloc: ^8.1.6
equatable: ^2.0.5 # untuk perbandingan state yang efisien
Cubit — Bloc yang Lebih Sederhana #
Cubit adalah Bloc yang disederhanakan — ia tidak menggunakan Event. Cubit mengekspos method yang memanggil emit() untuk mengeluarkan state baru. Perbedaannya dengan Bloc: Cubit memiliki fungsi yang memicu perubahan state, sementara Bloc memiliki event.
Buat State #
// State sederhana -- cukup dengan sealed class atau extends Equatable
abstract class KonterState extends Equatable {
const KonterState();
}
class KonterInitial extends KonterState {
@override
List<Object> get props => [];
}
class KonterUpdated extends KonterState {
final int nilai;
const KonterUpdated(this.nilai);
@override
List<Object> get props => [nilai]; // Equatable: state sama jika props sama
}
Buat Cubit #
class KonterCubit extends Cubit<KonterState> {
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());
}
Contoh Cubit yang Lebih Praktis — Auth #
// State dengan Equatable
@immutable
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 User user;
const AuthAuthenticated(this.user);
@override
List<Object> get props => [user];
}
class AuthUnauthenticated extends AuthState {}
class AuthError extends AuthState {
final String message;
const AuthError(this.message);
@override
List<Object> get props => [message];
}
// Cubit
class AuthCubit extends Cubit<AuthState> {
final AuthRepository _repository;
AuthCubit(this._repository) : super(AuthInitial());
Future<void> login(String email, String password) async {
emit(AuthLoading());
try {
final user = await _repository.login(email, password);
emit(AuthAuthenticated(user));
} catch (e) {
emit(AuthError(e.toString()));
}
}
Future<void> logout() async {
await _repository.logout();
emit(AuthUnauthenticated());
}
Future<void> cekSession() async {
final user = await _repository.getCurrentUser();
if (user != null) {
emit(AuthAuthenticated(user));
} else {
emit(AuthUnauthenticated());
}
}
}
Bloc — Event-Driven Architecture #
Bloc menambahkan lapisan Event di atas Cubit. Setiap perubahan state didahului oleh Event, yang membuat aliran data sepenuhnya traceable dan auditable.
Bloc Pattern:
UI ──[Event]──→ Bloc ──[emit(State)]──→ UI
Contoh:
Tombol Login ditekan
↓
LoginSubmitted(email, password) [Event]
↓
Bloc menerima event, panggil repository
↓
emit(AuthLoading) → emit(AuthAuthenticated) atau emit(AuthError)
↓
UI rebuild berdasarkan state baru
Definisikan Event #
// Event -- sealed class dengan subclass per aksi
abstract class AuthEvent extends Equatable {
const AuthEvent();
@override
List<Object> get props => [];
}
class LoginSubmitted extends AuthEvent {
final String email;
final String password;
const LoginSubmitted({required this.email, required this.password});
@override
List<Object> get props => [email, password];
}
class LogoutRequested extends AuthEvent {}
class SessionChecked extends AuthEvent {}
Buat Bloc #
class AuthBloc extends Bloc<AuthEvent, AuthState> {
final AuthRepository _repository;
AuthBloc(this._repository) : super(AuthInitial()) {
// Daftarkan handler untuk setiap event
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.password);
emit(AuthAuthenticated(user));
} 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.getCurrentUser();
if (user != null) {
emit(AuthAuthenticated(user));
} else {
emit(AuthUnauthenticated());
}
}
}
// Kirim event dari UI
context.read<AuthBloc>().add(LoginSubmitted(email: _email, password: _password));
context.read<AuthBloc>().add(LogoutRequested());
BlocProvider — Inject Bloc ke Widget Tree #
// Single Bloc
BlocProvider(
create: (context) => AuthCubit(context.read<AuthRepository>()),
child: const LoginScreen(),
)
// Buat dan langsung trigger event
BlocProvider(
create: (context) => AuthBloc(context.read<AuthRepository>())
..add(SessionChecked()), // langsung cek session saat dibuat
child: const App(),
)
// MultiBlocProvider untuk banyak Bloc
MultiBlocProvider(
providers: [
BlocProvider(create: (_) => AuthBloc(authRepo)),
BlocProvider(create: (_) => KeranjangCubit(keranjangRepo)),
BlocProvider(create: (_) => TemaBloc()),
],
child: const MyApp(),
)
BlocBuilder — Rebuild UI Berdasarkan State #
BlocBuilder<AuthBloc, AuthState>(
// buildWhen: opsional -- kontrol kapan builder dipanggil
buildWhen: (previous, current) => previous != current,
builder: (context, state) {
if (state is AuthLoading) {
return const CircularProgressIndicator();
}
if (state is AuthAuthenticated) {
return HomeScreen(user: state.user);
}
if (state is AuthError) {
return Text('Error: ${state.message}');
}
return const LoginForm();
},
)
// Dengan Cubit -- syntax identik
BlocBuilder<AuthCubit, AuthState>(
builder: (context, state) {
// sama persis dengan Bloc
},
)
BlocListener — Side Effect Berdasarkan State #
BlocListener dipanggil sekali per perubahan state — cocok untuk navigasi, snackbar, dan dialog (bukan untuk rebuild UI):
BlocListener<AuthBloc, AuthState>(
// listenWhen: opsional -- filter state yang ingin di-listen
listenWhen: (previous, current) => current is AuthError,
listener: (context, state) {
if (state is AuthAuthenticated) {
context.go('/home');
}
if (state is AuthError) {
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(content: Text(state.message)),
);
}
if (state is AuthUnauthenticated) {
context.go('/login');
}
},
child: const LoadingScreen(),
)
BlocConsumer — Gabungan Builder dan Listener #
BlocConsumer menggabungkan BlocBuilder dan BlocListener dalam satu widget — mengurangi nesting:
BlocConsumer<KeranjangCubit, KeranjangState>(
// listenWhen dan buildWhen keduanya opsional
listenWhen: (previous, current) => current is KeranjangItemDitambahkan,
listener: (context, state) {
// Side effect -- muncul sekali per state change
if (state is KeranjangItemDitambahkan) {
ScaffoldMessenger.of(context).showSnackBar(
const SnackBar(content: Text('Item ditambahkan ke keranjang!')),
);
}
},
buildWhen: (previous, current) => current is! KeranjangItemDitambahkan,
builder: (context, state) {
// Rebuild UI
return KeranjangView(state: state);
},
)
BlocSelector — Subscribe Hanya ke Sebagian State #
BlocSelector setara dengan context.select() di Provider — widget hanya rebuild saat bagian yang di-select berubah:
// Hanya rebuild saat jumlah item berubah
BlocSelector<KeranjangBloc, KeranjangState, int>(
selector: (state) => state.jumlahItem,
builder: (context, jumlah) {
return Badge(label: Text('$jumlah'));
},
)
Equatable — Perbandingan State yang Efisien #
Tanpa Equatable, Bloc akan selalu menganggap state berbeda (karena instance berbeda), menyebabkan rebuild yang tidak perlu:
// TANPA Equatable -- selalu rebuild meskipun nilai sama!
class KonterState {
final int nilai;
const KonterState(this.nilai);
// tidak ada == override -- dua KonterState(5) dianggap berbeda
}
// DENGAN Equatable -- hanya rebuild jika props berbeda
class KonterState extends Equatable {
final int nilai;
const KonterState(this.nilai);
@override
List<Object> get props => [nilai]; // dua KonterState(5) dianggap SAMA
}
Pola State yang Umum #
Pattern 1: Sealed Class per Feature #
// State yang tipe-safe dengan sealed class (Dart 3)
sealed class ProdukState extends Equatable {
const ProdukState();
@override
List<Object?> get props => [];
}
class ProdukInitial extends ProdukState {}
class ProdukLoading extends ProdukState {}
class ProdukLoaded extends ProdukState {
final List<Produk> produk;
const ProdukLoaded(this.produk);
@override
List<Object> get props => [produk];
}
class ProdukError extends ProdukState {
final String pesan;
const ProdukError(this.pesan);
@override
List<Object> get props => [pesan];
}
// Di widget -- exhaustive matching
BlocBuilder<ProdukBloc, ProdukState>(
builder: (context, state) => switch (state) {
ProdukInitial() => const SizedBox.shrink(),
ProdukLoading() => const CircularProgressIndicator(),
ProdukLoaded(:final produk) => ProdukGrid(produk: produk),
ProdukError(:final pesan) => ErrorView(pesan: pesan),
},
)
Pattern 2: Single State Class #
// Satu state class dengan copyWith -- cocok untuk state yang kompleks
class ProdukState extends Equatable {
final List<Produk> produk;
final bool isLoading;
final String? error;
final String query;
const ProdukState({
this.produk = const [],
this.isLoading = false,
this.error,
this.query = '',
});
ProdukState copyWith({
List<Produk>? produk,
bool? isLoading,
String? error,
String? query,
}) => ProdukState(
produk: produk ?? this.produk,
isLoading: isLoading ?? this.isLoading,
error: error,
query: query ?? this.query,
);
@override
List<Object?> get props => [produk, isLoading, error, query];
}
Kapan Cubit vs Bloc? #
Gunakan Cubit jika:
✓ State machine sederhana dengan sedikit transisi
✓ Tidak butuh audit trail / event history
✓ Tim lebih suka API yang langsung dan minimal boilerplate
✓ Fitur kecil dan terisolasi
Gunakan Bloc jika:
✓ Butuh traceability -- setiap perubahan state punya event yang jelas
✓ State machine kompleks dengan banyak transisi
✓ Tim besar yang butuh konvensi ketat
✓ Butuh logging event untuk debugging
✓ Butuh event transformer (throttle, debounce, concurrency control)
Ringkasan #
- Cubit adalah penyederhanaan Bloc — ia mengekspos method yang langsung memanggil
emit()tanpa Event sebagai perantara.- Bloc menambahkan lapisan Event — setiap perubahan state dipicu oleh Event yang terdefinisi, memberikan traceability dan audit trail yang lengkap.
- Gunakan
Equatabledi semua class state dan event untuk mencegah rebuild yang tidak perlu — dua instance dengan nilai sama dianggap identik.BlocBuilderme-rebuild UI berdasarkan state.BlocListenerdipanggil sekali per state change untuk side effect (navigasi, snackbar).BlocConsumermenggabungkan keduanya.BlocSelectorseperticontext.select()— widget hanya rebuild saat bagian yang di-select berubah.BlocProviderinject Bloc ke widget tree.MultiBlocProvideruntuk banyak Bloc sekaligus.- Gunakan sealed class (Dart 3) untuk state yang exhaustive dan type-safe. Gunakan single state class dengan
copyWithuntuk state yang kompleks dengan banyak field.- Pilih Cubit untuk fitur sederhana, Bloc untuk fitur kompleks yang butuh traceability dan event transformer.