Provider #

Pustaka Provider adalah solusi manajemen status (state management) yang secara historis pernah menjadi rekomendasi resmi utama dari tim pengembang Flutter. Pada dasarnya, Provider bertindak sebagai pembungkus cerdas (wrapper) di atas InheritedWidget bawaan Flutter. Ia dirancang untuk menyederhanakan kode boilerplate yang rumit, menyuntikkan ketergantungan secara dinamis (dependency injection), serta menyediakan fitur pengelolaan siklus hidup objek yang sangat rapi seperti inisialisasi lambat (lazy initialization) dan penghancuran objek otomatis (automatic disposal). Meskipun saat ini Riverpod sering kali lebih direkomendasikan untuk proyek baru, menguasai Provider sangatlah penting karena ia masih sangat mendominasi jutaan proyek produksi aktif dan paket pustaka di ekosistem Flutter global.


Instalasi #

Untuk menggunakan Provider, tambahkan dependensi berikut ke dalam berkas pubspec.yaml proyek kita:

dependencies:
  flutter:
    sdk: flutter
  provider: ^6.1.2

ChangeNotifierProvider — Provider Paling Umum #

ChangeNotifierProvider adalah jenis provider yang paling sering digunakan dalam pengembangan aplikasi Flutter. Ia mengawinkan kegunaan ChangeNotifier sebagai pemancar data reaktif dengan InheritedWidget sebagai pipa penyalur data vertikal ke bawah pohon widget.

1. Membuat Kelas Model reaktif #

Kita membuat model bisnis logik kita dengan mewarisi kelas ChangeNotifier.

import 'package:flutter/foundation.dart';

class ItemKeranjang {
  final String id;
  final String nama;
  final double harga;
  final int jumlah;

  const ItemKeranjang({
    required this.id,
    required this.nama,
    required this.harga,
    required this.jumlah,
  });

  double get subtotal => harga * jumlah;

  ItemKeranjang copyWith({int? jumlah}) {
    return ItemKeranjang(
      id: id,
      nama: nama,
      harga: harga,
      jumlah: jumlah ?? this.jumlah,
    );
  }
}

class KeranjangModel extends ChangeNotifier {
  final List<ItemKeranjang> _items = [];

  List<ItemKeranjang> get items => List.unmodifiable(_items);
  
  int get jumlahItem => _items.fold(0, (sum, item) => sum + item.jumlah);
  
  double get totalBelanja => _items.fold(0.0, (sum, item) => sum + item.subtotal);

  void tambahProduk(String id, String nama, double harga) {
    final indeks = _items.indexWhere((item) => item.id == id);
    if (indeks >= 0) {
      _items[indeks] = _items[indeks].copyWith(jumlah: _items[indeks].jumlah + 1);
    } else {
      _items.add(ItemKeranjang(id: id, nama: nama, harga: harga, jumlah: 1));
    }
    notifyListeners(); // Memicu pembaruan UI
  }

  void hapusProduk(String id) {
    _items.removeWhere((item) => item.id == id);
    notifyListeners();
  }

  void reset() {
    _items.clear();
    notifyListeners();
  }
}

2. Mendaftarkan Provider di Pohon Widget #

Agar status data dari KeranjangModel dapat diakses oleh widget-widget di bawahnya, kita wajib membungkus entry point aplikasi kita (atau subtree tertentu) menggunakan ChangeNotifierProvider.

void main() {
  runApp(
    // Meletakkan ChangeNotifierProvider di atas MaterialApp agar dapat diakses dari halaman manapun
    ChangeNotifierProvider(
      create: (BuildContext context) => KeranjangModel(),
      lazy: true, // Secara default bernilai true (hanya diinisialisasi saat diakses pertama kali)
      child: const MyApp(),
    ),
  );
}

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

  @override
  Widget build(BuildContext context) {
    return const MaterialApp(
      home: LayarUtama(),
    );
  }
}
Manajemen Memori Otomatis: Kelebihan utama dari ChangeNotifierProvider adalah ia akan secara otomatis memanggil metode dispose() pada objek KeranjangModel ketika widget provider tersebut dilepas secara permanen dari pohon widget, sehingga kita bebas dari risiko kebocoran memori.

