InheritedWidget #
InheritedWidget adalah fondasi tersembunyi dari hampir semua fitur Flutter yang sering kamu gunakan sehari-hari. Setiap kali kamu menulis Theme.of(context), MediaQuery.of(context), atau Navigator.of(context), di baliknya ada InheritedWidget yang bekerja. Provider, Riverpod, dan hampir semua state management library Flutter juga dibangun di atas InheritedWidget. Memahaminya adalah memahami bagaimana data mengalir di Flutter.
Masalah yang Diselesaikan InheritedWidget #
Bayangkan kamu punya widget tree yang dalam, dan data di root perlu diakses oleh widget yang jauh di dalam tree:
MaterialApp (punya ThemeData)
└── Scaffold
└── Column
└── Row
└── Card
└── ListTile
└── Text <-- butuh ThemeData!
Tanpa InheritedWidget, kamu harus meneruskan ThemeData melalui setiap constructor — disebut prop drilling:
// TANPA InheritedWidget: prop drilling yang melelahkan
class MyApp extends StatelessWidget {
final ThemeData tema;
const MyApp({required this.tema});
@override
Widget build(BuildContext context) {
return MyScaffold(tema: tema); // terus diteruskan...
}
}
class MyScaffold extends StatelessWidget {
final ThemeData tema;
const MyScaffold({required this.tema});
@override
Widget build(BuildContext context) {
return MyColumn(tema: tema); // ...dan diteruskan...
}
}
// ...dan seterusnya sampai ke widget yang butuh
InheritedWidget menyelesaikan ini dengan menempatkan data di tree dan memungkinkan siapa saja di bawahnya mengaksesnya langsung:
// DENGAN InheritedWidget: akses langsung dari mana saja
class MyText extends StatelessWidget {
@override
Widget build(BuildContext context) {
final tema = Theme.of(context); // langsung! tidak perlu prop drilling
return Text('Halo', style: TextStyle(color: tema.colorScheme.primary));
}
}
Cara Kerja Internal #
Ketika sebuah widget memanggil context.dependOnInheritedWidgetOfExactType<T>(), dua hal terjadi sekaligus. Pertama, framework mencari ancestor InheritedWidget bertipe T terdekat dalam tree. Kedua, widget pemanggil terdaftar sebagai dependent dari InheritedWidget yang ditemukan.
Widget Tree:
MyInheritedWidget (data: "A") <-- InheritedWidget
│
├── WidgetA <-- memanggil MyInheritedWidget.of(context)
│ (terdaftar sebagai dependent) terdaftar! akan di-notify jika data berubah
│
├── WidgetB <-- TIDAK memanggil of()
│ (tidak terdaftar) tidak akan di-notify
│
└── WidgetC <-- memanggil MyInheritedWidget.of(context)
(terdaftar sebagai dependent) terdaftar! akan di-notify jika data berubah
Ketika InheritedWidget diganti dengan data baru dan updateShouldNotify() mengembalikan true, hanya WidgetA dan WidgetC yang di-rebuild — bukan seluruh tree. Ini adalah selective rebuild yang sangat efisien.
Memanggil dependOnInheritedWidgetOfExactType mendaftarkan build context dengan widget yang dikembalikan. Ketika widget tersebut berubah, build context ini di-rebuild agar bisa mendapatkan nilai baru. Metode ini tidak boleh dipanggil dari constructor widget atau dari State.initState karena method tersebut tidak akan dipanggil lagi jika nilai inherited berubah.
Membuat InheritedWidget Kustom #
Struktur Dasar #
class TemaKustom extends InheritedWidget {
// Data yang dibagikan ke seluruh subtree
final Color warnaUtama;
final Color warnaAksen;
final double ukuranFont;
const TemaKustom({
super.key,
required this.warnaUtama,
required this.warnaAksen,
required this.ukuranFont,
required super.child, // child wajib -- ini yang akan di-wrap
});
// Konvensi: static of() untuk non-nullable (assert jika tidak ditemukan)
static TemaKustom of(BuildContext context) {
final result = context.dependOnInheritedWidgetOfExactType<TemaKustom>();
assert(result != null, 'Tidak ada TemaKustom di tree. '
'Pastikan TemaKustom berada di atas widget yang menggunakannya.');
return result!;
}
// Konvensi: static maybeOf() untuk nullable (return null jika tidak ditemukan)
static TemaKustom? maybeOf(BuildContext context) {
return context.dependOnInheritedWidgetOfExactType<TemaKustom>();
}
// Tentukan kapan dependent perlu di-rebuild
@override
bool updateShouldNotify(TemaKustom old) {
return warnaUtama != old.warnaUtama ||
warnaAksen != old.warnaAksen ||
ukuranFont != old.ukuranFont;
}
}
Konvensi adalah menyediakan dua static method, of dan maybeOf, pada InheritedWidget yang memanggil BuildContext.dependOnInheritedWidgetOfExactType. Method of biasanya mengembalikan instance non-nullable dan assert jika InheritedWidget tidak ditemukan, sedangkan maybeOf mengembalikan instance nullable dan mengembalikan null jika InheritedWidget tidak ditemukan.
Menempatkan InheritedWidget di Tree #
InheritedWidget sendiri bersifat immutable — ia tidak bisa mengubah datanya sendiri. Untuk data yang berubah, gabungkan dengan StatefulWidget:
class TemaKustomProvider extends StatefulWidget {
final Widget child;
const TemaKustomProvider({super.key, required this.child});
@override
State<TemaKustomProvider> createState() => _TemaKustomProviderState();
}
class _TemaKustomProviderState extends State<TemaKustomProvider> {
Color _warnaUtama = Colors.blue;
double _ukuranFont = 14.0;
void gantiTema(Color warna) {
setState(() => _warnaUtama = warna);
// Ini membuat InheritedWidget baru dengan data baru
// --> updateShouldNotify() dipanggil
// --> dependent yang terdaftar di-rebuild
}
@override
Widget build(BuildContext context) {
// Setiap setState() membuat instance TemaKustom baru
return TemaKustom(
warnaUtama: _warnaUtama,
warnaAksen: Colors.orange,
ukuranFont: _ukuranFont,
child: widget.child,
);
}
}
Mengakses dari Widget Descendant #
class TombolPrimary extends StatelessWidget {
final String label;
final VoidCallback onPressed;
const TombolPrimary({
super.key,
required this.label,
required this.onPressed,
});
@override
Widget build(BuildContext context) {
// Akses TemaKustom dari ancestor
final tema = TemaKustom.of(context);
return ElevatedButton(
style: ElevatedButton.styleFrom(
backgroundColor: tema.warnaUtama,
),
onPressed: onPressed,
child: Text(
label,
style: TextStyle(fontSize: tema.ukuranFont),
),
);
}
}
updateShouldNotify — Kontrol Presisi Rebuild #
updateShouldNotify() adalah mekanisme kontrol yang menentukan apakah dependent perlu di-rebuild ketika InheritedWidget diganti:
// Selalu notify -- setiap rebuild parent akan rebuild semua dependent
@override
bool updateShouldNotify(MyWidget old) => true;
// Tidak pernah notify -- gunakan jika data benar-benar tidak berubah
@override
bool updateShouldNotify(MyWidget old) => false;
// Notify hanya jika data benar-benar berubah (paling umum)
@override
bool updateShouldNotify(MyWidget old) =>
data != old.data;
// Notify berdasarkan beberapa field
@override
bool updateShouldNotify(TemaKustom old) =>
warnaUtama != old.warnaUtama ||
warnaAksen != old.warnaAksen ||
ukuranFont != old.ukuranFont;
// Untuk objek kompleks -- gunakan equality yang sudah di-override
@override
bool updateShouldNotify(UserProvider old) =>
user != old.user; // pastikan User mengimplementasikan ==
getInheritedWidgetOfExactType vs dependOnInheritedWidgetOfExactType #
Ada dua cara mengakses InheritedWidget dari context, dengan perilaku yang berbeda:
// dependOnInheritedWidgetOfExactType:
// --> MENDAFTARKAN widget sebagai dependent
// --> Widget AKAN di-rebuild jika InheritedWidget berubah
// --> Gunakan di dalam build(), didChangeDependencies()
final tema = context.dependOnInheritedWidgetOfExactType<TemaKustom>();
// getInheritedWidgetOfExactType:
// --> TIDAK mendaftarkan sebagai dependent
// --> Widget TIDAK akan di-rebuild jika InheritedWidget berubah
// --> Gunakan jika hanya butuh nilai sekali tanpa subscribe perubahan
final tema = context.getInheritedWidgetOfExactType<TemaKustom>();
Contoh penggunaan getInheritedWidgetOfExactType:
@override
void initState() {
super.initState();
// Baca nilai sekali untuk inisialisasi
// Tidak perlu subscribe perubahan
final config = context.getInheritedWidgetOfExactType<AppConfig>();
_apiUrl = config?.apiUrl ?? 'https://default.api.com';
}
InheritedNotifier — InheritedWidget + Listenable #
InheritedNotifier<T extends Listenable> adalah subclass InheritedWidget yang secara otomatis me-rebuild dependent setiap kali Listenable (ChangeNotifier, ValueNotifier, AnimationController, dll.) mengirim notifikasi:
// Tanpa InheritedNotifier: harus gabungkan StatefulWidget + InheritedWidget
// Dengan InheritedNotifier: jauh lebih ringkas
class KeranjangProvider extends InheritedNotifier<KeranjangModel> {
const KeranjangProvider({
super.key,
required KeranjangModel super.notifier, // Listenable
required super.child,
});
static KeranjangModel of(BuildContext context) {
return context
.dependOnInheritedWidgetOfExactType<KeranjangProvider>()!
.notifier!;
}
}
class KeranjangModel extends ChangeNotifier {
final List<Produk> _items = [];
List<Produk> get items => List.unmodifiable(_items);
int get jumlah => _items.length;
double get total => _items.fold(0, (sum, p) => sum + p.harga);
void tambah(Produk produk) {
_items.add(produk);
notifyListeners(); // otomatis trigger rebuild pada dependent!
}
void hapus(Produk produk) {
_items.remove(produk);
notifyListeners();
}
}
// Penggunaan
void main() {
runApp(
KeranjangProvider(
notifier: KeranjangModel(),
child: const MyApp(),
),
);
}
// Di widget mana saja:
class BadgeKeranjang extends StatelessWidget {
@override
Widget build(BuildContext context) {
final keranjang = KeranjangProvider.of(context);
// Otomatis rebuild ketika keranjang.notifyListeners() dipanggil
return Badge(label: Text('${keranjang.jumlah}'));
}
}
InheritedNotifier adalah varian dari InheritedWidget yang dikhususkan untuk subclass dari Listenable. Dependent dinotifikasi setiap kali notifier mengirim notifikasi, atau ketika identitas notifier berubah. Beberapa notifikasi digabungkan sehingga dependent hanya di-rebuild sekali meskipun notifier mengirim beberapa notifikasi antara dua frame.
InheritedModel — Rebuild Selektif Berdasarkan Aspek #
InheritedModel<T> adalah subclass InheritedWidget yang memungkinkan dependent mendaftar hanya untuk “aspek” tertentu dari data — sehingga perubahan pada aspek lain tidak memicu rebuild:
// InheritedModel: dependent hanya rebuild untuk aspek yang ia subscribe
class ABModel extends InheritedModel<String> {
final int nilaiA;
final int nilaiB;
const ABModel({
super.key,
required this.nilaiA,
required this.nilaiB,
required super.child,
});
static ABModel of(BuildContext context, {String? aspect}) {
return InheritedModel.inheritFrom<ABModel>(context, aspect: aspect)!;
}
@override
bool updateShouldNotify(ABModel old) =>
nilaiA != old.nilaiA || nilaiB != old.nilaiB;
// Hanya rebuild jika aspek yang di-subscribe memang berubah
@override
bool updateShouldNotifyDependent(ABModel old, Set<String> dependencies) {
return (nilaiA != old.nilaiA && dependencies.contains('a')) ||
(nilaiB != old.nilaiB && dependencies.contains('b'));
}
}
// Widget yang hanya subscribe aspek 'a'
class WidgetA extends StatelessWidget {
@override
Widget build(BuildContext context) {
// Hanya rebuild jika nilaiA berubah -- nilaiB tidak peduli
final model = ABModel.of(context, aspect: 'a');
return Text('A: ${model.nilaiA}');
}
}
// Widget yang subscribe aspek 'b'
class WidgetB extends StatelessWidget {
@override
Widget build(BuildContext context) {
// Hanya rebuild jika nilaiB berubah
final model = ABModel.of(context, aspect: 'b');
return Text('B: ${model.nilaiB}');
}
}
InheritedWidget sebagai Fondasi State Management #
Hampir semua state management library Flutter dibangun di atas InheritedWidget:
InheritedWidget (Flutter built-in)
|
+-- Provider
| InheritedProvider<T> extends InheritedWidget
| ChangeNotifierProvider = InheritedNotifier
|
+-- Riverpod
| ProviderScope menggunakan InheritedWidget di bawahnya
|
+-- Bloc / flutter_bloc
| BlocProvider menggunakan InheritedWidget
|
+-- GetIt + GetX
Menggunakan InheritedWidget untuk UI binding
// Provider di balik layar (sangat disederhanakan):
class Provider<T> extends InheritedWidget {
final T value;
const Provider({
super.key,
required this.value,
required super.child,
});
static T of<T>(BuildContext context) {
return context.dependOnInheritedWidgetOfExactType<Provider<T>>()!.value;
}
@override
bool updateShouldNotify(Provider<T> old) => value != old.value;
}
// context.read<T>() di Provider = getInheritedWidgetOfExactType (tidak subscribe)
// context.watch<T>() di Provider = dependOnInheritedWidgetOfExactType (subscribe)
Ringkasan #
- InheritedWidget adalah mekanisme Flutter untuk meneruskan data ke bawah widget tree tanpa prop drilling — fondasi dari
Theme.of(),MediaQuery.of(), Provider, Riverpod, dan hampir semua state management library.- Cara kerjanya:
dependOnInheritedWidgetOfExactTypemencari ancestor InheritedWidget terdekat dan mendaftarkan widget sebagai dependent. Hanya dependent yang terdaftar yang di-rebuild saat data berubah.- Selalu sediakan dua static method:
of()(non-nullable, assert jika tidak ditemukan) danmaybeOf()(nullable, return null jika tidak ditemukan).updateShouldNotify()menentukan apakah dependent perlu di-rebuild — bandingkan nilai lama dan baru secara tepat untuk menghindari rebuild yang tidak perlu.- Gunakan
getInheritedWidgetOfExactType(tanpa subscribe) jika hanya butuh nilai sekali, misalnya diinitState().InheritedNotifiermenyederhanakan kombinasi InheritedWidget + ChangeNotifier — dependent otomatis di-rebuild setiap kali notifier memanggilnotifyListeners().InheritedModelmemungkinkan rebuild selektif berdasarkan “aspek” data — hanya widget yang subscribe aspek yang berubah yang di-rebuild.