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 periksa mounted sebelum 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.
  • BuildContext adalah referensi ke posisi widget dalam tree — digunakan untuk mengakses Theme, Navigator, MediaQuery, dan InheritedWidget.
  • Lifecycle StatefulWidget: createState → initState → didChangeDependencies → build → [setState/didUpdateWidget] → deactivate → dispose. Selalu dispose() resource untuk mencegah memory leak.
  • Key memberi 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.

← Sebelumnya: Best Practice   Berikutnya: StatelessWidget →

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