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 bersama ValueListenableBuilder untuk rebuild yang minimal dan granular.
  • ChangeNotifier menyimpan banyak nilai dan memberi kontrol penuh atas kapan notifyListeners() dipanggil. Cocok untuk state yang lebih kompleks.
  • ListenableBuilder (Flutter 3.7+) adalah cara modern untuk listen ke ChangeNotifier — setara dengan Consumer di Provider tapi tanpa library eksternal.
  • ValueNotifier adalah subclass dari ChangeNotifier — bedanya, ValueNotifier otomatis notify saat .value di-assign, sedangkan ChangeNotifier harus memanggil notifyListeners() secara eksplisit.
  • Untuk state yang perlu dibagikan ke banyak halaman atau butuh dependency injection yang bersih, pertimbangkan upgrade ke Provider atau Riverpod.

← Sebelumnya: Overview   Berikutnya: Provider →

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