setState

Mengubah state dengan setState, memahami kontrak rebuild-nya, dan menerapkan alur kerja hot reload.

Daftar isi

Topik ini membahas cara mengubah state, yaitu data sebuah widget yang nilainya dapat berubah sepanjang masa hidupnya, menggunakan setState. Pembahasan mencakup kontrak rebuild, yakni proses memanggil ulang metode build untuk menggambar ulang tampilan, kesalahan yang membuat layar tidak ikut diperbarui, serta alur kerja hot reload, yaitu memuat ulang kode ke aplikasi yang sedang berjalan. Seluruh materi diterapkan pada aplikasi penghitung langkah utuh di bagian akhir.

State lokal pada StatefulWidget#

StatefulWidget menyimpan data yang dapat berubah di dalam objek State. Field pada kelas State itulah yang disebut state lokal. Sebuah aplikasi penghitung langkah, misalnya, cukup menyimpan satu field bertipe int untuk jumlah langkah. Nilai field tersebut dibaca oleh metode build untuk menampilkan angka di layar.

dart Kelas State dengan satu field state
// tidak-dijalankan
class _LayarPenghitungState extends State<LayarPenghitung> {
  int _langkah = 0;

  @override
  Widget build(BuildContext context) {
    return Text('Jumlah langkah: $_langkah');
  }
}

Perubahan pada field hanyalah perubahan data biasa. Flutter tidak memantau setiap field secara otomatis, dan kerangka kerja hanya membangun ulang antarmuka apabila diberi tahu. Memberi tahu kerangka kerja itulah tugas setState.

Kontrak setState#

setState menerima satu callback, yaitu fungsi yang diteruskan sebagai argumen, lalu menjalankannya secara sinkron. Setelah callback selesai, Flutter menandai elemen widget yang bersangkutan perlu dibangun ulang, sehingga metode build dijadwalkan pada frame berikutnya, yaitu satu siklus penggambaran layar. Dengan penjadwalan tersebut, perubahan tidak digambar di tengah proses yang setengah selesai, melainkan tampil utuh pada frame berikutnya.

Kontraknya sederhana tetapi wajib dipenuhi. Apabila field diubah tanpa setState, nilai di memori memang berubah, tetapi build tidak pernah dipanggil ulang dan layar tetap menampilkan nilai lama. Kasus ini sulit dilacak karena tidak ada error sama sekali.

dart Perubahan field tanpa setState tidak memicu rebuild
// tidak-dijalankan
void _tambahLangkah() {
  // Nilai bertambah di memori, tetapi build tidak pernah dipanggil ulang.
  _langkah += 1;
}

Bentuk yang benar menaruh perubahan di dalam closure setState. Closure adalah fungsi yang ditulis di dalam fungsi lain dan dapat membaca variabel milik fungsi pembungkusnya. Isi closure boleh berupa lebih dari satu pernyataan, dan semuanya selesai dijalankan sebelum rebuild dijadwalkan.

dart Perubahan field di dalam closure setState
// tidak-dijalankan
void _tambahLangkah() {
  setState(() {
    _langkah += 1;
  });
}

Kapan setState dipanggil#

Panggilan setState dilakukan dari dalam kelas State, biasanya sebagai reaksi atas peristiwa. Sumbernya dapat berupa callback tombol, hasil operasi asinkron, atau pemberitahuan dari listener, yaitu fungsi yang dipanggil setiap kali peristiwa tertentu terjadi. Dua tempat tidak tepat untuk memanggil setState. Di dalam metode build, panggilan itu melempar error setState() or markNeedsBuild() called during build, sedangkan di dalam initState panggilan tidak diperlukan karena build pertama memang akan berjalan.

  • Saat pengguna menekan tombol dan nilai yang baru perlu tampil di layar.
  • Saat hasil operasi asinkron tiba dan datanya digunakan oleh tampilan.
  • Saat listener atau timer melaporkan perubahan yang dibaca oleh tampilan.

Setiap panggilan setState menjadwalkan rebuild untuk seluruh metode build milik kelas State tersebut, bukan hanya bagian tampilan yang nilainya berubah. Oleh karena itu, widget besar sebaiknya dipecah menjadi widget kecil sehingga rebuild hanya terjadi seluas yang diperlukan. Disiplin ini belum terasa pada aplikasi kecil, tetapi menjadi penting seiring bertambahnya kompleksitas layar.

