setState & ValueNotifier #
Sebelum menggunakan library state management eksternal, penting memahami alat yang sudah disediakan Flutter secara built-in. setState, ValueNotifier, dan ChangeNotifier bukanlah alat “pemula” yang harus segera ditinggalkan — mereka adalah fondasi yang digunakan oleh semua library state management, dan sangat capable untuk banyak skenario nyata.
setState — Fondasi yang Sudah Kamu Kenal #
setState() adalah mekanisme paling dasar untuk memperbarui UI di Flutter. Ia memberi tahu framework bahwa state internal widget telah berubah dan perlu di-rebuild.
class _KonterState extends State<KonterWidget> {
int _nilai = 0;
bool _isLoading = false;
String _pesan = '';
// setState membungkus perubahan state
void _tambah() {
setState(() {
_nilai++;
_pesan = 'Nilai sekarang: $_nilai';
});
}
// Semua perubahan di satu setState -- lebih efisien
Future<void> _reset() async {
setState(() => _isLoading = true);
await Future.delayed(const Duration(seconds: 1));
if (!mounted) return;
setState(() {
_nilai = 0;
_isLoading = false;
_pesan = 'Direset!';
});
}
@override
Widget build(BuildContext context) {
return Column(
children: [
if (_isLoading) const CircularProgressIndicator()
else Text('$_nilai', style: const TextStyle(fontSize: 48)),
Text(_pesan),
Row(
children: [
ElevatedButton(onPressed: _tambah, child: const Text('+')),
ElevatedButton(onPressed: _reset, child: const Text('Reset')),
],
),
],
);
}
}
Keterbatasan setState #
setState hanya me-rebuild widget yang berisi setState tersebut dan seluruh subtree-nya. Ini menjadi masalah ketika:
1. State perlu dibagikan ke widget lain yang bukan descendant:
WidgetA (punya state X)
WidgetB (butuh state X) -- bukan child dari WidgetA!
--> setState tidak bisa membagikan state ke sini
2. Prop drilling yang dalam:
Screen → Section → Card → Item → Text (butuh state dari Screen)
--> harus kirim props melalui 4 layer -- melelahkan!
3. Bisnis logik tercampur dengan kode UI:
class _ScreenState extends State<Screen> {
// Logika validasi, kalkulasi, format, fetch -- semua di sini
// Sulit diuji secara unit test
}
ValueNotifier — setState yang Lebih Granular #
ValueNotifier<T> adalah subclass dari ChangeNotifier yang menyimpan satu nilai bertipe T. Ia secara otomatis menotifikasi listener ketika nilainya berubah (berbeda dari ChangeNotifier yang harus memanggil notifyListeners() secara manual).
// Buat ValueNotifier
final ValueNotifier<int> _counter = ValueNotifier<int>(0);
final ValueNotifier<bool> _isLoading = ValueNotifier<bool>(false);
final ValueNotifier<String?> _error = ValueNotifier<String?>(null);
// Ubah nilai -- listener otomatis di-notify
_counter.value++;
_isLoading.value = true;
_error.value = 'Terjadi kesalahan';
// Baca nilai
print(_counter.value); // 1
ValueListenableBuilder — Rebuild Minimal #
Pasangan alami ValueNotifier adalah ValueListenableBuilder — ia hanya me-rebuild bagian UI yang benar-benar bergantung pada nilai:
class KonterScreen extends StatelessWidget {
// ValueNotifier bisa hidup di StatelessWidget!
final _counter = ValueNotifier<int>(0);
KonterScreen({super.key});
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: const AppBar(title: Text('Konter')), // tidak pernah rebuild
body: Center(
child: ValueListenableBuilder<int>(
valueListenable: _counter,
// child: statis, tidak di-rebuild setiap perubahan nilai
child: const Text('Tap tombol untuk menambah'),
builder: (context, value, child) {
// Hanya bagian ini yang rebuild saat _counter berubah!
return Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Text(
'$value',
style: const TextStyle(fontSize: 72, fontWeight: FontWeight.bold),
),
child!, // child statis di-reuse
],
);
},
),
),
floatingActionButton: FloatingActionButton(
onPressed: () => _counter.value++, // tidak butuh setState!
child: const Icon(Icons.add),
),
);
}
}
ValueNotifier untuk State yang Lebih Kompleks #
// Gunakan dengan data class + copyWith
@immutable
class ProfilState {
final String nama;
final String email;
final bool isLoading;
final String? error;
const ProfilState({
required this.nama,
required this.email,
this.isLoading = false,
this.error,
});
ProfilState copyWith({
String? nama,
String? email,
bool? isLoading,
String? error,
}) {
return ProfilState(
nama: nama ?? this.nama,
email: email ?? this.email,
isLoading: isLoading ?? this.isLoading,
error: error,
);
}
}
class ProfilNotifier extends ValueNotifier<ProfilState> {
ProfilNotifier() : super(const ProfilState(nama: '', email: ''));
Future<void> loadProfil(String userId) async {
value = value.copyWith(isLoading: true, error: null);
try {
final profil = await profilApi.getById(userId);
value = value.copyWith(
nama: profil.nama,
email: profil.email,
isLoading: false,
);
} catch (e) {
value = value.copyWith(isLoading: false, error: e.toString());
}
}
void updateNama(String nama) {
value = value.copyWith(nama: nama);
}
}
// Penggunaan di widget
class ProfilScreen extends StatefulWidget {
const ProfilScreen({super.key});
@override
State<ProfilScreen> createState() => _ProfilScreenState();
}
class _ProfilScreenState extends State<ProfilScreen> {
late final ProfilNotifier _notifier;
@override
void initState() {
super.initState();
_notifier = ProfilNotifier()..loadProfil('user-123');
}
@override
Widget build(BuildContext context) {
return ValueListenableBuilder<ProfilState>(
valueListenable: _notifier,
builder: (context, state, _) {
if (state.isLoading) return const Center(child: CircularProgressIndicator());
if (state.error != null) return Center(child: Text('Error: ${state.error}'));
return Column(
children: [
Text(state.nama),
Text(state.email),
],
);
},
);
}
@override
void dispose() {
_notifier.dispose();
super.dispose();
}
}
ChangeNotifier — Satu Notifier, Banyak Nilai #
ChangeNotifier memberikan lebih banyak kontrol dari ValueNotifier — kamu memutuskan sendiri kapan memanggil notifyListeners(), dan bisa menyimpan banyak nilai sekaligus.
class KeranjangModel extends ChangeNotifier {
final List<CartItem> _items = [];
// Getter untuk akses state (immutable dari luar)
List<CartItem> get items => List.unmodifiable(_items);
int get jumlahItem => _items.fold(0, (sum, item) => sum + item.jumlah);
double get totalHarga => _items.fold(0, (sum, item) => sum + item.subtotal);
void tambah(Produk produk) {
final index = _items.indexWhere((i) => i.produk.id == produk.id);
if (index >= 0) {
_items[index] = _items[index].copyWith(
jumlah: _items[index].jumlah + 1,
);
} else {
_items.add(CartItem(produk: produk, jumlah: 1));
}
notifyListeners(); // beritahu semua listener
}
void hapus(String produkId) {
_items.removeWhere((i) => i.produk.id == produkId);
notifyListeners();
}
void kosongkan() {
_items.clear();
notifyListeners();
}
}
ListenableBuilder — Widget Listener Modern #
Sejak Flutter 3.7, ListenableBuilder adalah cara modern untuk listen ke ChangeNotifier (dan Listenable lainnya) tanpa library eksternal:
class _KeranjangScreenState extends State<KeranjangScreen> {
final _keranjang = KeranjangModel();
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: ListenableBuilder(
listenable: _keranjang,
builder: (context, _) => Text(
'Keranjang (${_keranjang.jumlahItem})',
),
),
),
body: ListenableBuilder(
listenable: _keranjang,
builder: (context, child) {
if (_keranjang.items.isEmpty) {
return const Center(child: Text('Keranjang kosong'));
}
return Column(
children: [
Expanded(
child: ListView.builder(
itemCount: _keranjang.items.length,
itemBuilder: (context, index) {
final item = _keranjang.items[index];
return ListTile(
title: Text(item.produk.nama),
subtitle: Text('${item.jumlah}x'),
trailing: Text('Rp ${item.subtotal}'),
onLongPress: () => _keranjang.hapus(item.produk.id),
);
},
),
),
child!, // Total -- tidak di-rebuild bersama list
],
);
},
child: ListenableBuilder(
listenable: _keranjang,
builder: (context, _) => Padding(
padding: const EdgeInsets.all(16),
child: Text(
'Total: Rp ${_keranjang.totalHarga.toStringAsFixed(0)}',
style: const TextStyle(fontSize: 18, fontWeight: FontWeight.bold),
),
),
),
),
);
}
@override
void dispose() {
_keranjang.dispose();
super.dispose();
}
}
Kapan Upgrade ke Library Eksternal? #
Tetap gunakan setState / ValueNotifier / ChangeNotifier jika:
✓ State hanya relevan untuk satu screen atau area kecil
✓ Tidak butuh sharing state antar halaman yang jauh
✓ Tim kecil atau proyek personal
✓ Tidak perlu dependency injection
Pertimbangkan Provider / Riverpod / Bloc jika:
✗ State yang sama dibutuhkan di banyak screen yang berbeda
✗ Perlu inject service/repository ke ChangeNotifier secara clean
✗ Perlu testing yang mudah (mock dependencies)
✗ Tim besar yang butuh konvensi yang konsisten
✗ Perlu scoping provider per feature (Riverpod)
Ringkasan #
setState()memicu rebuild widget dan seluruh subtree-nya — cocok untuk state lokal sederhana. Batasi scope rebuild dengan memindahkan state ke widget yang lebih kecil.ValueNotifier<T>menyimpan satu nilai dan otomatis menotifikasi listener saat nilai berubah. Gunakan bersamaValueListenableBuilderuntuk rebuild yang minimal dan granular.ChangeNotifiermenyimpan banyak nilai dan memberi kontrol penuh atas kapannotifyListeners()dipanggil. Cocok untuk state yang lebih kompleks.ListenableBuilder(Flutter 3.7+) adalah cara modern untuk listen keChangeNotifier— setara dengan Consumer di Provider tapi tanpa library eksternal.ValueNotifieradalah subclass dariChangeNotifier— bedanya,ValueNotifierotomatis notify saat.valuedi-assign, sedangkanChangeNotifierharus memanggilnotifyListeners()secara eksplisit.- Untuk state yang perlu dibagikan ke banyak halaman atau butuh dependency injection yang bersih, pertimbangkan upgrade ke Provider atau Riverpod.