Perbandingan & Kapan Memilih #

Setelah mempelajari lima pendekatan state management — setState/ValueNotifier, Provider, Riverpod, Bloc, dan MobX — pertanyaan yang wajar adalah: mana yang harus saya pilih? Jawabannya selalu bergantung pada konteks. Artikel ini memberikan kerangka yang jelas untuk membuat keputusan yang tepat.

Perbandingan Fitur #

FitursetStateProviderRiverpodBloc/CubitMobX
Kurva belajarRendahRendahSedangTinggiSedang
BoilerplateMinimalRendahRendah-SedangTinggiRendah (codegen)
Compile-time safetyRendahTinggiSedangSedang
TestabilitySulitSedangTinggiTinggiSedang
Performa rebuildManualManualOtomatisManualOtomatis
Async stateManualFutureProviderAsyncValueEmit state@action async
Code generationOpsionalWajib
Bergantung Context
Multiple instanceSulitMudah (family)Sulit
DevToolsBasicRiverpod DevBloc ObserverMobX DevTools
Dukungan FlutterOfficialRecommendedRecommendedCommunityCommunity
Cocok untukMicroSmall-MedMed-LargeLarge/EntMed

Perbandingan Kode — Fitur yang Sama #

Untuk melihat perbedaan nyata, berikut implementasi fitur yang sama (load dan tampilkan daftar produk) dengan masing-masing pendekatan:

setState #

class _ProdukState extends State<ProdukScreen> {
  List<Produk> _produk = [];
  bool _isLoading = false;
  String? _error;

  @override
  void initState() {
    super.initState();
    _load();
  }

  Future<void> _load() async {
    setState(() => _isLoading = true);
    try {
      _produk = await produkApi.getAll();
      if (mounted) setState(() => _isLoading = false);
    } catch (e) {
      if (mounted) setState(() { _isLoading = false; _error = e.toString(); });
    }
  }

  @override
  Widget build(BuildContext context) {
    if (_isLoading) return const CircularProgressIndicator();
    if (_error != null) return Text(_error!);
    return ProdukList(produk: _produk);
  }
}
// Baris kode: ~30 | Testability: Rendah | State scope: Lokal

Provider #

// model
class ProdukModel extends ChangeNotifier {
  List<Produk> produk = [];
  bool isLoading = false;
  String? error;

  Future<void> load() async {
    isLoading = true; notifyListeners();
    try {
      produk = await produkApi.getAll();
    } catch (e) { error = e.toString(); }
    isLoading = false; notifyListeners();
  }
}

// widget
Consumer<ProdukModel>(
  builder: (context, model, _) {
    if (model.isLoading) return const CircularProgressIndicator();
    if (model.error != null) return Text(model.error!);
    return ProdukList(produk: model.produk);
  },
)
// Baris kode: ~25 | Testability: Sedang | State scope: Global/Shared

Riverpod #

// provider
class ProdukNotifier extends AsyncNotifier<List<Produk>> {
  @override
  Future<List<Produk>> build() => ref.watch(produkRepoProvider).getAll();
}
final produkProvider = AsyncNotifierProvider<ProdukNotifier, List<Produk>>(ProdukNotifier.new);

// widget
ref.watch(produkProvider).when(
  loading: () => const CircularProgressIndicator(),
  error: (e, _) => Text('$e'),
  data: (produk) => ProdukList(produk: produk),
)
// Baris kode: ~12 | Testability: Tinggi | State scope: Global/Scoped

Bloc #

// event + state + bloc (3 file terpisah)
// Event: LoadProduk
// State: ProdukLoading, ProdukLoaded, ProdukError
// Bloc: on<LoadProduk> → emit(Loading) → load → emit(Loaded/Error)