Hot reload dan hot restart#

Flutter menyediakan dua cara memuat ulang kode saat aplikasi masih berjalan. Hot reload memasukkan kode terbaru ke aplikasi yang sedang berjalan, mengeksekusi ulang metode build, dan mempertahankan state. Hot restart menjalankan ulang fungsi main dari awal sehingga seluruh state kembali ke nilai awalnya.

Alur kerjanya sebagai berikut. Jalankan aplikasi dengan flutter run atau tombol run pada editor, sunting kode, lalu tekan r pada terminal atau tombol hot reload pada editor. Layar diperbarui dalam hitungan detik tanpa mengulang langkah pembuka aplikasi. Apabila perubahan menyentuh struktur yang lebih besar, misalnya penambahan field pada kelas atau penggantian kelas dasar, hot reload dapat gagal dan aplikasi perlu dimuat ulang dengan hot restart.

Aplikasi penghitung langkah utuh#

Kode berikut merupakan aplikasi lengkap yang dapat disalin ke berkas main.dart pada proyek Flutter baru. Widget LayarPenghitung menyimpan field _langkah, dua metode mengubahnya melalui setState, dan metode build membacanya.

dart main.dart
// tidak-dijalankan
import 'package:flutter/material.dart';

void main() => runApp(const AplikasiLangkah());

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

  @override
  Widget build(BuildContext context) {
    return const MaterialApp(
      title: 'Penghitung langkah',
      home: LayarPenghitung(),
    );
  }
}

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

  @override
  State<LayarPenghitung> createState() => _LayarPenghitungState();
}

class _LayarPenghitungState extends State<LayarPenghitung> {
  int _langkah = 0;

  void _tambahLangkah() {
    setState(() {
      _langkah += 1;
    });
  }

  void _aturUlang() {
    setState(() {
      _langkah = 0;
    });
  }

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: const Text('Penghitung langkah'),
        actions: [
          IconButton(
            onPressed: _aturUlang,
            icon: const Icon(Icons.refresh),
          ),
        ],
      ),
      body: Center(
        child: Column(
          mainAxisAlignment: MainAxisAlignment.center,
          children: [
            const Text('Jumlah langkah hari ini'),
            Text(
              '$_langkah',
              style: Theme.of(context).textTheme.headlineMedium,
            ),
          ],
        ),
      ),
      floatingActionButton: FloatingActionButton(
        onPressed: _tambahLangkah,
        child: const Icon(Icons.add),
      ),
    );
  }
}

Setiap penekanan tombol tambah memanggil _tambahLangkah, yang menaikkan _langkah di dalam closure setState. Tombol perbarui pada bilah aplikasi memanggil _aturUlang untuk mengembalikan nilai ke nol. Dengan hot reload, perubahan tampilan seperti penggantian teks dapat dicoba tanpa kehilangan angka langkah yang sedang tampil.

Kesalahan umum#

  • Mengubah field tanpa setState. Layar diam tanpa pesan apa pun karena build tidak pernah dipanggil ulang. Perbaikannya, bungkus perubahan field di dalam closure setState.
  • Memanggil setState di dalam build. Flutter melempar error setState() or markNeedsBuild() called during build. Perubahan seharusnya berada pada callback peristiwa, sedangkan build hanya membaca nilai.
  • Menaruh pekerjaan berat di dalam closure setState. Frame berikutnya tertahan dan animasi terasa tersendat karena closure dijalankan sebelum rebuild dijadwalkan. Hitung atau ambil data lebih dahulu, lalu panggil setState hanya untuk menetapkan hasilnya.

Ringkasan#

  • setState menjalankan closure-nya secara sinkron, lalu menjadwalkan rebuild pada frame berikutnya.
  • Field yang berubah tanpa setState tidak pernah memicu rebuild dan tidak menimbulkan error.
  • Panggilan setState berasal dari reaksi peristiwa di dalam kelas State, bukan dari konstruktor atau build.
  • Hot reload mempertahankan state dan mengeksekusi ulang build, sedangkan hot restart mengosongkan state.

Referensi#