Consumer dan Selector

Mempersempit cakupan rebuild dengan Consumer dan Selector.

Daftar isi

Topik ini membahas dua widget dari package Provider yang mengendalikan cakupan rebuild, yaitu Consumer dan Selector. context.watch pada topik sebelumnya membuat seluruh build widget pemanggil dijalankan ulang setiap kali state berubah, sedangkan pada layar yang besar sering kali hanya satu bagian kecil yang benar-benar bergantung pada perubahan tersebut. Consumer memberikan nilai kepada subpohon tertentu saja, dan Selector melangkah lebih jauh dengan berlangganan sebagian data saja. Setelah topik ini dipraktikkan, pemilihan antara ketiganya menjadi keputusan yang memperhitungkan biayanya.

Rebuild yang lebih luas dari yang diperlukan#

Aplikasi jadwal piket memperlihatkan masalah ini dengan jelas. Layarnya memuat judul dengan jumlah piket yang sudah selesai dan daftar kartu piket di bawahnya, sedangkan state StatePiket menyimpan daftar Tugas beserta method untuk menandai tugas selesai dan mengubah namanya. Apabila watch dipanggil pada awal build layar, seluruh build layar menjadi dependen dari penyedianya.

dart watch pada build layar membuat seluruh layar ikut dibangun ulang
// tidak-dijalankan
class LayarPiket extends StatelessWidget {
  const LayarPiket({super.key});

  @override
  Widget build(BuildContext context) {
    final state = context.watch<StatePiket>();

    return Scaffold(
      appBar: AppBar(title: Text('Selesai: ${state.jumlahSelesai}')),
      body: ListView(
        children: [
          for (final tugas in state.tugas) KartuPiket(tugas: tugas),
        ],
      ),
    );
  }
}

Setiap notifyListeners() dari StatePiket memicu build ulang Scaffold, AppBar, dan seluruh daftar kartu. Apabila satu nama piket diedit tanpa mengubah status selesai, judul yang menampilkan jumlah selesai sebenarnya tidak berubah isinya, tetapi Text tersebut tetap dibangun ulang karena langganan watch melekat pada seluruh build. Pada daftar yang pendek, dampaknya tidak terasa, sedangkan pada layar yang memuat puluhan kartu dengan tampilan yang berat, sebagian besar kerja build terbuang untuk widget yang tidak perlu diperbarui.

Consumer#

Consumer<T> adalah widget pembungkus yang mengambil nilai T dari penyedia di atasnya tanpa membuat seluruh build widget luarnya ikut berlangganan. Nilainya diterima oleh parameter builder, sehingga hanya builder yang dijalankan ulang setiap kali notifyListeners() dipanggil. Widget lain pada build yang sama, termasuk kerangka Scaffold di sekelilingnya, tidak tersentuh.

dart Consumer membungkus hanya bagian yang membutuhkan nilai
// tidak-dijalankan
class LayarPiket extends StatelessWidget {
  const LayarPiket({super.key});

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: Consumer<StatePiket>(
          builder: (context, state, child) {
            return Text('Selesai: ${state.jumlahSelesai}');
          },
        ),
      ),
      body: Consumer<StatePiket>(
        builder: (context, state, child) {
          return ListView(
            children: [
              for (final tugas in state.tugas) KartuPiket(tugas: tugas),
            ],
          );
        },
      ),
    );
  }
}

Perubahan state kini hanya menjalankan ulang dua builder, yaitu judul yang menampilkan jumlah selesai dan daftar kartu yang memang harus mengikuti isi state. Kerangka Scaffold, AppBar, dan widget statis lainnya tidak tersentuh, sedangkan pada versi watch ketiganya ikut dibangun ulang setiap kali notifyListeners() dipanggil. Pekerjaan utama saat menyempurnakan cakupan rebuild adalah memindahkan langganan dari titik yang terlalu luas ke titik yang tepat.

Parameter builder memuat satu parameter lagi bernama child, yaitu widget yang diteruskan utuh tanpa dibangun ulang. Subpohon yang tidak bergantung pada state diletakkan pada parameter tersebut, lalu dirujuk di dalam builder, sehingga satu-satunya bagian yang dibangun ulang benar-benar hanya hasil builder.

Selector#

