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 Equatable di semua class state dan event untuk mencegah rebuild yang tidak perlu — dua instance dengan nilai sama dianggap identik.
  • BlocBuilder me-rebuild UI berdasarkan state. BlocListener dipanggil sekali per state change untuk side effect (navigasi, snackbar). BlocConsumer menggabungkan keduanya.
  • BlocSelector seperti context.select() — widget hanya rebuild saat bagian yang di-select berubah.
  • BlocProvider inject Bloc ke widget tree. MultiBlocProvider untuk banyak Bloc sekaligus.
  • Gunakan sealed class (Dart 3) untuk state yang exhaustive dan type-safe. Gunakan single state class dengan copyWith untuk state yang kompleks dengan banyak field.
  • Pilih Cubit untuk fitur sederhana, Bloc untuk fitur kompleks yang butuh traceability dan event transformer.

← Sebelumnya: Riverpod   Berikutnya: MobX →

About | Author | Content Scope | Editorial Policy | Privacy Policy | Disclaimer | Contact