Mengakses Provider — Tiga Metode Ekstensi #

Pustaka Provider menyediakan tiga metode ekstensi praktis pada BuildContext untuk berinteraksi dengan status data yang terdaftar di pohon widget:

flowchart TD
    Start["Butuh Akses Provider?"] --> Where{"Di mana lokasi akses?"}
    Where -->|Di dalam build method| RebuildNeeded{"Apakah widget perlu\nrebuild saat state berubah?"}
    RebuildNeeded -->|Ya| PartialNeeded{"Apakah hanya butuh\nsebagian properti state?"}
    PartialNeeded -->|Ya| UseSelect["Gunakan context.select()"]
    PartialNeeded -->|Tidak| UseWatch["Gunakan context.watch()"]
    RebuildNeeded -->|Tidak| UseRead["Gunakan context.read()"]
    Where -->|Di luar build / callback| UseReadCallback["Gunakan context.read()"]

1. context.watch<T>() (Berlangganan Penuh) #

Metode context.watch<T>() bertugas membaca data status bertipe T saat ini sekaligus mendaftarkan widget pemanggil sebagai dependen. Setiap kali metode notifyListeners() pada objek T dipanggil, widget yang menggunakan context.watch akan di-build ulang secara menyeluruh.

  • Kapan Digunakan: Di dalam metode build() pada widget yang bertugas menampilkan informasi dinamis secara langsung ke layar.
class IconKeranjangBelanja extends StatelessWidget {
  const IconKeranjangBelanja({super.key});

  @override
  Widget build(BuildContext context) {
    // Widget ini akan di-rebuild secara otomatis setiap kali ada item baru di keranjang
    final keranjang = context.watch<KeranjangModel>();
    
    return Badge(
      label: Text('${keranjang.jumlahItem}'),
      child: const Icon(Icons.shopping_cart),
    );
  }
}

2. context.read<T>() (Hanya Membaca Tanpa Rebuild) #

Metode context.read<T>() bertugas membaca objek T saat ini tanpa mendaftarkan widget sebagai pendengar aktif. Dengan kata lain, pemanggilan metode ini tidak akan pernah memicu rekonstruksi pada widget pemanggil saat status data mengalami pembaruan.

  • Kapan Digunakan: Di dalam fungsi-fungsi callback interaksi pengguna seperti tombol onPressed, kolom input onChanged, atau di dalam inisialisasi awal initState.
class TombolTambahKeranjang extends StatelessWidget {
  final String idProduk;
  final String namaProduk;
  final double hargaProduk;

  const TombolTambahKeranjang({
    super.key,
    required this.idProduk,
    required this.namaProduk,
    required this.hargaProduk,
  });

  @override
  Widget build(BuildContext context) {
    return ElevatedButton(
      onPressed: () {
        // Memicu aksi bisnis logik tanpa membuat tombol ini di-rebuild secara sia-sia
        context.read<KeranjangModel>().tambahProduk(idProduk, namaProduk, hargaProduk);
      },
      child: const Text('Tambah ke Keranjang'),
    );
  }
}

3. context.select<T, R>() (Berlangganan Terarah) #

Metode context.select<T, R>() memungkinkan kita hanya memantau properti spesifik berjenis R dari objek status T. Widget pemanggil hanya akan di-rebuild ketika properti spesifik tersebut berubah nilai, mengabaikan perubahan pada properti lain di dalam model yang sama.

  • Kapan Digunakan: Pada komponen UI mikro yang hanya membutuhkan sepotong kecil informasi dari model yang besar demi optimasi performa render yang ketat.
class TotalTagihanWidget extends StatelessWidget {
  const TotalTagihanWidget({super.key});

