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
InheritedWidgetyang menyederhanakan state management — ia otomatis disposeChangeNotifierdan menangani lifecycle.- Gunakan
context.watch<T>()untuk subscribe dan rebuild dibuild(). Gunakancontext.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.Consumerberguna saat kamu butuh parameterchilduntuk bagian UI statis yang tidak perlu rebuild.MultiProvidermengelola banyak provider sekaligus di root aplikasi — hindari nesting provider manual.ProxyProvidermembuat provider yang bergantung pada provider lain — berguna untuk dependency chain sepertiAuth → ApiService → Repository → Model.FutureProviderdanStreamProvidermemudahkan 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 →