Menaikkan state dan InheritedWidget

Menaikkan state ke widget leluhur dan memperkenalkan InheritedWidget sebagai jalan menuju pengelolaan state yang sesungguhnya.

Daftar isi

Topik ini membahas dua mekanisme berbagi state di Flutter, yaitu pola lifting state up (menaikkan state) dan widget InheritedWidget. Keduanya menyelesaikan masalah yang muncul ketika batas setState terlampaui, dengan cara yang berbeda. Pemahaman atas keduanya menjadi fondasi menuju package Provider, yang dibangun di atas mekanisme InheritedWidget.

Pola menaikkan state#

Pola menaikkan state memindahkan penyimpanan state ke leluhur terdekat yang mencakup seluruh widget pemakainya. Data mengalir turun melalui konstruktor, sedangkan perubahan dilaporkan naik melalui callback. Arahnya selalu satu. Anak tidak pernah mengubah state leluhur secara langsung, melainkan meminta leluhur melakukannya melalui callback.

Contoh daftar belanja pribadi berikut menempatkan List<String> pada widget LayarBelanja, lalu meneruskannya ke dua anak, yaitu KotakInput dan TampilanBelanja.

dart State dinaikkan ke leluhur terdekat
// tidak-dijalankan
class LayarBelanja extends StatefulWidget {
  const LayarBelanja({super.key});

  @override
  State<LayarBelanja> createState() => _LayarBelanjaState();
}

class _LayarBelanjaState extends State<LayarBelanja> {
  final _belanjaan = <String>[];

  void _tambahBarang(String barang) {
    setState(() {
      _belanjaan.add(barang);
    });
  }

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(title: Text('Daftar belanja (${_belanjaan.length})')),
      body: Column(
        children: [
          KotakInput(onTambah: _tambahBarang),
          Expanded(child: TampilanBelanja(belanjaan: _belanjaan)),
        ],
      ),
    );
  }
}

Anak yang menghasilkan perubahan menerima callback tanpa mengetahui bagaimana daftar disimpan, sedangkan anak yang hanya menampilkan menerima data sebagai parameter konstruktor biasa.

dart Anak melaporkan perubahan naik melalui callback
// tidak-dijalankan
class KotakInput extends StatefulWidget {
  const KotakInput({super.key, required this.onTambah});

  final ValueChanged<String> onTambah;

  @override
  State<KotakInput> createState() => _KotakInputState();
}

class _KotakInputState extends State<KotakInput> {
  final _kontrol = TextEditingController();

  void _kirim() {
    if (_kontrol.text.isNotEmpty) {
      widget.onTambah(_kontrol.text);
      _kontrol.clear();
    }
  }

  @override
  Widget build(BuildContext context) {
    return Row(
      children: [
        Expanded(
          child: TextField(
            controller: _kontrol,
            onSubmitted: (_) => _kirim(),
          ),
        ),
        IconButton(onPressed: _kirim, icon: const Icon(Icons.add)),
      ],
    );
  }

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

Di dalam kelas State, callback milik widget diakses melalui widget.onTambah. Keuntungan pola ini adalah konsistensi yang diberikan secara otomatis. Hanya ada satu tempat penyimpanan, sehingga tidak ada dua tampilan yang menunjukkan nilai berbeda untuk data yang sama. Setiap perubahan dari anak memicu setState pada leluhur, dan seluruh anak yang meneruskan data ikut dibangun ulang dengan nilai terbaru.

Diagram berikut merangkum arah aliran pada pola ini. State disimpan pada widget leluhur, data mengalir turun ke anak melalui konstruktor, dan callback membawa laporan perubahan ke arah sebaliknya.

Widget leluhurstate disimpan di siniWidget anak AWidget anak Bdata turunmelalui konstruktorcallback naikmelaporkan perubahan
Aliran data satu arah pada pola menaikkan state

InheritedWidget#

InheritedWidget adalah widget khusus bawaan Flutter yang menyebarkan data ke seluruh subpohon di bawahnya tanpa penerusan konstruktor. Widget mana pun yang berada di bawahnya dapat membaca data tersebut langsung dari BuildContext. Widget bawaan seperti Theme dan MediaQuery sebenarnya bekerja dengan mekanisme ini, sehingga Theme.of(context) yang sudah sering digunakan merupakan contoh nyatanya.

Cara menggunakannya adalah membuat subclass yang menyimpan data pada field final, menimpa updateShouldNotify, dan menyediakan metode statis untuk membacanya. Instans subclass tersebut selanjutnya disebut penyedia karena tugasnya menyediakan data bagi subpohon di bawahnya. Penyedia dapat dipasang pada titik mana pun di atas seluruh pemakainya. Data yang digunakan satu layar cukup diletakkan di atas body layar tersebut, sedangkan data lintas layar dapat ditempatkan di atas MaterialApp agar seluruh subpohon berada di bawahnya. Cakupan data ditentukan oleh penempatan ini, sehingga perlu dirancang bersama dengan rancangan pohon widget.

dart Subclass InheritedWidget untuk data jumlah langkah
// tidak-dijalankan
class PenghitungData extends InheritedWidget {
  const PenghitungData({
    super.key,
    required this.jumlahLangkah,
    required super.child,
  });