  @override
  Widget build(BuildContext context) {
    // Hanya di-rebuild jika totalBelanja berubah. 
    // Perubahan pada daftar item tidak akan mempengaruhi widget ini jika nilainya tetap sama.
    final total = context.select<KeranjangModel, double>(
      (keranjang) => keranjang.totalBelanja,
    );
    
    return Text(
      'Total: Rp $total',
      style: const TextStyle(fontSize: 20.0, fontWeight: FontWeight.bold),
    );
  }
}

Consumer — Widget Builder Alternatif #

Widget Consumer<T> bertugas sebagai alternatif penulisan reaktif selain menggunakan context.watch<T>(). Keunggulan utama Consumer adalah kemampuannya membatasi cakupan pembaruan UI secara lebih presisi melalui penggunaan parameter child statis yang di-cache.

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

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      body: Consumer<KeranjangModel>(
        // child statis dideklarasikan di luar builder agar terbebas dari siklus rebuild
        child: const Padding(
          padding: EdgeInsets.all(16.0),
          child: Text('Ringkasan Belanjaan Anda:'),
        ),
        builder: (BuildContext context, KeranjangModel keranjang, Widget? staticChild) {
          return Column(
            children: [
              staticChild!, // Menggunakan kembali widget statis yang sudah di-cache
              Text('Item Aktif: ${keranjang.jumlahItem}'),
              Text('Total: Rp ${keranjang.totalBelanja}'),
            ],
          );
        },
      ),
    );
  }
}

Kapan Memilih context.watch vs Consumer? #

  • Gunakan context.watch<T>(): Saat kode build kita relatif pendek dan hampir seluruh isi widget di dalam fungsi build tersebut memang bergantung pada perubahan status data. Cara ini menghasilkan struktur kode yang lebih bersih dan ringkas.
  • Gunakan Consumer<T>: Saat kita memiliki widget dengan build method yang sangat panjang dan memiliki banyak elemen visual statis di dalamnya. Dengan menempatkan Consumer di tingkat terdalam, kita mengisolasi siklus rebuild agar tidak mempengaruhi widget tetangga.

MultiProvider — Menghindari Bersarangnya Kode #

Ketika aplikasi kita tumbuh dewasa, kita pasti akan mengelola banyak model status sekaligus. Menumpuk widget Provider satu per satu secara manual akan memicu masalah keterbacaan kode (nesting hell). Untuk mengatasinya, kita menggunakan MultiProvider.

// HINDARI: Pola bersarang yang menyulitkan pembacaan kode
void main() {
  runApp(
    ChangeNotifierProvider(
      create: (_) => AuthModel(),
      child: ChangeNotifierProvider(
        create: (_) => TemaModel(),
        child: MaterialApp(home: const LayarUtama()),
      ),
    ),
  );
}

// GUNAKAN: MultiProvider yang rapi dan terstruktur
void main() {
  runApp(
    MultiProvider(
      providers: [
        ChangeNotifierProvider(create: (context) => AuthModel()),
        ChangeNotifierProvider(create: (context) => TemaModel()),
        ChangeNotifierProvider(create: (context) => KeranjangModel()),
      ],
      child: const MyApp(),
    ),
  );
}

ProxyProvider — Mengelola Ketergantungan Antar-Provider #

Di aplikasi dunia nyata, sering kali sebuah modul status membutuhkan informasi dari modul status lainnya. Sebagai contoh, sebuah layanan data API (ApiService) membutuhkan token otentikasi dari model otentikasi pengguna (AuthModel) agar dapat melakukan pemanggilan data yang sah. Untuk skenario ketergantungan ini, kita menggunakan ProxyProvider.

flowchart LR
    Auth["AuthModel (Mengelola Token)"] -->|Di-inject ke| Api["ProxyProvider: ApiService"]
    Api -->|Di-inject ke| Repo["ProxyProvider: ProdukRepository"]
    Repo -->|Di-inject ke| Model["ChangeNotifierProxyProvider: ProdukModel"]

