Flutter #29: BLoC — Cubit & BlocProvider
Halo! Di artikel #28 kita sudah bikin custom widget yang reusable. Sekarang waktunya upgrade state management kita — dari Provider ke BLoC (Business Logic Component).
Kenapa BLoC? Karena Provider cukup untuk app kecil, tapi begitu app-nya tumbuh (banyak screen, banyak state, banyak API call), kamu butuh sesuatu yang lebih terstruktur, predictable, dan testable. BLoC hadir untuk itu.
Hari ini kita mulai dari Cubit — versi ringkas dari BLoC yang lebih simpel tapi sudah sangat powerful. Next article (#30) kita lanjut ke BLoC penuh dengan Event & State. Let’s go!
1. Kenapa BLoC?
Sebelum kita code, pahami dulu kenapa BLoC itu penting:
| Masalah di Provider | Solusi BLoC |
|---|---|
| Logic campur di UI | Logic terpisah di Cubit/BLoC |
| Hard to test | Cubit testable tanpa Flutter |
| Race condition async | State berubah predictably |
| State tumpuk susah debug | State history bisa di-track |
BLoC memisahkan UI dari Business Logic. UI cuma render state, Cubit/BLoC cuma keluarkan state baru. Clean!
2. Instalasi
Tambahkan ini ke pubspec.yaml:
dependencies:
flutter_bloc: ^8.1.0
equatable: ^2.0.5
Lalu jalankan:
flutter pub get
flutter_bloc— package utama untuk BLoC + Cubitequatable— biar perbandingan state gampang (tanpa harus override==manual)
3. Apa Itu Cubit?
Cubit adalah class yang meng-extend Cubit<State>. Tugasnya cuma satu: emit state baru.
Bedanya sama BLoC:
- Cubit → fungsi biasa yang emit state langsung
- BLoC → pakai Event → proses → emit state (lebih terstruktur)
Kalau baru mulai, mulai dari Cubit dulu. Nanti kalau logic-nya makin kompleks, tinggal migrasi ke BLoC.
4. Contoh Pertama: Counter Cubit
Bikin file counter_cubit.dart:
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:equatable/equatable.dart';
// === State ===
// Equatable bikin perbandingan state jadi mudah
class CounterState extends Equatable {
final int count;
final String message;
const CounterState({required this.count, this.message = ''});
// Method untuk copy state baru (immutable pattern)
CounterState copyWith({int? count, String? message}) {
return CounterState(
count: count ?? this.count,
message: message ?? this.message,
);
}
@override
List<Object?> get props => [count, message];
}
// === Cubit ===
class CounterCubit extends Cubit<CounterState> {
// Mulai dengan state awal
CounterCubit() : super(const CounterState(count: 0));
void increment() {
final newCount = state.count + 1;
emit(state.copyWith(
count: newCount,
message: 'Count sekarang: $newCount',
));
}
void decrement() {
final newCount = state.count > 0 ? state.count - 1 : 0;
emit(state.copyWith(
count: newCount,
message: newCount == 0 ? 'Minimal 0 ya!' : 'Count: $newCount',
));
}
void reset() {
emit(const CounterState(count: 0, message: 'Di-reset!'));
}
}
Penjelasan:
CounterState— merepresentasikan semua data yang dibutuhkan UI. PakaiEquatablebiar kalau state-nya sama, Flutter gak perlu rebuild.CounterCubit— punya fungsiincrement(),decrement(),reset()yang masing-masing emit state baru.emit()— fungsi dari Cubit untuk “kirim” state baru ke UI yang consume.
5. BlocProvider — Menanam Cubit ke Widget Tree
BlocProvider fungsinya sama seperti Provider di package Provider — yaitu menanam Cubit/BLoC ke widget tree supaya bisa diakses dari mana aja.
import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import 'counter_cubit.dart'; // import Cubit kita
void main() {
runApp(const MyApp());
}
class MyApp extends StatelessWidget {
const MyApp({super.key});
@override
Widget build(BuildContext context) {
// BlocProvider menanam CounterCubit ke tree
return BlocProvider(
create: (_) => CounterCubit(),
child: const MaterialApp(
home: CounterPage(),
),
);
}
}
Key point: BlocProvider itu parent-nya MaterialApp, jadi Cubit bisa diakses dari semua screen di dalam app.
6. BlocConsumer — Listen & Build
BlocConsumer adalah widget yang paling sering dipakai. Dia punya dua callback:
listener— buat side-effect (show SnackBar, navigate, dll)builder— buat rebuild UI berdasarkan state
class CounterPage extends StatelessWidget {
const CounterPage({super.key});
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(title: const Text('Counter Cubit')),
body: Center(
child: BlocConsumer<CounterCubit, CounterState>(
// Listener: side-effect (sekali aksi)
listenWhen: (previous, current) {
// Cuma listen kalau message berubah
return previous.message != current.message;
},
listener: (context, state) {
// Munculkan SnackBar kalau message ada
if (state.message.isNotEmpty) {
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(
content: Text(state.message),
duration: const Duration(seconds: 1),
),
);
}
},
// Builder: rebuild UI
buildWhen: (previous, current) {
// Cuma rebuild kalau count berubah
return previous.count != current.count;
},
builder: (context, state) {
return Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Text(
'${state.count}',
style: const TextStyle(
fontSize: 64,
fontWeight: FontWeight.bold,
),
),
const SizedBox(height: 20),
Row(
mainAxisAlignment: MainAxisAlignment.center,
children: [
ElevatedButton.icon(
onPressed: () {
context.read<CounterCubit>().decrement();
},
icon: const Icon(Icons.remove),
label: const Text('-'),
),
const SizedBox(width: 16),
ElevatedButton.icon(
onPressed: () {
context.read<CounterCubit>().increment();
},
icon: const Icon(Icons.add),
label: const Text('+'),
),
const SizedBox(width: 16),
OutlinedButton(
onPressed: () {
context.read<CounterCubit>().reset();
},
child: const Text('Reset'),
),
],
),
],
);
},
),
),
);
}
}
Penjelasan penting:
context.read<CounterCubit>()— ambil Cubit yang sudah ditanam olehBlocProviderlistenWhen&buildWhen— filter supaya rebuild gak terus-terusan (performa!)listenerdipanggil setiap state berubah, tapi bukan untuk rebuild UIbuilderyang rebuild UI berdasarkan state terbaru
7. BlocBuilder vs BlocListener vs BlocConsumer
Pusing? Ini cheat sheet-nya:
| Widget | Fungsi | Kapan Pakai |
|---|---|---|
| BlocBuilder | Rebuild UI dari state | Kebanyakan kasus UI update |
| BlocListener | Eksekusi side-effect | Show SnackBar, Navigate |
| BlocConsumer | Builder + Listener | Butuh keduanya sekaligus |
8. Contoh Nyata: Todo Cubit
Counter kan terlalu simpel. Yuk bikin sesuatu yang lebih real — Todo Cubit!
class Todo {
final String id;
final String title;
final bool isDone;
const Todo({
required this.id,
required this.title,
this.isDone = false,
});
Todo copyWith({String? title, bool? isDone}) {
return Todo(
id: id,
title: title ?? this.title,
isDone: isDone ?? this.isDone,
);
}
}
class TodoState extends Equatable {
final List<Todo> todos;
final String filter; // 'all', 'done', 'undone'
const TodoState({
this.todos = const [],
this.filter = 'all',
});
List<Todo> get filteredTodos {
switch (filter) {
case 'done':
return todos.where((t) => t.isDone).toList();
case 'undone':
return todos.where((t) => !t.isDone).toList();
default:
return todos;
}
}
@override
List<Object?> get props => [todos, filter];
}
class TodoCubit extends Cubit<TodoState> {
TodoCubit() : super(const TodoState());
void addTodo(String title) {
final newTodo = Todo(
id: DateTime.now().millisecondsSinceEpoch.toString(),
title: title,
);
emit(state.copyWith(
todos: [...state.todos, newTodo],
));
}
void toggleTodo(String id) {
final updated = state.todos.map((todo) {
if (todo.id == id) {
return todo.copyWith(isDone: !todo.isDone);
}
return todo;
}).toList();
emit(state.copyWith(todos: updated));
}
void deleteTodo(String id) {
final updated = state.todos.where((t) => t.id != id).toList();
emit(state.copyWith(todos: updated));
}
void setFilter(String filter) {
emit(state.copyWith(filter: filter));
}
}
Ini pattern yang umum di production app:
- State punya computed property (
filteredTodos) — logic filtering di state, bukan di UI - Cubit punya action methods —
addTodo(),toggleTodo(),deleteTodo() - Immutable updates — setiap perubahan bikin list baru, gak mutasi langsung
9. BlocSelector — Selective Rebuild
Mau rebuild cuma bagian tertentu dari UI? Pakai BlocSelector:
// Cuma rebuild Text angka, bukan tombol
BlocSelector<CounterCubit, CounterState, int>(
selector: (state) => state.count,
builder: (context, count) {
return Text(
'$count',
style: const TextStyle(fontSize: 48),
);
},
),
BlocSelector cuma rebuild kalau value yang dipilih berubah. Super hemat performa!
10. Pattern yang Disarankan
Kalau project sudah mulai besar, ikuti pattern ini:
lib/
├── cubit/
│ ├── counter_cubit.dart
│ └── todo_cubit.dart
├── state/
│ ├── counter_state.dart
│ └── todo_state.dart
├── page/
│ ├── counter_page.dart
│ └── todo_page.dart
└── main.dart
Pisahkan state, cubit, dan page — jangan campur jadi satu file. Ini bikin codebase rapi dan mudah di-maintain.
Kesimpulan
Hari ini kita belajar:
- Cubit — state management simpel dari BLoC ecosystem
emit()— cara update stateBlocProvider— tanam Cubit ke widget treeBlocConsumer— listener + builder dalam satu widgetBlocSelector— selective rebuild untuk performa- Pattern — pisahkan state, cubit, dan UI
Di artikel #30 kita lanjut ke BLoC penuh dengan Event dan State class yang terpisah — lebih terstruktur untuk app yang kompleks.
Coba sendiri! Bikin Cubit untuk theme switcher (dark/light mode), lalu pakai BlocProvider di level MaterialApp. Share hasilnya ke sosial media dan tag @ahsai001 🚀