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.
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:
setState() di mana-mana sampai kode jadi sulit di-maintainState 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 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:
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:
Kekurangan Provider:
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:
// 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:
Kekurangan Riverpod:
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:
Kekurangan Bloc:
Biar lebih jelas, berikut tabel perbandingan dari berbagai aspek:
Ini pertanyaan yang paling sering ditanyakan dan jawabannya tergantung konteks:
Pakai Provider kalau:
Pakai Riverpod kalau:
Pakai Bloc kalau:
Setelah pakai ketiganya di project yang berbeda, berikut beberapa insight:
setState() atau ValueNotifier sudah cukup. Nggak perlu pasang framework berat untuk masalah kecil.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!