Overview #
Widget adalah unit terkecil dan paling fundamental di Flutter. Semua yang tampak di layar — teks, tombol, gambar, layout, animasi, bahkan padding kosong — adalah widget. Memahami bagaimana widget bekerja, bagaimana ia disusun, dan bagaimana Flutter mengelolanya secara internal adalah fondasi untuk menulis kode Flutter yang efisien dan terstruktur.
Everything is a Widget #
Filosofi Flutter yang paling terkenal: everything is a widget. Ini bukan sekadar slogan — ini adalah keputusan arsitektur yang konsekuensinya terasa di setiap baris kode Flutter yang kamu tulis.
// Semua ini adalah widget:
Text('Halo') // konten visual
Icon(Icons.star) // ikon
Padding(padding: EdgeInsets.all(8)) // spasi
Center(child: ...) // layout
GestureDetector(onTap: ...) // interaksi
Opacity(opacity: 0.5, child: ...) // efek visual
Theme(data: ..., child: ...) // konfigurasi
Navigator(...) // navigasi
MaterialApp(...) // aplikasi itu sendiri
Konsekuensinya: tidak ada sistem yang terpisah untuk layout, styling, animasi, atau navigasi. Semuanya adalah widget yang bisa dikomposisikan dengan cara yang sama.
Tiga Jenis Widget Utama #
Flutter memiliki tiga jenis widget yang menjadi dasar dari semua widget lainnya:
StatelessWidget #
Widget yang outputnya hanya bergantung pada konfigurasi yang diberikan saat dibuat. Tidak memiliki state internal yang bisa berubah. Stateless widget berguna ketika bagian UI yang kamu deskripsikan tidak bergantung pada apapun selain informasi konfigurasi di dalam objek itu sendiri dan BuildContext di mana widget di-inflate.
class ProfilCard extends StatelessWidget {
final String nama;
final String email;
final String? fotoUrl;
const ProfilCard({
super.key,
required this.nama,
required this.email,
this.fotoUrl,
});
@override
Widget build(BuildContext context) {
// Pure function -- output hanya bergantung pada input (props)
// Tidak ada setState(), tidak ada state internal
return Card(
child: ListTile(
leading: fotoUrl != null
? CircleAvatar(backgroundImage: NetworkImage(fotoUrl!))
: const CircleAvatar(child: Icon(Icons.person)),
title: Text(nama),
subtitle: Text(email),
),
);
}
}
Stateless widget seharusnya murni dan bebas side effect. Hindari network call, timer, atau subscription di dalam build. Komputasi mahal seharusnya terjadi di luar build atau di parent state object dan dikirim sebagai constructor parameter.
StatefulWidget #
Widget yang memiliki state internal yang bisa berubah sepanjang lifetime-nya. StatefulWidget instance sendiri bersifat immutable dan menyimpan state mutabelnya di objek State terpisah yang dibuat oleh method createState.
class KonterWidget extends StatefulWidget {
final int initial;
const KonterWidget({super.key, this.initial = 0});
@override
State<KonterWidget> createState() => _KonterWidgetState();
}
class _KonterWidgetState extends State<KonterWidget> {
late int _nilai;
@override
void initState() {
super.initState();
_nilai = widget.initial; // akses props via widget
}
void _tambah() {
setState(() => _nilai++); // trigger rebuild
}
@override
Widget build(BuildContext context) {
return Row(
children: [
Text('Nilai: $_nilai'),
IconButton(
onPressed: _tambah,
icon: const Icon(Icons.add),
),
],
);
}
@override
void dispose() {
// cleanup resources di sini
super.dispose();
}
}
InheritedWidget #
Widget khusus yang memungkinkan data mengalir ke bawah widget tree tanpa harus dikirim secara manual melalui constructor — disebut prop drilling. InheritedWidget berperan sebagai container untuk data yang perlu diakses oleh banyak widget dalam widget tree. Ketika data yang dipegang InheritedWidget berubah, Flutter secara otomatis me-rebuild semua descendant widget yang bergantung pada data tersebut.
class TemaApp extends InheritedWidget {
final Color warnaUtama;
final String bahasa;
const TemaApp({
super.key,
required this.warnaUtama,
required this.bahasa,
required super.child,
});
static TemaApp of(BuildContext context) {
return context.dependOnInheritedWidgetOfExactType<TemaApp>()!;
}
@override
bool updateShouldNotify(TemaApp old) =>
warnaUtama != old.warnaUtama || bahasa != old.bahasa;
}
// Akses dari widget mana pun di bawahnya
class TombolPrimary extends StatelessWidget {
final String label;
const TombolPrimary({super.key, required this.label});
@override
Widget build(BuildContext context) {
final tema = TemaApp.of(context); // tidak perlu prop drilling!
return ElevatedButton(
style: ElevatedButton.styleFrom(backgroundColor: tema.warnaUtama),
onPressed: () {},
child: Text(label),
);
}
}
Widget Tree, Element Tree, dan Render Tree #
Seperti yang sudah dibahas di artikel Framework Layer, Flutter memaintain tiga pohon terpisah. Tapi sekarang kita lihat dari sudut pandang developer — bagaimana ketiga pohon ini berhubungan dengan kode yang kamu tulis:
Kode yang kamu tulis:
Column(
children: [
Text('Halo'),
ElevatedButton(onPressed: ..., child: Text('Klik')),
],
)
Widget Tree Element Tree Render Tree
(blueprint) (instansi) (layout/paint)
Column ColumnElement RenderFlex
| | |
Text TextElement RenderParagraph
ElevatedButton ButtonElement RenderBox (kompleks)
| |
Text TextElement
Widget hanya mendeskripsikan apa yang seharusnya ada — ringan dan immutable. Element adalah instansi widget yang menjaga state dan posisi dalam tree. RenderObject melakukan pekerjaan berat: layout dan painting.
Ketika kamu memanggil setState(), Flutter hanya membangun ulang widget tree untuk widget tersebut dan descendant-nya. Element tree dan Render tree hanya diperbarui jika benar-benar diperlukan.
BuildContext — Lebih dari Sekadar Parameter #
BuildContext adalah referensi ke lokasi sebuah widget dalam widget tree. Ia bukan hanya “parameter wajib” — ia adalah alat komunikasi antara widget dan tree di sekitarnya.
@override
Widget build(BuildContext context) {
// Akses tema dari tree
final tema = Theme.of(context);
final warna = tema.colorScheme.primary;
// Akses MediaQuery
final ukuranLayar = MediaQuery.sizeOf(context);
final isTablet = ukuranLayar.width > 600;
// Akses Navigator
Navigator.of(context).push(...);
// Akses InheritedWidget kustom
final temaApp = TemaApp.of(context);
// Akses Scaffold (ancestor terdekat)
ScaffoldMessenger.of(context).showSnackBar(...);
return Container();
}
Context dan Subtree #
Setiap widget punya context-nya sendiri yang merepresentasikan posisinya dalam tree:
class ContohContext extends StatelessWidget {
@override
Widget build(BuildContext context) {
// context di sini merujuk ke ContohContext
return Builder(
builder: (innerContext) {
// innerContext merujuk ke Builder -- child dari ContohContext
// Gunakan innerContext untuk akses widget yang di-insert
// di dalam build method ini (seperti Scaffold)
ScaffoldMessenger.of(innerContext).showSnackBar(...);
return const SizedBox();
},
);
}
}
Jangan gunakan context setelah widget di-dispose. Setelah
dispose()dipanggil, context tidak lagi valid. Selalu periksamountedsebelum menggunakan context di dalam callback async:Future<void> proses() async { await operasiAsync(); if (!mounted) return; // periksa sebelum pakai context Navigator.of(context).pop(); }
Lifecycle Widget #
Lifecycle StatelessWidget #
Lifecycle StatelessWidget sangat sederhana: Constructor dipanggil untuk membuat instance widget dan mengatur propertinya, lalu build() dipanggil untuk me-render widget.
StatelessWidget lifecycle:
constructor() --> widget dibuat dengan konfigurasi
build() --> widget di-render (bisa dipanggil berkali-kali)
Lifecycle StatefulWidget #
StatefulWidget melibatkan dua objek: Widget yang immutable dan State yang mutable. Objek State mengekspos lifecycle callback yang harus kamu gunakan untuk mengelola resource.
StatefulWidget + State lifecycle:
createState() --> State object dibuat (sekali)
|
initState() --> State diinisialisasi (sekali)
[baca: init controllers, subscriptions, satu kali fetch]
|
didChangeDependencies()--> dipanggil setelah initState()
[dan setiap kali InheritedWidget yang digunakan berubah]
|
build() --> UI di-render
[dipanggil setiap: initState, didChangeDependencies,
didUpdateWidget, setState, atau parent rebuild]
|
setState() --> trigger rebuild
|
didUpdateWidget() --> parent berubah konfigurasinya
[bandingkan oldWidget dengan widget untuk update]
|
deactivate() --> state dilepas dari tree sementara
[bisa di-insert kembali ke tree di frame yang sama]
|
dispose() --> state dihancurkan permanen
[cleanup: dispose controllers, cancel subscriptions]
Penggunaan yang Tepat Per Lifecycle Method #
class _MyScreenState extends State<MyScreen>
with SingleTickerProviderStateMixin {
late AnimationController _controller;
late StreamSubscription _sub;
String? _cachedThemeColor;
// initState: inisialisasi resource, satu kali
@override
void initState() {
super.initState();
_controller = AnimationController(
vsync: this,
duration: const Duration(milliseconds: 300),
);
// Jangan akses context.dependOn di sini! InheritedWidget belum siap
}
// didChangeDependencies: akses InheritedWidget / Provider
@override
void didChangeDependencies() {
super.didChangeDependencies();
// Aman mengakses Theme, MediaQuery, Provider di sini
final color = Theme.of(context).colorScheme.primary.toString();
if (color != _cachedThemeColor) {
_cachedThemeColor = color;
// Lakukan sesuatu saat tema berubah
}
}
// didUpdateWidget: ketika parent mengubah konfigurasi
@override
void didUpdateWidget(MyScreen oldWidget) {
super.didUpdateWidget(oldWidget);
if (widget.animationDuration != oldWidget.animationDuration) {
_controller.duration = widget.animationDuration;
}
}
@override
Widget build(BuildContext context) => Container();
// dispose: SELALU cleanup resource!
@override
void dispose() {
_controller.dispose(); // AnimationController
_sub.cancel(); // StreamSubscription
super.dispose();
}
}
Key System — Identitas Widget #
Key adalah identifier opsional yang memberi identitas unik pada widget. Tanpa key, Flutter mengidentifikasi widget berdasarkan tipe dan posisinya dalam tree. Dengan key, Flutter bisa melacak widget berdasarkan identitasnya meskipun posisinya berubah.
Kapan Key Diperlukan #
// MASALAH tanpa key: state ter-attach ke posisi, bukan ke item
// Ketika list di-reorder, state (checkbox) tidak ikut pindah!
Column(
children: items.map((item) =>
CheckboxListTile( // state checkbox di posisi 0, 1, 2...
title: Text(item.nama),
value: item.dipilih,
onChanged: (_) {},
)
).toList(),
)
// SOLUSI dengan ValueKey: state ter-attach ke item.id
Column(
children: items.map((item) =>
CheckboxListTile(
key: ValueKey(item.id), // identitas unik berdasarkan id
title: Text(item.nama),
value: item.dipilih,
onChanged: (_) {},
)
).toList(),
)
Jenis-jenis Key #
// ValueKey -- berdasarkan nilai (String, int, dll.)
ValueKey('user-123')
ValueKey(product.id)
// ObjectKey -- berdasarkan referensi objek
ObjectKey(userObject)
// UniqueKey -- selalu unik, dibuat baru setiap kali
// HATI-HATI: rebuild parent akan membuat Key baru = State direset!
UniqueKey() // gunakan hanya jika benar-benar ingin reset state
// GlobalKey -- akses state dari mana saja di tree
final formKey = GlobalKey<FormState>();
Form(
key: formKey,
child: ...
)
// Dari mana saja:
formKey.currentState?.validate();
formKey.currentState?.save();
Prinsip Komposisi Widget #
Filosofi inti Flutter: lebih baik komposisi daripada inheritance. Alih-alih membuat subclass widget, gabungkan widget yang sudah ada:
// KURANG IDEAL -- inheritance untuk kustomisasi
class MyButton extends ElevatedButton {
// Tidak direkomendasikan -- ElevatedButton punya banyak internal
}
// LEBIH BAIK -- komposisi
class PrimaryButton extends StatelessWidget {
final String label;
final VoidCallback? onPressed;
final bool isLoading;
const PrimaryButton({
super.key,
required this.label,
this.onPressed,
this.isLoading = false,
});
@override
Widget build(BuildContext context) {
return ElevatedButton(
onPressed: isLoading ? null : onPressed,
child: isLoading
? const SizedBox(
width: 16,
height: 16,
child: CircularProgressIndicator(strokeWidth: 2),
)
: Text(label),
);
}
}
// Penggunaan
PrimaryButton(
label: 'Simpan',
onPressed: _handleSave,
isLoading: _isSaving,
)
Ringkasan #
- Flutter menggunakan filosofi “everything is a widget” — layout, styling, animasi, navigasi, semuanya adalah widget yang bisa dikomposisikan.
- Ada tiga jenis widget utama: StatelessWidget (output hanya dari props), StatefulWidget (memiliki state internal yang bisa berubah), dan InheritedWidget (meneruskan data ke bawah tree tanpa prop drilling).
- Flutter memaintain tiga pohon: Widget Tree (blueprint), Element Tree (instansi), dan Render Tree (layout/paint). Hanya yang perlu diperbarui saja yang diperbarui.
BuildContextadalah referensi ke posisi widget dalam tree — digunakan untuk mengakses Theme, Navigator, MediaQuery, dan InheritedWidget.- Lifecycle StatefulWidget:
createState → initState → didChangeDependencies → build → [setState/didUpdateWidget] → deactivate → dispose. Selaludispose()resource untuk mencegah memory leak.Keymemberi identitas unik pada widget sehingga Flutter bisa melacaknya saat posisi berubah — penting untuk list yang bisa di-reorder.- Preferensikan komposisi daripada inheritance untuk kustomisasi widget.