Provider #

Provider adalah package state management yang paling lama direkomendasikan secara resmi oleh tim Flutter. Ia adalah wrapper cerdas di atas InheritedWidget yang menyederhanakan boilerplate dan menambahkan fitur seperti lazy initialization dan automatic disposal. Meskipun Riverpod kini lebih direkomendasikan untuk proyek baru, Provider masih sangat relevan dan digunakan luas di ekosistem Flutter.

Instalasi #

# pubspec.yaml
dependencies:
  provider: ^6.1.2

ChangeNotifierProvider — Provider Paling Umum #

ChangeNotifierProvider menggabungkan ChangeNotifier dengan sistem InheritedWidget — ia menyimpan instance ChangeNotifier dan me-rebuild dependents ketika notifyListeners() dipanggil.

Buat Model #

import 'package:flutter/foundation.dart';

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

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

  void tambah(Produk produk) {
    final idx = _items.indexWhere((i) => i.produk.id == produk.id);
    if (idx >= 0) {
      _items[idx] = _items[idx].copyWith(jumlah: _items[idx].jumlah + 1);
    } else {
      _items.add(CartItem(produk: produk, jumlah: 1));
    }
    notifyListeners();
  }

  void hapus(String produkId) {
    _items.removeWhere((i) => i.produk.id == produkId);
    notifyListeners();
  }

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

Daftarkan di Widget Tree #

void main() {
  runApp(
    // Letakkan di atas MaterialApp agar tersedia di seluruh app
    ChangeNotifierProvider(
      create: (_) => KeranjangModel(),
      child: const MyApp(),
    ),
  );
}

Provider otomatis memanggil dispose() pada KeranjangModel ketika provider dihapus dari tree — tidak perlu dispose manual.


Mengakses Provider — Tiga Cara #

1. context.watch<T>() — Subscribe dan Rebuild #

Membaca nilai dan mendaftarkan widget untuk rebuild setiap kali nilai berubah. Gunakan di dalam build().

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

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

2. context.read<T>() — Baca Tanpa Subscribe #

Membaca nilai tanpa mendaftarkan widget untuk rebuild. Gunakan di dalam callback (onPressed, onChanged) — bukan di build().

ElevatedButton(
  onPressed: () {
    // context.read di dalam callback -- tidak menyebabkan rebuild
    context.read<KeranjangModel>().tambah(produk);
  },
  child: const Text('Tambah ke Keranjang'),
)

3. context.select<T, R>() — Subscribe Hanya Sebagian #

Subscribe hanya pada sebagian dari model — widget hanya rebuild jika bagian yang di-select berubah. Sangat berguna untuk performa.

class KeranjangBadge extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    // Hanya rebuild saat jumlahItem berubah -- bukan saat total atau items berubah
    final jumlah = context.select<KeranjangModel, int>(
      (keranjang) => keranjang.jumlahItem,
    );
    return Badge(label: Text('$jumlah'));
  }
}

Tabel Perbandingan #

context.watch<T>()    -- subscribe semua perubahan di T
                         gunakan di build() untuk data yang ditampilkan

context.read<T>()     -- tidak subscribe, tidak rebuild
                         gunakan di callback untuk memanggil method

context.select<T,R>() -- subscribe hanya ke properti R di T
                         gunakan saat T punya banyak properti tapi
                         widget hanya butuh satu

Consumer — Widget Builder #

Consumer adalah alternatif widget untuk context.watch — lebih eksplisit dan memungkinkan penggunaan parameter child untuk optimasi:

Consumer<KeranjangModel>(
  // child tidak di-rebuild saat keranjang berubah
  child: const TombolCheckout(),
  builder: (context, keranjang, child) {
    return Column(
      children: [
        Text('${keranjang.jumlahItem} item'),
        Text('Total: Rp ${keranjang.total}'),
        child!,  // di-reuse setiap rebuild
      ],
    );
  },
)

Kapan Consumer vs context.watch? #

context.watch<T>():
  ✓ Lebih ringkas -- hanya satu baris
  ✓ Cocok untuk sebagian besar kasus
  ✗ Seluruh build() akan dipanggil ulang

Consumer<T>():
  ✓ Bisa menggunakan parameter child untuk optimasi
  ✓ Lebih eksplisit tentang mana bagian yang reactive
  ✓ Rebuild dibatasi hanya pada bagian Consumer
  ✓ Cocok saat build() berisi banyak widget statis

MultiProvider — Banyak Provider Sekaligus #

Untuk aplikasi yang punya banyak model/provider:

void main() {
  runApp(
    MultiProvider(
      providers: [
        ChangeNotifierProvider(create: (_) => AuthModel()),
        ChangeNotifierProvider(create: (_) => KeranjangModel()),
        ChangeNotifierProvider(create: (_) => TemaModel()),
        Provider(create: (_) => ApiService()),
        Provider(create: (_) => DatabaseService()),
      ],
      child: const MyApp(),
    ),
  );
}

ProxyProvider — Provider yang Bergantung pada Provider Lain #

ProxyProvider membuat provider yang nilainya bergantung pada provider lain. Ia di-update setiap kali dependency-nya berubah:

MultiProvider(
  providers: [
    // AuthModel tersedia dulu
    ChangeNotifierProvider(create: (_) => AuthModel()),

    // ApiService bergantung pada AuthModel
    ProxyProvider<AuthModel, ApiService>(
      update: (context, auth, previousApi) => ApiService(
        token: auth.token,
        // previousApi bisa digunakan untuk reuse instance jika perlu
      ),
    ),

    // ProdukRepository bergantung pada ApiService
    ProxyProvider<ApiService, ProdukRepository>(
      update: (context, api, _) => ProdukRepository(api: api),
    ),

    // ProdukModel bergantung pada ProdukRepository
    ChangeNotifierProxyProvider<ProdukRepository, ProdukModel>(
      create: (_) => ProdukModel(),
      update: (context, repo, previousModel) {
        previousModel!.updateRepository(repo);
        return previousModel;
      },
    ),
  ],
  child: const MyApp(),
)

FutureProvider dan StreamProvider #

Provider juga menyediakan FutureProvider dan StreamProvider untuk data asinkron:

// FutureProvider -- untuk data yang di-load sekali
FutureProvider<List<Produk>?>(
  initialData: null,
  create: (_) => produkApi.getAllProduk(),
  child: const ProdukListScreen(),
)

// Akses di widget
final produk = context.watch<List<Produk>?>();
if (produk == null) return const CircularProgressIndicator();
return ProdukList(produk: produk);

// StreamProvider -- untuk data real-time
StreamProvider<List<Notifikasi>>(
  initialData: const [],
  create: (_) => notifikasiService.stream,
  child: const NotifikasiScreen(),
)

Struktur Folder yang Direkomendasikan #

lib/
  ├── main.dart
  ├── app.dart
  ├── models/                  -- ChangeNotifier classes
  │   ├── auth_model.dart
  │   ├── keranjang_model.dart
  │   └── tema_model.dart
  ├── services/               -- Logika bisnis / API
  │   ├── auth_service.dart
  │   └── produk_service.dart
  ├── screens/               -- UI screens
  │   ├── home_screen.dart
  │   └── produk_screen.dart
  └── widgets/               -- Reusable widgets
      └── kartu_produk.dart

Contoh Lengkap — Aplikasi Auth #

// models/auth_model.dart
class AuthModel extends ChangeNotifier {
  User? _user;
  bool _isLoading = false;
  String? _error;

  User? get user => _user;
  bool get isLoggedIn => _user != null;
  bool get isLoading => _isLoading;
  String? get error => _error;

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

    try {
      _user = await authService.login(email, password);
    } catch (e) {
      _error = e.toString();
    } finally {
      _isLoading = false;
      notifyListeners();
    }
  }

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

// main.dart
void main() {
  runApp(
    ChangeNotifierProvider(
      create: (_) => AuthModel(),
      child: const MyApp(),
    ),
  );
}

// screens/login_screen.dart
class LoginScreen extends StatelessWidget {
  final _emailController = TextEditingController();
  final _passwordController = TextEditingController();

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      body: Column(
        children: [
          TextField(controller: _emailController),
          TextField(controller: _passwordController, obscureText: true),

          // context.select untuk hanya rebuild bagian yang relevan
          context.select<AuthModel, bool>((auth) => auth.isLoading)
              ? const CircularProgressIndicator()
              : ElevatedButton(
                  onPressed: () {
                    // context.read di dalam callback
                    context.read<AuthModel>().login(
                      _emailController.text,
                      _passwordController.text,
                    );
                  },
                  child: const Text('Login'),
                ),

          // Tampilkan error jika ada
          Consumer<AuthModel>(
            builder: (context, auth, _) {
              if (auth.error == null) return const SizedBox.shrink();
              return Text(auth.error!, style: const TextStyle(color: Colors.red));
            },
          ),
        ],
      ),
    );
  }
}

Keterbatasan Provider #

Provider, meski sangat berguna, memiliki beberapa keterbatasan yang dijawab oleh Riverpod:

Keterbatasan Provider:
  ✗ Bergantung pada BuildContext -- tidak bisa akses state di luar widget
  ✗ Sulit menangani state async (loading/error/data) secara elegan
  ✗ Tidak ada compile-time safety untuk ProviderNotFoundException
  ✗ Tidak mendukung provider yang berexist di luar widget tree
  ✗ Sulit membuat beberapa instance dari provider yang sama

Riverpod mengatasi semua keterbatasan ini.

Ringkasan #

  • Provider adalah wrapper InheritedWidget yang menyederhanakan state management — ia otomatis dispose ChangeNotifier dan menangani lifecycle.
  • Gunakan context.watch<T>() untuk subscribe dan rebuild di build(). Gunakan context.read<T>() untuk memanggil method di callback tanpa subscribe.
  • Gunakan context.select<T, R>() untuk subscribe hanya pada properti tertentu — widget hanya rebuild saat properti tersebut berubah.
  • Consumer berguna saat kamu butuh parameter child untuk bagian UI statis yang tidak perlu rebuild.
  • MultiProvider mengelola banyak provider sekaligus di root aplikasi — hindari nesting provider manual.
  • ProxyProvider membuat provider yang bergantung pada provider lain — berguna untuk dependency chain seperti Auth → ApiService → Repository → Model.
  • FutureProvider dan StreamProvider memudahkan menampilkan state loading/data/error dari operasi async.
  • Provider tepat untuk aplikasi skala kecil hingga menengah. Untuk kebutuhan yang lebih kompleks (async state, testing mudah, scoping), pertimbangkan Riverpod.

← Sebelumnya: setState & ValueNotifier   Berikutnya: Riverpod →

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