Package Provider

Mengelola state aplikasi dengan package Provider melalui ChangeNotifier serta context.read dan context.watch.

Daftar isi

Topik ini memperkenalkan package Provider, yaitu package pengelolaan state yang direkomendasikan sebagai titik awal untuk state tingkat aplikasi di Flutter. Pada topik sebelumnya, InheritedWidget sudah dikenal sebagai mekanisme dasar berbagi data lintas subpohon, tetapi penggunaannya masih menuntut kode yang rinci dan state yang berubah belum memiliki tempat penyimpanan yang tepat. Di sini state dipindahkan ke kelas tersendiri yang diturunkan dari ChangeNotifier, didaftarkan pada pohon widget melalui ChangeNotifierProvider, lalu dibaca dari widget mana pun melalui context.watch dan context.read. Setelah topik ini dipraktikkan, state lintas layar tidak lagi dilewatkan melalui rangkaian konstruktor.

Dari InheritedWidget ke package Provider#

Package Provider dibangun di atas mekanisme InheritedWidget, sehingga cara kerja pendaftaran dependen yang sudah dipelajari tetap berlaku. Package ini menambahkan dua hal yang sebelumnya harus ditulis manual. Pertama, widget penyedia siap pakai untuk setiap jenis data, sehingga subclass InheritedWidget dan method of tidak perlu ditulis lagi. Kedua, pengelolaan siklus hidup objek state, yaitu objek state dibuat ketika pertama kali dibutuhkan dan dibuang secara otomatis ketika penyedianya lepas dari pohon widget.

Pemasangan dilakukan dengan menambahkan package ke pubspec.yaml, lalu menjalankan flutter pub get. Perintah flutter pub add provider melakukan keduanya sekaligus. Baris yang diperlukan pada pubspec.yaml berupa nama package, rentang versi Dart, dan dependensi provider berikut.

yaml pubspec.yaml
name: daftar_belanja
description: Aplikasi daftar belanja pribadi dengan package Provider.
environment:
  sdk: ^3.13.0

dependencies:
  flutter:
    sdk: flutter
  provider: ^6.1.5

flutter:
  uses-material-design: true
bash
# tidak-dijalankan
flutter pub add provider

Kelas state dengan ChangeNotifier#

ChangeNotifier adalah kelas bawaan Flutter yang menyediakan mekanisme pemberitahuan bagi pendengar (listener), yaitu objek yang mendaftar agar diberi tahu setiap kali state berubah. Kelas state aplikasi diturunkan dari ChangeNotifier, lalu setiap method yang mengubah state diakhiri pemanggilan notifyListeners(). Pemanggilan tersebut memicu rebuild seluruh widget yang berlangganan.

Contoh berikut memindahkan state daftar belanja ke kelas DaftarBelanja. Daftar disimpan pada field privat, sedangkan getter mengembalikan salinan yang tidak dapat diubah agar bagian luar tidak dapat menyunting isinya tanpa melewati method kelas ini.

dart Kelas state DaftarBelanja extends ChangeNotifier
// tidak-dijalankan
class DaftarBelanja extends ChangeNotifier {
  final _barang = <String>[];

  List<String> get barang => List.unmodifiable(_barang);

  void tambah(String nama) {
    if (nama.isEmpty) {
      return;
    }
    _barang.add(nama);
    notifyListeners();
  }

  void hapus(String nama) {
    _barang.remove(nama);
    notifyListeners();
  }
}

Pada kelas di atas, kedua method yang mengubah isi daftar diakhiri notifyListeners(), sedangkan pembacaan melalui getter tidak memicu apa pun. Pola ini menjaga satu aturan sederhana, yaitu perubahan state hanya boleh terjadi melalui method kelas tersebut.

Mendaftarkan state pada pohon widget#

ChangeNotifierProvider menggabungkan kedua bagian tersebut dalam satu widget, yaitu menyimpan objek ChangeNotifier di dalam mekanisme InheritedWidget dan mengelola siklus hidupnya. Parameter create berisi fungsi yang membuat objek state, dan fungsi itu baru dipanggil ketika state pertama kali dibutuhkan. Ketika penyedia lepas dari pohon widget, objek state yang dibuatnya ikut dibuang secara otomatis melalui dispose, sehingga tidak ada pembersihan manual.

Penyedia dipasang di atas seluruh widget yang menggunakannya. State yang digunakan lintas layar biasanya diletakkan di atas MaterialApp.

dart ChangeNotifierProvider dipasang di atas MaterialApp
// tidak-dijalankan
void main() {
  runApp(
    ChangeNotifierProvider(
      create: (_) => DaftarBelanja(),
      child: const AplikasiBelanja(),
    ),
  );
}

