Mobile

State Management di Flutter - Perbandingan Provider Riverpod dan Bloc

Pertama kali bikin app Flutter yang agak kompleks, saya langsung kena masalah klasik: state berantakan. Satu halaman update data, halaman lain nggak ikut berubah. Passing data lewat constructor sampai 5 level ke bawah. Sound familiar? Kalau kamu pernah ngalamin ini, berarti saatnya kenalan dengan state management di Flutter.

Flutter punya banyak opsi state management, tapi tiga yang paling populer di komunitas: Provider, Riverpod, dan Bloc. Masing-masing punya filosofi dan cara kerja yang beda. Di artikel ini, saya akan bandingkan ketiganya dari sisi praktis bukan cuma teori, tapi juga contoh kode dan kapan sebaiknya pakai masing-masing.

Apa Itu State Management dan Kenapa Kamu Butuh?

State adalah data yang berubah selama aplikasi berjalan. Misalnya: apakah user sudah login? Berapa item di keranjang? Apakah sedang loading data dari API? Tanpa state management yang baik, kamu akan:

  • Passing data lewat constructor ke mana-mana (prop drilling)
  • Memanggil setState() di mana-mana sampai kode jadi sulit di-maintain
  • Bug yang muncul karena state tidak sinkron antar halaman
  • Kesulitan melakukan testing karena logika tersebar

State management membantu memisahkan logika bisnis dari UI. Hasilnya? Kode lebih rapi, lebih mudah di-test, dan lebih gampang di-maintain saat aplikasi makin besar.

Provider Solusi Resmi dari Tim Flutter

Provider adalah state management yang direkomendasikan langsung oleh tim Flutter. Konsepnya sederhana: kamu punya data (state), dan widget yang butuh data itu bisa "mendengarkan" perubahan tanpa perlu passing data manual.

Provider menggunakan InheritedWidget di bawah hood, tapi kamu nggak perlu pusing soal itu. Yang perlu kamu tahu:

  • ChangeNotifierProvider untuk state sederhana yang bisa berubah
  • FutureProvider untuk data yang diambil secara async
  • StreamProvider untuk data yang berubah secara real-time
  • MultiProvider untuk menggabungkan beberapa provider

Berikut contoh sederhana counter app pakai Provider:

// counter_provider.dart
import 'package:flutter/material.dart';

class CounterProvider extends ChangeNotifier {
  int _count = 0;
  int get count => _count;

  void increment() {
    _count++;
    notifyListeners(); // Memberitahu semua listener bahwa state berubah
  }

  void decrement() {
    if (_count > 0) {
      _count--;
      notifyListeners();
    }
  }

  void reset() {
    _count = 0;
    notifyListeners();
  }
}
// main.dart   wrap app dengan ChangeNotifierProvider
import 'package:flutter/material.dart';
import 'package:provider/provider.dart';
import 'counter_provider.dart';

void main() {
  runApp(
    ChangeNotifierProvider(
      create: (_) => CounterProvider(),
      child: const MyApp(),
    ),
  );
}

class CounterPage extends StatelessWidget {
  const CounterPage({super.key});

  @override
  Widget build(BuildContext context) {
    // Consumer membangun ulang hanya widget di dalamnya saat state berubah
    return Consumer<CounterProvider>(
      builder: (context, counter, child) {
        return Column(
          mainAxisAlignment: MainAxisAlignment.center,
          children: [
            Text('${counter.count}', style: const TextStyle(fontSize: 48)),
            const SizedBox(height: 20),
            Row(
              mainAxisAlignment: MainAxisAlignment.center,
              children: [
                ElevatedButton(
                  onPressed: counter.decrement,
                  child: const Text('-'),
                ),
                const SizedBox(width: 16),
                ElevatedButton(
                  onPressed: counter.increment,
                  child: const Text('+'),
                ),
              ],
            ),
          ],
        );
      },
    );
  }
}

Kelebihan Provider:

  • Mudah dipelajari untuk pemula
  • Dokumentasi lengkap dan banyak tutorial
  • Didukung langsung oleh tim Flutter
  • Boilerplate kode relatif sedikit

Kekurangan Provider:

  • Bisa jadi sulit di-maintain saat app besar (banyak ChangeNotifier)
  • Tidak compile-safe runtime error jika lupa daftarkan provider
  • Tidak mendukung modifier seperti autoDispose secara native

Riverpod Provider yang Lebih Kuat dan Type-Safe

Riverpod dibuat oleh Remi Rousselet, orang yang sama yang bikin Provider. Tujuannya: memperbaiki kekurangan Provider. Di Riverpod, provider didefinisikan sebagai variabel global (top-level), bukan di dalam widget tree. Ini bikinnya lebih mudah diakses dan di-test.