BlocBuilder<ProdukBloc, ProdukState>(
  builder: (context, state) => switch(state) {
    ProdukLoading() => const CircularProgressIndicator(),
    ProdukError(:final pesan) => Text(pesan),
    ProdukLoaded(:final produk) => ProdukList(produk: produk),
    _ => const SizedBox(),
  },
)
// Baris kode: ~60 (3 file) | Testability: Tinggi | State scope: Global/Scoped

MobX #

// store
abstract class _ProdukStore with Store {
  @observable ObservableList<Produk> produk = ObservableList();
  @observable bool isLoading = false;
  @observable String? error;

  @action Future<void> load() async {
    isLoading = true;
    try { produk = ObservableList.of(await produkApi.getAll()); }
    catch (e) { error = e.toString(); }
    finally { isLoading = false; }
  }
}

// widget
Observer(builder: (_) {
  if (store.isLoading) return const CircularProgressIndicator();
  if (store.error != null) return Text(store.error!);
  return ProdukList(produk: store.produk);
})
// Baris kode: ~25 + codegen | Testability: Sedang | State scope: Global/Local

Decision Tree — Langkah demi Langkah #

MULAI
  │
  ▼
Apakah state hanya digunakan dalam SATU widget?
  │
  ├─ YA → Gunakan setState atau ValueNotifier
  │         Tidak perlu library eksternal
  │
  └─ TIDAK → State perlu dibagikan ke banyak widget
               │
               ▼
             Berapa besar tim dan proyek?
               │
               ├─ KECIL (1-3 orang, <10 screen)
               │    │
               │    ├─ Familiar dengan React/MobX? → MobX
               │    └─ Tidak → Provider
               │
               ├─ MENENGAH (3-8 orang, 10-30 screen)
               │    │
               │    ├─ Butuh async state yang elegan? → Riverpod
               │    ├─ Tim sudah pakai Provider? → Tetap Provider
               │    └─ Butuh traceability penuh? → Bloc/Cubit
               │
               └─ BESAR (8+ orang, 30+ screen, enterprise)
                    │
                    ├─ Butuh audit trail event? → Bloc
                    ├─ Butuh fleksibilitas + testability? → Riverpod
                    └─ Tim biasa dengan reactive programming? → MobX

Faktor-faktor Keputusan #

1. Pengalaman Tim #

Tim baru Flutter:
  → setState → Provider → Riverpod (bertahap)
  → Jangan langsung pakai Bloc atau MobX

Tim berpengalaman React/Redux/MobX:
  → MobX akan terasa sangat familiar
  → Atau Riverpod dengan paradigma functional

Tim enterprise dengan background Java/C#:
  → Bloc sangat cocok -- pola event-driven mirip Command pattern

2. Kebutuhan Testability #

Unit test yang mudah adalah keunggulan Riverpod dan Bloc:

Riverpod:
  final container = ProviderContainer(overrides: [
    produkRepoProvider.overrideWith((_) => MockProdukRepo()),
  ]);
  final state = await container.read(produkProvider.future);
  expect(state, isA<List<Produk>>());

Bloc:
  blocTest<ProdukBloc, ProdukState>(
    'emits [Loading, Loaded] ketika LoadProduk dikirim',
    build: () => ProdukBloc(mockRepo),
    act: (bloc) => bloc.add(LoadProduk()),
    expect: () => [ProdukLoading(), ProdukLoaded(mockProduk)],
  );

3. Penanganan State Async #

Async state adalah salah satu yang paling sering menyebabkan bug.
Setiap library menanganinya berbeda:

setState:    Manual (isLoading, data, error terpisah -- rawan tidak konsisten)
Provider:    FutureProvider bagus, tapi kurang ergonomis untuk mutasi
Riverpod:    AsyncValue.when() paling elegan -- loading/error/data ter-handle
Bloc:        Perlu emit state terpisah (Loading, Loaded, Error) -- verbose tapi jelas
MobX:        @action async cukup intuitif, tapi perlu hati-hati dengan state

4. Skala dan Kompleksitas #

Fitur sederhana (toggle, counter, form local):
  → setState / ValueNotifier selalu cukup