Selector<S, T> mempersempit langganan pada potongan data tertentu. Callback selector memetakan state S menjadi nilai turunan T, dan build ulang hanya terjadi apabila nilai turunan tersebut berubah menurut operator ==. Perbandingan dijalankan setiap kali notifyListeners() dipanggil, sehingga perubahan state yang tidak menyentuh potongan data yang dipantau tidak memicu apa pun.

dart Selector berlangganan jumlah selesai saja
// tidak-dijalankan
Selector<StatePiket, int>(
  selector: (context, state) => state.jumlahSelesai,
  builder: (context, jumlahSelesai, child) {
    return Text('Selesai: $jumlahSelesai');
  },
)

Apabila nama sebuah piket diedit, jumlahSelesai tetap bernilai sama, perbandingan == menghasilkan true, dan judul tidak dibangun ulang. Sebaliknya, saat satu piket ditandai selesai, nilainya berubah dan builder dijalankan. Kelas state pendukung cukup memuat getter turunan berikut.

dart Getter turunan pada kelas state
// tidak-dijalankan
class StatePiket extends ChangeNotifier {
  final _tugas = <Tugas>[];

  List<Tugas> get tugas => List.unmodifiable(_tugas);

  int get jumlahSelesai => _tugas.where((t) => t.selesai).length;
}

Ketepatan Selector bergantung pada keandalan perbandingan ==. Nilai turunan berupa int atau String mudah dibandingkan, sedangkan objek model dibandingkan berdasarkan identitas secara bawaan. Model tak berubah yang selalu menghasilkan objek baru untuk setiap perubahan membuat perbandingan itu akurat, karena objek lama tidak pernah diubah di tempat. Sisi tersebut diulas secara utuh pada pola immutability.

Memilih antara watch, Consumer, dan Selector#

Ketiganya menggunakan mekanisme langganan yang sama, dan pilihannya ditentukan oleh cakupan, bukan kemampuan. Tabel berikut membandingkan cara nilai diperoleh, cakupan build ulang, dan tempat yang cocok untuk masing-masing.

PendekatanNilai diperolehCakupan build ulangCocok untuk
context.watchlangsung pada buildseluruh build widget pemanggillayar sederhana yang hampir seluruhnya bergantung pada state
Consumerbuilder di dalam pohonhanya builder yang dipasangsubpohon kecil yang membutuhkan nilai di antara widget lain
Selectorbuilder dari nilai turunanbuilder, dan hanya apabila nilai turunan berubahbagian yang bergantung pada sebagian data state

Urutannya dapat dibaca dari yang paling mudah ditulis hingga yang paling terkendali. watch menyelesaikan layar kecil tanpa widget tambahan, Consumer memindahkan langganan ke subpohon tertentu, dan Selector menambah syarat perubahan pada potongan data. Dalam satu layar, ketiganya lazim digunakan bersamaan sesuai kebutuhan setiap bagiannya.

Kesalahan umum#

  • Menggunakan watch pada build layar besar tanpa kebutuhan. Seluruh isi layar ikut dibangun ulang setiap perubahan state. Pindahkan langganan dengan Consumer atau Selector ke bagian yang benar-benar menampilkan nilai tersebut.
  • Callback selector yang membuat objek baru pada setiap pemanggilan. Callback selector yang mengembalikan list literal atau hasil pemetaan baru selalu menghasilkan objek berbeda, sehingga == menghasilkan false dan build ulang terjadi pada setiap notifyListeners(). Kembalikan field atau getter yang sama, atau nilai primitif seperti int.
  • Memasang satu Consumer di atas MaterialApp untuk seluruh aplikasi. Satu pembungkus pada posisi itu menjadikan seluruh pohon di bawahnya dibangun ulang pada setiap perubahan, sehingga tujuan mempersempit cakupan rebuild tidak tercapai. Pasang widget ini sedekat mungkin pada widget yang membutuhkan nilai tersebut.

Ringkasan#

  • Consumer memberikan nilai state kepada builder tertentu, sehingga build ulang tidak menjalar ke seluruh build widget luarnya.
  • Parameter child pada builder meneruskan subpohon tanpa dibangun ulang, dan hanya hasil builder yang diperbarui.
  • Selector berlangganan nilai turunan dari callback selector dan membangun ulang hanya apabila nilai tersebut berubah menurut ==.
  • watch, Consumer, dan Selector menggunakan mekanisme yang sama, dan pilihan di antaranya menentukan luas cakupan rebuild.

Referensi#