Berikut adalah contoh implementasi rantai ketergantungan menggunakan ProxyProvider:

MultiProvider(
  providers: [
    // 1. Definisikan Source of Truth utama
    ChangeNotifierProvider(create: (_) => AuthModel()),

    // 2. Gunakan ProxyProvider untuk menyuntikkan token dari AuthModel ke ApiService
    ProxyProvider<AuthModel, ApiService>(
      update: (BuildContext context, AuthModel auth, ApiService? previousApi) {
        return ApiService(token: auth.token);
      },
    ),

    // 3. Gunakan ChangeNotifierProxyProvider untuk model reaktif yang butuh dependensi
    ChangeNotifierProxyProvider<ApiService, DashboardModel>(
      create: (BuildContext context) => DashboardModel(),
      update: (BuildContext context, ApiService api, DashboardModel? previousDashboard) {
        return previousDashboard!..perbaruiKetergantungan(api);
      },
    ),
  ],
  child: const MyApp(),
)

FutureProvider dan StreamProvider — Data Asinkron Mudah #

Pustaka Provider juga menyediakan dua tipe khusus untuk menyalurkan data asinkron langsung ke dalam widget tree tanpa perlu menulis widget FutureBuilder atau StreamBuilder secara berulang:

1. FutureProvider #

Sangat berguna untuk operasi pemuatan data satu kali yang memakan waktu (seperti memuat data konfigurasi dari penyimpanan lokal).

FutureProvider<List<String>?>(
  initialData: null,
  create: (BuildContext context) => LayananKonfigurasi.ambilDaftarFitur(),
  child: const LayarDaftarFitur(),
)

2. StreamProvider #

Sangat ideal untuk mendengarkan aliran data real-time, seperti konektivitas internet atau sinkronisasi database Firebase secara berkelanjutan.

StreamProvider<StatusKoneksi>(
  initialData: StatusKoneksi.menghubungkan,
  create: (BuildContext context) => PemantauKoneksi.streamStatus,
  child: const WidgetIndikatorJaringan(),
)

Struktur Folder yang Direkomendasikan #

Untuk menjaga ketertiban arsitektur tim, disarankan bagi kita untuk mengatur struktur folder manajemen status dengan rapi sebagai berikut:

lib/
  ├── main.dart
  ├── app_entry.dart
  ├── state/                  # Folder pusat pengelolaan status data
  │   ├── auth_notifier.dart
  │   ├── theme_notifier.dart
  │   └── cart_notifier.dart
  ├── services/               # Folder layanan eksternal (API, DB)
  │   ├── api_service.dart
  │   └── local_storage.dart
  ├── models/                 # Folder data model murni (plain Dart)
  │   ├── user.dart
  │   └── product.dart
  └── ui/                     # Folder antarmuka visual
      ├── screens/
      └── widgets/

Contoh Lengkap — Implementasi Halaman Otentikasi #

Mari kita tinjau contoh implementasi lengkap sistem otentikasi login menggunakan Provider:

// state/auth_notifier.dart
class PenggunaInfo {
  final String nama;
  final String token;
  const PenggunaInfo({required this.nama, required this.token});
}

class AuthNotifier extends ChangeNotifier {
  PenggunaInfo? _user;
  bool _isLoading = false;
  String? _pesanError;

  PenggunaInfo? get user => _user;
  bool get isLoggedIn => _user != null;
  bool get isLoading => _isLoading;
  String? get pesanError => _pesanError;

  Future<void> login(String email, String sandi) async {
    _isLoading = true;
    _pesanError = null;
    notifyListeners();

    try {
      // Simulasi panggilan server API
      await Future.delayed(const Duration(seconds: 2));
      
      if (email == '[email protected]' && sandi == 'rahasia123') {
        _user = const PenggunaInfo(nama: 'Ahmad Gani', token: 'token-jwt-abc');
      } else {
        throw Exception('Email atau kata sandi salah!');
      }
    } catch (e) {
      _pesanError = e.toString().replaceAll('Exception: ', '');
    } finally {
      _isLoading = false;
      notifyListeners();
    }
  }

