widgetstatefulsetStatePemula5 mnt baca

StatefulWidget & setState

Widget dengan state: counter interaktif dan cara kerja setState.

Analogi: Papan Skor Digital

Bayangkan papan skor digital di lapangan futsal. Bingkai besinya, lampunya, dan kabelnya tidak pernah berubah sepanjang pertandingan, tapi angka di layarnya berubah setiap ada gol. Di Flutter, StatefulWidget bekerja persis seperti itu. Ada bagian yang tetap (konfigurasi widget, tidak bisa diubah) dan ada bagian yang berubah-ubah (datanya). Bagian yang berubah itu disebut state.

Kenapa dipisah? Supaya Flutter tahu persis mana yang boleh berubah. Saat angka berubah, Flutter tidak membangun ulang seluruh papan dari nol, ia hanya menggambar ulang bagian yang berubah. Itulah rahasia kenapa UI Flutter terasa cepat.

Kenapa Konsep State Itu Ada?

Aplikasi tanpa state itu seperti brosur cetak: tampilannya selalu sama apa pun yang kamu lakukan. Tapi aplikasi nyata itu interaktif. Counter bertambah saat tombol ditekan, centang muncul saat item dipilih, daftar belanja bertambah saat barang ditambahkan.

Masalahnya: kalau kamu mengubah variabel biasa di Dart, Flutter tidak akan tahu dan layar tetap menampilkan nilai lama. Kamu butuh cara resmi untuk berkata, "Hei Flutter, dataku berubah, tolong gambar ulang layarku." Cara resmi itu namanya setState.

Flutter memisahkan widget (immutable, tidak bisa diubah setelah dibuat) dari state (mutable, boleh berubah) dengan satu alasan besar: kecepatan dan keamanan. Dengan widget yang immutable, Flutter bisa membandingkan tampilan lama dan baru dengan murah, lalu hanya memperbarui yang berubah. Widget statis tetap memakai StatelessWidget supaya kamu tidak membayar biaya state untuk hal yang memang tidak berubah.

Contoh 1: Counter Sederhana

Struktur StatefulWidget selalu terdiri dari dua kelas: kelas widget dan kelas State.

dart
class Counter extends StatefulWidget {
  const Counter({super.key});

  @override
  State<Counter> createState() => _CounterState();
}

class _CounterState extends State<Counter> {
  int _angka = 0;

  void _tambah() {
    setState(() {
      _angka++;
    });
  }

  @override
  Widget build(BuildContext context) {
    return Column(
      children: [
        Text('Angka: $_angka'),
        ElevatedButton(onPressed: _tambah, child: const Text('Tambah')),
      ],
    );
  }
}

Perhatikan: _angka tinggal di kelas State, bukan di kelas widget. Hanya kelas State yang boleh menyimpan data mutable.

Contoh 2: Daftar Belanja Interaktif

Contoh yang lebih realistis: daftar belanja dengan checkbox dan form tambah item. Di sini state-nya berupa list, bukan sekadar angka.

dart
class DaftarBelanja extends StatefulWidget {
  const DaftarBelanja({super.key});

  @override
  State<DaftarBelanja> createState() => _DaftarBelanjaState();
}

class _DaftarBelanjaState extends State<DaftarBelanja> {
  final List<String> _items = ['Kopi', 'Gula'];
  final Set<String> _dicentang = {};
  final _controller = TextEditingController();

  void _tambahItem() {
    final nama = _controller.text.trim();
    if (nama.isEmpty) return;
    setState(() {
      _items.add(nama);
      _controller.clear();
    });
  }

  void _toggleCentang(String item, bool? nilai) {
    setState(() {
      if (nilai == true) {
        _dicentang.add(item);
      } else {
        _dicentang.remove(item);
      }
    });
  }

  @override
  void dispose() {
    _controller.dispose();
    super.dispose();
  }

  @override
  Widget build(BuildContext context) {
    return Column(
      children: [
        Padding(
          padding: const EdgeInsets.all(12),
          child: Row(
            children: [
              Expanded(child: TextField(controller: _controller)),
              IconButton(icon: const Icon(Icons.add), onPressed: _tambahItem),
            ],
          ),
        ),
        Expanded(
          child: ListView.builder(
            itemCount: _items.length,
            itemBuilder: (context, i) {
              final item = _items[i];
              return CheckboxListTile(
                title: Text(item),
                value: _dicentang.contains(item),
                onChanged: (v) => _toggleCentang(item, v),
              );
            },
          ),
        ),
      ],
    );
  }
}

Pola ini (list + setState + ListView.builder) adalah fondasi hampir semua layar daftar di aplikasi nyata.

Cara Kerja setState

Saat kamu memanggil setState, tiga hal terjadi berurutan:

  1. Callback yang kamu berikan dijalankan, di sinilah kamu mengubah state.
  2. Widget ditandai "kotor" (dirty), artinya tampilannya sudah kedaluwarsa.
  3. Pada frame berikutnya, Flutter memanggil ulang build dan membandingkan hasilnya dengan tampilan lama, lalu hanya memperbarui yang berubah.

Catatan teknis: Jangan panggil setState di dalam build, karena build memicu setState yang memicu build lagi: infinite loop. Untuk operasi async (misalnya setelah await mengambil data), selalu cek if (!mounted) return; sebelum setState, karena widget-nya bisa saja sudah dihapus dari layar saat datanya tiba.

State vs Parameter

Parameter (widget.judul) adalah milik kelas widget: immutable, diteruskan dari parent, dan tidak boleh diubah. State (_angka) adalah milik kelas State: mutable dan privat untuk widget itu sendiri. Dari dalam kelas State, kamu membaca parameter lewat widget.namaParameter.

Aturan praktisnya: kalau datanya datang dari luar dan widget tidak boleh mengubahnya, jadikan parameter. Kalau datanya berubah karena interaksi di dalam widget itu sendiri, jadikan state.

Kesalahan umum pemula

1. Mengubah state tanpa setState: UI tidak ikut berubah.

dart
// SALAH: nilai berubah, tapi layar tetap menampilkan angka lama
void _tambah() {
  _angka++;
}

// BENAR: Flutter diberi tahu, layar digambar ulang
void _tambah() {
  setState(() {
    _angka++;
  });
}

2. Memanggil setState di dalam build: infinite loop.

dart
// SALAH: build -> setState -> build -> setState, selamanya
Widget build(BuildContext context) {
  setState(() { _angka = hitungSesuatu(); });
  return Text('$_angka');
}

// BENAR: hitung di luar build, misalnya di initState atau event handler
@override
void initState() {
  super.initState();
  _angka = hitungSesuatu();
}

3. setState setelah widget di-dispose pada operasi async.

dart
// SALAH: crash "setState() called after dispose()" jika user pindah halaman
Future<void> _muatData() async {
  final data = await ambilDariServer();
  setState(() { _data = data; });
}

// BENAR: cek mounted dulu
Future<void> _muatData() async {
  final data = await ambilDariServer();
  if (!mounted) return;
  setState(() { _data = data; });
}

4. Menaruh logika berat di dalam callback setState. Callback setState sebaiknya hanya berisi perubahan state yang ringan. Panggil fungsi beratnya dulu, simpan hasilnya ke variabel, baru panggil setState untuk menyimpan hasilnya.

Tantangan

Counter plus minus

Kembangkan counter di atas: tambah tombol kurang (-) dan tombol reset. Pastikan angka tidak bisa kurang dari 0.