Aplikasi dengan 5-15 screen, shared state terbatas:
  → Provider atau Riverpod

Aplikasi besar dengan banyak fitur paralel:
  → Riverpod (provider per-feature yang isolated)
  → atau Bloc (satu Bloc per feature)

Aplikasi enterprise dengan kebutuhan audit:
  → Bloc -- setiap perubahan state punya Event yang tercatat

5. Apakah Ada Library Lain yang Sudah Digunakan? #

Menggunakan Firebase?
  → Riverpod StreamProvider sangat cocok untuk authStateChanges
  → atau Provider StreamProvider

Menggunakan Freezed untuk immutable data class?
  → Riverpod sangat sinergis dengan Freezed
  → Bloc juga cocok (state class dengan Freezed)

Menggunakan GetIt sebagai service locator?
  → Pertimbangkan Riverpod yang menggantikan GetIt + state management sekaligus

Rekomendasi Berdasarkan Skenario #

Proyek Personal / Side Project #

Pilihan: Riverpod Sedikit boilerplate, penanganan async yang elegan, compile-time safe. Tidak perlu khawatir Provider tidak tersedia di luar widget tree.

Startup / Tim Kecil yang Bergerak Cepat #

Pilihan: Riverpod atau Provider Provider lebih mudah untuk onboarding anggota baru. Riverpod untuk tim yang sudah siap belajar sedikit lebih banyak demi manfaat jangka panjang.

Aplikasi Enterprise / Tim Besar #

Pilihan: Bloc Strict architecture, traceability penuh, ekosistem testing yang matang. Overhead boilerplate terbayar dengan maintainability di skala besar.

Developer dengan Background React #

Pilihan: MobX Filosofi yang sangat mirip — observable, action, computed adalah konsep yang langsung familiar. Produktivitas awal paling tinggi.

Migrasi dari setState ke State Management #

Jalur yang direkomendasikan:

setState
  ↓ (state mulai dibagikan ke 2-3 widget)
ValueNotifier / ChangeNotifier
  ↓ (state dibagikan ke banyak halaman)
Provider
  ↓ (butuh async state yang lebih baik, compile-time safety)
Riverpod

Bisa Menggunakan Lebih dari Satu? #

Ya — dan ini umum di aplikasi nyata:

// setState untuk UI state lokal (expand/collapse, hover, focus)
// Provider/Riverpod untuk shared business state
// MobX untuk fitur yang sangat reactive

// Contoh: aplikasi e-commerce
- setState: tab yang aktif di detail produk (lokal)
- Riverpod: daftar produk, auth state, keranjang (shared)
- Provider: tema, lokalisasi (app-wide config)

Yang penting: konsisten di dalam satu fitur. Jangan campur Riverpod dan Bloc untuk fitur yang sama.


Ringkasan #

  • Tidak ada pilihan yang “terbaik” secara universal — pilih berdasarkan konteks: ukuran tim, pengalaman, skala proyek, dan kebutuhan spesifik.
  • setState/ValueNotifier untuk state lokal — jangan diremehkan, ia sangat capable untuk banyak skenario.
  • Provider untuk tim baru yang butuh shared state tanpa kurva belajar yang curam.
  • Riverpod untuk proyek menengah-besar yang butuh compile-time safety, async state yang elegan, dan testability tinggi.
  • Bloc untuk enterprise dengan tim besar — strict architecture, traceability penuh, dan ekosistem testing yang matang.
  • MobX untuk developer dengan background React/JavaScript yang menginginkan reactive programming dengan boilerplate minimal.
  • Konsistensi lebih penting dari pilihan — sebuah proyek yang konsisten dengan Provider lebih baik dari proyek yang setengah Riverpod setengah Bloc.
  • Boleh menggunakan lebih dari satu pendekatan — tapi pastikan konsisten dalam satu fitur yang sama.

← Sebelumnya: MobX   Berikutnya: Best Practice →

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