  final int jumlahLangkah;

  static PenghitungData? of(BuildContext context) {
    return context.dependOnInheritedWidgetOfExactType<PenghitungData>();
  }

  @override
  bool updateShouldNotify(PenghitungData lama) {
    return lama.jumlahLangkah != jumlahLangkah;
  }
}

Metode of memanggil dependOnInheritedWidgetOfExactType, yang sekaligus mendaftarkan widget pemanggil sebagai dependen. Dengan pendaftaran itu, setiap kali penyedia baru dipasang dengan data yang berubah, metode build seluruh dependen dipanggil ulang. Keputusan apakah dependen perlu diperbarui ditentukan oleh updateShouldNotify, yaitu metode yang membandingkan data lama dengan data baru.

Mekanisme pendaftaran ini berlangsung pada setiap build. Flutter mencatat dependensi saat of dipanggil, lalu menggunakan catatan itu untuk memutuskan widget mana yang ikut dibangun ulang. Karena pencatatan berlangsung per widget, dua widget di bawah penyedia yang sama dapat memiliki perilaku rebuild yang berbeda sesuai pemanggilan of masing-masing.

Pembacaan data dari widget mana pun di bawah penyedia cukup dilakukan dengan satu baris. Karena of mengembalikan tipe nullable, yaitu tipe yang boleh bernilai null, operator ! digunakan ketika keberadaan penyedia sudah dipastikan.

dart Membaca data penyedia dari widget turunan
// tidak-dijalankan
final data = PenghitungData.of(context)!;

Pembacaan melalui of bersifat satu arah dan tidak mengubah apa pun. Nilai yang diterima hanyalah data terbaru pada saat build berjalan, sehingga widget tidak perlu menyimpan salinan datanya sendiri.

Batas InheritedWidget dan jalan menuju Provider#

InheritedWidget menyelesaikan masalah penerusan konstruktor, tetapi penggunaannya masih memerlukan langkah yang rinci. Setiap jenis data menuntut satu subclass, satu metode of, dan satu penimpaan updateShouldNotify. Penyedia juga harus dipasang ulang dengan data baru agar dependen diberi tahu, sehingga state yang bisa berubah tetap perlu tempat penyimpanan tersendiri di atas penyedia.

Package Provider dibangun di atas mekanisme ini. Provider membungkus InheritedWidget dengan API yang lebih ringkas, bekerja sama dengan ChangeNotifier untuk bagian state yang berubah, dan menghilangkan penulisan subclass secara manual. Pemahaman terhadap InheritedWidget membuat cara kerja Provider terlihat jelas, karena keduanya mengandalkan pendaftaran dependen yang sama. State yang digunakan di seluruh aplikasi juga mendapat tempat yang tepat, yaitu kelas tersendiri di luar pohon widget.

Kesalahan umum#

  • updateShouldNotify yang selalu mengembalikan false. Dependen tidak pernah dibangun ulang meskipun data berubah. Kembalikan hasil perbandingan data lama dan data baru.
  • Memanggil of(context) dengan context di atas penyedia. Nilai kembaliannya bernilai null sehingga operator ! melempar error. Pastikan widget pemanggil berada di bawah penyedia dalam pohon.
  • Mengubah isi penyedia tanpa memasang ulang penyedia. Tidak ada pemberitahuan yang dikirim karena Flutter membandingkan instans penyedia, bukan isi fieldnya. Pasang ulang penyedia dengan data baru agar updateShouldNotify bekerja.

Ringkasan#

  • Pola menaikkan state menempatkan state pada leluhur terdekat, data turun melalui konstruktor, dan perubahan naik melalui callback.
  • InheritedWidget menyebarkan data ke seluruh subpohon dan memberi tahu dependen yang terdaftar melalui dependOnInheritedWidgetOfExactType.
  • updateShouldNotify membandingkan data lama dengan data baru untuk memutuskan rebuild.
  • Package Provider dibangun di atas mekanisme InheritedWidget dengan API yang lebih ringkas.

Referensi#