Kelebihan utama Riverpod dibanding Provider:

  • Type-safe error ketahuan saat compile, bukan runtime
  • AutoDispose state otomatis dihapus saat tidak digunakan
  • Family modifier bikin provider dengan parameter
  • Bisa digunakan di mana saja nggak perlu BuildContext
// counter_riverpod.dart
import 'package:flutter_riverpod/flutter_riverpod.dart';

// StateNotifier untuk logic yang lebih kompleks
class CounterNotifier extends StateNotifier<int> {
  CounterNotifier() : super(0);

  void increment() => state++;
  void decrement() {
    if (state > 0) state--;
  }
  void reset() => state = 0;
}

// Definisi provider   top-level, bisa diakses dari mana saja
final counterProvider = StateNotifierProvider<CounterNotifier, int>((ref) {
  return CounterNotifier();
});

// Contoh FutureProvider   untuk data async
final userProvider = FutureProvider<User>((ref) async {
  final response = await http.get(Uri.parse('https://api.example.com/user'));
  return User.fromJson(jsonDecode(response.body));
});

// Contoh Provider dengan parameter (family)
final postProvider = FutureProvider.family<Post, int>((ref, postId) async {
  final response = await http.get(
    Uri.parse('https://api.example.com/posts/$postId'),
  );
  return Post.fromJson(jsonDecode(response.body));
});
// Menggunakan di widget   pakai ConsumerWidget, bukan StatelessWidget
import 'package:flutter_riverpod/flutter_riverpod.dart';

class CounterPage extends ConsumerWidget {
  const CounterPage({super.key});

  @override
  Widget build(BuildContext context, WidgetRef ref) {
    // ref.watch()   rebuild saat state berubah
    final count = ref.watch(counterProvider);

    return Column(
      mainAxisAlignment: MainAxisAlignment.center,
      children: [
        Text('$count', style: const TextStyle(fontSize: 48)),
        const SizedBox(height: 20),
        Row(
          mainAxisAlignment: MainAxisAlignment.center,
          children: [
            ElevatedButton(
              // ref.read()   sekali baca, tidak listen
              onPressed: () => ref.read(counterProvider.notifier).decrement(),
              child: const Text('-'),
            ),
            const SizedBox(width: 16),
            ElevatedButton(
              onPressed: () => ref.read(counterProvider.notifier).increment(),
              child: const Text('+'),
            ),
          ],
        ),
      ],
    );
  }
}

Kelebihan Riverpod:

  • Type-safe error compile-time, bukan runtime
  • AutoDispose dan family modifier built-in
  • Mudah di-test (nggak perlu mock BuildContext)
  • Bisa combine beberapa provider dengan mudah
  • Generator support (riverpod_generator) untuk codegen

Kekurangan Riverpod:

  • Learning curve lebih tinggi dari Provider
  • Syntax agak verbose (terutama tanpa codegen)
  • Komunitas lebih kecil dari Provider (tapi berkembang pesat)

Bloc Arsitektur yang Terstruktur dan Testable

Bloc (Business Logic Component) mengikuti pola arsitektur yang lebih formal. Konsepnya: semua perubahan state harus melalui Event Bloc State. Nggak ada cara langsung mengubah state dari luar kamu harus mengirim event terlebih dahulu.

Pendekatan ini sangat cocok untuk tim besar yang butuh konsistensi kode. Setiap developer tahu persis bagaimana state berubah, karena semua perubahan melewati satu jalur yang terdefinisi.

// counter_event.dart
abstract class CounterEvent {}

class IncrementEvent extends CounterEvent {}
class DecrementEvent extends CounterEvent {}
class ResetEvent extends CounterEvent {}
// counter_state.dart
class CounterState {
  final int count;
  final bool isLoading;

  const CounterState({this.count = 0, this.isLoading = false});

  CounterState copyWith({int? count, bool? isLoading}) {
    return CounterState(
      count: count ?? this.count,
      isLoading: isLoading ?? this.isLoading,
    );
  }
}
// counter_bloc.dart
import 'package:flutter_bloc/flutter_bloc.dart';

class CounterBloc extends Bloc<CounterEvent, CounterState> {
  CounterBloc() : super(const CounterState()) {
    on<IncrementEvent>((event, emit) {
      emit(state.copyWith(count: state.count + 1));
    });

    on<DecrementEvent>((event, emit) {
      if (state.count > 0) {
        emit(state.copyWith(count: state.count - 1));
      }
    });

    on<ResetEvent>((event, emit) {
      emit(const CounterState());
    });
  }
}
// Menggunakan di widget   BlocProvider + BlocBuilder
class CounterPage extends StatelessWidget {
  const CounterPage({super.key});