Setelah penyedia terpasang, seluruh widget di bawahnya dapat mengakses DaftarBelanja dari BuildContext, tanpa penerusan konstruktor dan tanpa prop drilling. Cakupannya mengikuti posisi penyedia pada pohon, sehingga state yang digunakan satu layar saja cukup diletakkan lebih rendah dari MaterialApp.

context.watch dan context.read#

Ekstensi context dari package Provider menyediakan dua cara mengakses state untuk keperluan yang berbeda. context.watch<T>() mengambil nilai T sekaligus mendaftarkan widget pemanggil sebagai dependen dari penyedianya, sehingga setiap notifyListeners() memicu build ulang widget tersebut. context.read<T>() hanya mengambil nilai saat itu tanpa mendaftarkan apa pun, sehingga widget pemanggil tidak ikut dibangun ulang ketika state berubah.

Aturan penggunaannya mengikuti tempat pemanggilan. watch dipanggil di dalam build, karena di sanalah widget menyusun tampilan dari state dan wajib berlangganan pembaruannya. read dipanggil di dalam callback seperti onPressed, karena aksesnya hanya terjadi sekali dan tidak ada tampilan yang bergantung padanya.

dart watch dipanggil pada build, read dipanggil di dalam callback
// tidak-dijalankan
class LayarBelanja extends StatelessWidget {
  const LayarBelanja({super.key});

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

    return Scaffold(
      appBar: AppBar(title: Text('Belanja (${barang.length})')),
      body: ListView(
        children: [
          for (final nama in barang) ListTile(title: Text(nama)),
        ],
      ),
      floatingActionButton: FloatingActionButton(
        onPressed: () {
          context.read<DaftarBelanja>().tambah('Gula');
        },
        child: const Icon(Icons.add),
      ),
    );
  }
}

Pada widget LayarBelanja di atas, watch membuat AppBar dan daftar selalu menampilkan isi terbaru, sedangkan read di dalam tombol hanya perlu memanggil tambah() tanpa berlangganan. Perubahan state tetap sampai ke tampilan karena tambah() memanggil notifyListeners(), lalu widget yang berlangganan membangun ulang dirinya sendiri.

MultiProvider#

Aplikasi nyata biasanya menyimpan lebih dari satu kelas state, misalnya daftar belanja dan mode tampilan. Tanpa alat bantu, penyedia-penyedia tersebut disusun bertingkat, satu di dalam yang lain, sehingga susunannya cepat sulit dibaca. MultiProvider menyusun beberapa penyedia secara berurutan dan menghasilkan susunan pohon yang sama.

dart MultiProvider untuk lebih dari satu kelas state
// tidak-dijalankan
void main() {
  runApp(
    MultiProvider(
      providers: [
        ChangeNotifierProvider(create: (_) => DaftarBelanja()),
        ChangeNotifierProvider(create: (_) => ModeTampilan()),
      ],
      child: const AplikasiBelanja(),
    ),
  );
}

Setiap entri pada providers bekerja persis seperti penyedia tunggal, sehingga keputusan berbagi state tetap terkendali per kelas. Urutan penulisan menentukan susunan pohon, karena entri yang lebih dahulu menjadi induk bagi entri di bawahnya.

Kesalahan umum#

  • Menampilkan state dengan context.read. Nilainya hanya benar pada saat itu dan tidak diperbarui ketika state berubah, sehingga tampilan tampak tertinggal. Gunakan context.watch di dalam build untuk setiap nilai yang ditampilkan.
  • Lupa memanggil notifyListeners() setelah mengubah state. Tidak ada pemberitahuan yang dikirim, sehingga widget yang berlangganan tetap menampilkan nilai lama. Pastikan setiap method yang mengubah state diakhiri pemanggilan tersebut.
  • Memaparkan koleksi state secara langsung. Getter yang mengembalikan list internal memungkinkan bagian lain aplikasi memanggil add() tanpa pemberitahuan, sehingga state dan tampilan tidak sinkron. Kembalikan List.unmodifiable dan sediakan method khusus untuk perubahan.

Ringkasan#

  • Kelas state diturunkan dari ChangeNotifier, dan setiap perubahan state diakhiri notifyListeners().
  • ChangeNotifierProvider mendaftarkan objek state pada pohon widget, membuatnya saat pertama kali dibutuhkan, dan membuangnya secara otomatis.
  • context.watch berlangganan pembaruan dan digunakan di dalam build, sedangkan context.read mengambil nilai saat dipanggil tanpa berlangganan.
  • MultiProvider menyusun beberapa kelas state sekaligus tanpa penyusunan bertingkat yang sulit dibaca.

Referensi#