  void logout() {
    _user = null;
    notifyListeners();
  }
}

// ui/screens/login_screen.dart
class LayarLogin extends StatefulWidget {
  const LayarLogin({super.key});

  @override
  State<LayarLogin> createState() => _LayarLoginState();
}

class _LayarLoginState extends State<LayarLogin> {
  final TextEditingController _emailController = TextEditingController();
  final TextEditingController _sandiController = TextEditingController();

  @override
  void dispose() {
    _emailController.dispose();
    _sandiController.dispose();
    super.dispose();
  }

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

    return Scaffold(
      appBar: AppBar(title: const Text('Login Portal')),
      body: Padding(
        padding: const EdgeInsets.all(24.0),
        child: Column(
          mainAxisAlignment: MainAxisAlignment.center,
          children: [
            TextField(
              controller: _emailController,
              decoration: const InputDecoration(labelText: 'Alamat Email'),
            ),
            const SizedBox(height: 12.0),
            TextField(
              controller: _sandiController,
              obscureText: true,
              decoration: const InputDecoration(labelText: 'Kata Sandi'),
            ),
            const SizedBox(height: 24.0),
            if (auth.pesanError != null)
              Text(
                auth.pesanError!,
                style: const TextStyle(color: Colors.red),
              ),
            const SizedBox(height: 12.0),
            auth.isLoading
                ? const CircularProgressIndicator()
                : ElevatedButton(
                    onPressed: () {
                      context.read<AuthNotifier>().login(
                            _emailController.text,
                            _sandiController.text,
                          );
                    },
                    child: const Text('Login'),
                  ),
          ],
        ),
      ),
    );
  }
}

Keterbatasan Provider #

Walaupun Provider sangat populer dan mudah digunakan, kita harus memahami beberapa keterbatasan bawaannya yang kemudian melahirkan pustaka Riverpod sebagai penyempurna:

  1. Ketergantungan Mutlak pada BuildContext: Kita tidak bisa membaca atau memodifikasi status data di luar widget tree (misalnya di dalam file layanan independen) tanpa melewatkan BuildContext secara paksa.
  2. Ketiadaan Keamanan Waktu Kompilasi (No Compile-time Safety): Jika kita memanggil context.watch<LayananA>() namun kita lupa mendaftarkan LayananA di atas pohon widget, aplikasi kita akan mengalami crash secara tiba-tiba di runtime dengan pesan error ProviderNotFoundException.
  3. Kesulitan Instansiasi Ganda: Sangat rumit bagi kita untuk membuat beberapa instance terpisah dari satu tipe provider yang sama di dalam widget tree yang sama.

Pustaka Riverpod dirancang secara khusus untuk menjawab dan menghapus seluruh batasan teknis di atas.

Ringkasan #

  • Provider menyederhanakan penggunaan InheritedWidget dengan otomatisasi proses penutupan sumber daya (disposal) dan pengurangan boilerplate.
  • Tiga Metode Akses: Gunakan context.watch<T>() untuk rebuild di build method, context.read<T>() untuk interaksi non-reaktif di dalam callback, dan context.select<T, R>() untuk membatasi rebuild hanya pada properti spesifik.
  • Consumer Widget: Memungkinkan penyisipan cache child statis demi optimalisasi performa render antarmuka.
  • ProxyProvider: Solusi rantai dependensi antar model status (dependency injection) yang rapi.
  • Rencana Skalabilitas: Untuk aplikasi berskala menengah ke atas dengan kebutuhan penanganan error asinkron yang ketat dan pengujian unit terisolasi tanpa framework UI, pertimbangkan untuk beralih menggunakan Riverpod.

← Sebelumnya: setState & ValueNotifier   Berikutnya: Riverpod →

About | Author | Content Scope | Editorial Policy | Privacy Policy | Disclaimer | Contact