  @override
  Widget build(BuildContext context) {
    return BlocProvider(
      create: (_) => CounterBloc(),
      child: BlocBuilder<CounterBloc, CounterState>(
        builder: (context, state) {
          return Column(
            mainAxisAlignment: MainAxisAlignment.center,
            children: [
              Text('${state.count}', style: const TextStyle(fontSize: 48)),
              const SizedBox(height: 20),
              Row(
                mainAxisAlignment: MainAxisAlignment.center,
                children: [
                  ElevatedButton(
                    onPressed: () => context.read<CounterBloc>().add(DecrementEvent()),
                    child: const Text('-'),
                  ),
                  const SizedBox(width: 16),
                  ElevatedButton(
                    onPressed: () => context.read<CounterBloc>().add(IncrementEvent()),
                    child: const Text('+'),
                  ),
                ],
              ),
            ],
          );
        },
      ),
    );
  }
}

Kelebihan Bloc:

  • Arsitektur terstruktur cocok untuk tim besar
  • Sangat testable setiap event dan state bisa di-test terpisah
  • Debugging mudah dengan BlocObserver
  • Prediktable semua perubahan state tercatat
  • Komunitas besar dan ekosistem matang

Kekurangan Bloc:

  • Boilerplate kode paling banyak (event, state, bloc terpisah)
  • Untuk app sederhana, terasa overkill
  • Learning curve paling tinggi di antara ketiganya

Perbandingan Langsung: Provider vs Riverpod vs Bloc

Biar lebih jelas, berikut tabel perbandingan dari berbagai aspek:

  • Complexity: Provider (rendah) Riverpod (sedang) Bloc (tinggi)
  • Boilerplate: Provider (sedikit) Riverpod (sedang) Bloc (banyak)
  • Type Safety: Provider (runtime) Riverpod (compile-time) Bloc (compile-time)
  • Testability: Provider (cukup) Riverpod (baik) Bloc (sangat baik)
  • Learning Curve: Provider (mudah) Riverpod (sedang) Bloc (sulit)
  • Best for: Provider (small-medium app) Riverpod (medium-large app) Bloc (enterprise/large team)

Kapan Harus Pakai yang Mana?

Ini pertanyaan yang paling sering ditanyakan dan jawabannya tergantung konteks:

Pakai Provider kalau:

  • Kamu baru mulai belajar Flutter
  • Aplikasi masih sederhana sampai menengah
  • Butuh solusi cepat tanpa banyak konfigurasi
  • Tim kecil (1-3 orang)

Pakai Riverpod kalau:

  • Kamu sudah paham Provider tapi butuh lebih banyak fitur
  • Butuh type safety dan auto dispose
  • Aplikasi menengah sampai besar dengan banyak state
  • Ingin testing yang lebih mudah tanpa BuildContext

Pakai Bloc kalau:

  • Tim besar yang butuh standar arsitektur yang ketat
  • Aplikasi enterprise dengan complex business logic
  • Butuh debug dan logging yang detail
  • Sudah familiar dengan reactive programming atau pattern BLoC

Tips dari Pengalaman Saya

Setelah pakai ketiganya di project yang berbeda, berikut beberapa insight:

  • Jangan terlalu dini pakai Bloc untuk app personal atau prototype, Provider atau Riverpod sudah lebih dari cukup. Bloc baru terasa manfaatnya saat tim besar dan kode harus sangat terstruktur.
  • Riverpod adalah upgrade yang worth it dari Provider kalau kamu sudah paham Provider, transisi ke Riverpod relatif mulus. Dan fitur autoDispose-nya sangat menghemat memori.
  • Mix and match boleh beberapa project menggunakan Riverpod untuk state global dan setState() untuk state lokal yang sederhana (seperti form validation). Nggak harus pakai satu solusi untuk semua.
  • Jangan over-engineer kalau app kamu cuma butuh 2-3 state sederhana, setState() atau ValueNotifier sudah cukup. Nggak perlu pasang framework berat untuk masalah kecil.

Kesimpulan

Tidak ada state management yang "paling baik" yang ada adalah yang paling cocok untuk kebutuhanmu. Provider untuk kesederhanaan, Riverpod untuk keseimbangan fitur dan usability, Bloc untuk arsitektur enterprise. Yang penting: pahami dulu masalah yang kamu hadapi, baru pilih solusinya.

Kalau kamu baru mulai, mulai dari Provider. Sudah nyaman? Coba Riverpod. Butuh standar ketat untuk tim besar? Bloc jawabannya. Apapun pilihanmu, pastikan kamu paham konsep dasarnya bukan cuma copy-paste kode dari tutorial.

Kamu sendiri pakai state management yang mana di project Flutter-mu? Share di kolom komentar ya!


You may also like


0 Comments


Leave a Reply

Comments with links or spam keywords will be rejected.
Scroll to Top