Skip to content

Bab 3 — Widget & Widget Tree

Ada satu kalimat yang diulang-ulang di setiap kursus Flutter, dan kali ini percayalah sepenuhnya: semuanya adalah widget. Tombol adalah widget. Teks adalah widget. Jarak kosong adalah widget. Padding adalah widget. Bahkan keputusan "susun ini secara vertikal" diwujudkan sebagai widget.

Begitu kamu benar-benar menerima ini, Flutter berubah dari kumpulan API yang harus dihafal menjadi satu ide sederhana yang diterapkan berulang-ulang.

Widget adalah cetak biru, bukan piksel

Kesalahpahaman paling umum: mengira widget adalah benda yang tampak di layar. Bukan. Widget adalah deskripsi tentang apa yang seharusnya tampak.

dart
const Text('Halo')

Objek Text itu sendiri tidak menggambar apa pun. Ia hanya sebuah objek Dart biasa yang menyimpan konfigurasi: "teks yang ingin ditampilkan adalah 'Halo'". Flutter kemudian membaca deskripsi itu dan memutuskan piksel mana yang harus dinyalakan.

Konsekuensinya penting dan sering mengejutkan pendatang dari framework lain: widget itu murah dan sekali pakai. Flutter membuang dan membangun ulang seluruh pohon widget berkali-kali dalam satu detik ketika terjadi animasi, dan itu bukan masalah — karena yang dibuang hanyalah objek konfigurasi ringan, bukan elemen antarmuka yang sesungguhnya. Mekanisme di balik ini dibedah tuntas di Bab 12.

Huruf besar dan huruf kecil

Ada aturan visual sederhana yang akan mempercepat pembacaan kodemu, dan ini salah satu hal pertama yang perlu kamu latih matanya:

Diawali huruf besar → itu widget atau kelas. Diawali huruf kecil → itu argumen atau properti.

dart
Container(          // ← C besar: ini widget
  width: 200,       // ← w kecil: ini argumen
  height: 100,      // ← h kecil: ini argumen
  color: Colors.blue,
  child: Text(      // ← T besar: widget lagi
    'Halo',
    style: TextStyle(   // ← T besar: kelas konfigurasi
      fontSize: 20,     // ← f kecil: argumen
    ),
  ),
)

Sekali kamu terbiasa, kamu bisa membaca struktur sebuah widget tree hanya dengan memindai bentuk hurufnya, tanpa membaca setiap kata.

Widget di dalam widget

Antarmuka Flutter dibangun dengan menyarangkan widget. Setiap widget membungkus atau menampung widget lain, dan hasilnya membentuk struktur pohon.

Ada dua pola penampungan yang perlu kamu bedakan:

child — untuk widget yang menampung tepat satu anak:

dart
Center(
  child: Text('Sendirian di tengah'),
)

children — untuk widget yang menampung banyak anak, berupa list:

dart
Column(
  children: [
    Text('Baris pertama'),
    Text('Baris kedua'),
    Text('Baris ketiga'),
  ],
)

Aturannya konsisten di seluruh Flutter. Container, Padding, Center, SizedBox, dan Expanded memakai child. Column, Row, Stack, ListView, dan Wrap memakai children.

Membaca sebuah widget tree

Mari lihat contoh nyata dan gambarkan pohonnya.

dart
// lib/main.dart
import 'package:flutter/material.dart';

void main() {
  runApp(const AplikasiSaya());
}

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

  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      title: 'Contoh Widget Tree',
      debugShowCheckedModeBanner: false,
      home: Scaffold(
        appBar: AppBar(
          title: const Text('Profil'),
        ),
        body: Center(
          child: Column(
            mainAxisAlignment: MainAxisAlignment.center,
            children: [
              const CircleAvatar(
                radius: 48,
                backgroundColor: Colors.indigo,
                child: Icon(Icons.person, size: 48, color: Colors.white),
              ),
              const SizedBox(height: 16),
              const Text(
                'Firnanda',
                style: TextStyle(fontSize: 24, fontWeight: FontWeight.bold),
              ),
              const SizedBox(height: 4),
              Text(
                'Pembelajar Flutter',
                style: TextStyle(fontSize: 16, color: Colors.grey.shade600),
              ),
            ],
          ),
        ),
      ),
    );
  }
}

Susunannya kalau digambarkan:

MaterialApp
└── Scaffold
    ├── AppBar
    │   └── Text ("Profil")
    └── Center
        └── Column
            ├── CircleAvatar
            │   └── Icon (person)
            ├── SizedBox (tinggi 16)
            ├── Text ("Firnanda")
            ├── SizedBox (tinggi 4)
            └── Text ("Pembelajar Flutter")

Perhatikan dua hal:

SizedBox dipakai sebagai pemberi jarak. Ini idiom Flutter yang sangat umum — alih-alih margin, kamu menyisipkan kotak kosong dengan tinggi tertentu di antara dua widget. Sederhana, eksplisit, dan mudah diatur.

CircleAvatar punya child berupa Icon. Sebuah widget bisa menjadi induk sekaligus anak; tidak ada batasan kedalaman.

Widget bawaan yang wajib kamu kenal

Sebelum menyelam ke layout di bab berikutnya, ini widget-widget yang akan kamu pakai hampir setiap hari.

MaterialApp dan Scaffold

MaterialApp adalah pembungkus terluar. Ia menyediakan tema, rute navigasi, lokalisasi, dan konteks Material untuk seluruh widget di bawahnya.

dart
MaterialApp(
  title: 'Nama Aplikasi',            // judul untuk task switcher OS
  debugShowCheckedModeBanner: false, // hilangkan pita "DEBUG" di pojok
  theme: ThemeData(
    colorScheme: ColorScheme.fromSeed(seedColor: Colors.teal),
    useMaterial3: true,
  ),
  home: const HalamanUtama(),        // halaman pertama
)

Scaffold adalah kerangka satu halaman. Ia menyediakan slot-slot standar:

dart
Scaffold(
  appBar: AppBar(title: const Text('Judul')),
  body: const Center(child: Text('Isi halaman')),
  floatingActionButton: FloatingActionButton(
    onPressed: () {},
    child: const Icon(Icons.add),
  ),
  drawer: const Drawer(child: Text('Menu samping')),
  bottomNavigationBar: BottomNavigationBar(
    items: const [
      BottomNavigationBarItem(icon: Icon(Icons.home), label: 'Beranda'),
      BottomNavigationBarItem(icon: Icon(Icons.settings), label: 'Setelan'),
    ],
  ),
)

Tanpa Scaffold, kamu kehilangan latar belakang, penanganan area aman, dan kemampuan menampilkan snackbar. Setiap halaman sebaiknya punya satu.

AppBar

dart
AppBar(
  title: const Text('Pengeluaran'),
  backgroundColor: Colors.indigo,
  foregroundColor: Colors.white,
  centerTitle: true,
  leading: IconButton(
    icon: const Icon(Icons.menu),
    onPressed: () {},
  ),
  actions: [
    IconButton(
      icon: const Icon(Icons.add),
      onPressed: () {},
    ),
    IconButton(
      icon: const Icon(Icons.search),
      onPressed: () {},
    ),
  ],
)

leading adalah slot kiri, actions adalah daftar tombol di kanan. Kalau kamu tidak mengisi leading dan halaman ini dibuka lewat navigasi, Flutter otomatis menaruh tombol kembali di sana.

Text

dart
Text(
  'Teks dengan gaya',
  style: TextStyle(
    fontSize: 18,
    fontWeight: FontWeight.bold,
    color: Colors.indigo,
    letterSpacing: 0.5,
    height: 1.4,              // tinggi baris, kelipatan dari fontSize
  ),
  textAlign: TextAlign.center,
  maxLines: 2,
  overflow: TextOverflow.ellipsis,   // potong dengan "..." kalau kepanjangan
)

maxLines berpasangan dengan overflow adalah kombinasi penting. Tanpanya, teks panjang akan meluber dan memicu peringatan overflow kuning-hitam yang terkenal itu.

Icon

dart
Icon(
  Icons.favorite,
  size: 32,
  color: Colors.red,
)

Flutter menyertakan ribuan ikon Material. Di editor, ketik Icons. lalu biarkan autocomplete menampilkan daftarnya.

Objek konfigurasi bukan-widget

Tidak semua yang berhuruf besar adalah widget. Beberapa kelas hanya berfungsi sebagai wadah konfigurasi dan tidak pernah muncul sendiri di pohon.

dart
Container(
  decoration: BoxDecoration(          // ← bukan widget, objek konfigurasi
    gradient: LinearGradient(         // ← juga objek konfigurasi
      colors: [
        Colors.deepPurple,
        Colors.indigo.shade900,
      ],
      begin: Alignment.topLeft,
      end: Alignment.bottomRight,
    ),
    borderRadius: BorderRadius.circular(16),
    boxShadow: [
      BoxShadow(
        color: Colors.black.withValues(alpha: 0.2),
        blurRadius: 12,
        offset: const Offset(0, 4),
      ),
    ],
  ),
  padding: const EdgeInsets.all(24),
  child: const Text(
    'Kartu bergradasi',
    style: TextStyle(color: Colors.white, fontSize: 20),
  ),
)

BoxDecoration, LinearGradient, BorderRadius, BoxShadow, EdgeInsets, TextStyle, dan Offset — semuanya objek konfigurasi. Kamu tidak bisa menaruhnya sebagai child; mereka hanya masuk ke properti.

Perhatikan colors: pada LinearGradient bertipe List<Color> — sebuah generic, persis seperti yang dibahas di Bab 2. Kamu harus memberi minimal dua warna, karena gradasi butuh titik awal dan akhir.

Warna dengan transparansi

API lama memakai Colors.black.withOpacity(0.2). Sejak Flutter 3.27 metode itu ditandai usang dan digantikan withValues(alpha: 0.2), yang lebih presisi karena bekerja pada ruang warna lebar. Kalau kamu menemukan withOpacity di tutorial lama, itu penggantinya.

Kenapa kamu butuh widget kustom

Cepat atau lambat, build() kamu akan menjadi seratus baris dengan kurung yang bersarang belasan tingkat. Ketika itu terjadi, kamu kehilangan kemampuan melihat struktur.

Ada tiga alasan memecahnya menjadi widget kustom:

Keterbacaan. Pohon yang dalam sulit dibaca. Memberi nama pada sepotong pohon mengubahnya dari "sekumpulan Container dan Column" menjadi "KartuPengeluaran" — dan nama itu menjelaskan maksudnya.

Penggunaan ulang. Kalau lima tempat butuh kartu dengan gaya yang sama, kamu tidak menyalin kodenya lima kali.

Performa. Ini yang paling sering dilupakan. Ketika sebuah widget dibangun ulang, seluruh isi build()-nya ikut dibangun ulang. Memecah bagian yang jarang berubah ke widget terpisah membatasi cakupan pembangunan ulang itu. Dibahas lebih dalam di Bab 12.

Membangun widget kustom

Polanya selalu sama: buat kelas yang mewarisi StatelessWidget, terima konfigurasi lewat konstruktor, kembalikan pohon dari build().

dart
// lib/widgets/kartu_profil.dart
import 'package:flutter/material.dart';

class KartuProfil extends StatelessWidget {
  const KartuProfil({
    super.key,
    required this.nama,
    required this.peran,
    required this.ikon,
    this.warna = Colors.indigo,
  });

  final String nama;
  final String peran;
  final IconData ikon;
  final Color warna;

  @override
  Widget build(BuildContext context) {
    return Card(
      elevation: 2,
      margin: const EdgeInsets.symmetric(horizontal: 16, vertical: 8),
      shape: RoundedRectangleBorder(
        borderRadius: BorderRadius.circular(12),
      ),
      child: Padding(
        padding: const EdgeInsets.all(16),
        child: Row(
          children: [
            CircleAvatar(
              radius: 28,
              backgroundColor: warna,
              child: Icon(ikon, color: Colors.white, size: 28),
            ),
            const SizedBox(width: 16),
            Column(
              crossAxisAlignment: CrossAxisAlignment.start,
              children: [
                Text(
                  nama,
                  style: const TextStyle(
                    fontSize: 18,
                    fontWeight: FontWeight.bold,
                  ),
                ),
                const SizedBox(height: 4),
                Text(
                  peran,
                  style: TextStyle(
                    fontSize: 14,
                    color: Colors.grey.shade600,
                  ),
                ),
              ],
            ),
          ],
        ),
      ),
    );
  }
}

Beberapa hal yang perlu diperhatikan pada kode di atas:

Properti bertanda final. Widget bersifat tidak berubah — begitu dibuat, konfigurasinya tidak boleh diganti. Kalau kamu perlu nilai yang berubah, itu pertanda kamu butuh StatefulWidget (Bab 5).

Nilai bawaan pada warna. Karena this.warna = Colors.indigo diberi nilai bawaan, pemanggil boleh tidak mengisinya. Tetapi perhatikan: nilai bawaan wajib berupa konstanta waktu kompilasi, sehingga Colors.grey.shade600 tidak bisa dipakai di posisi itu.

Konstruktor const. Karena semua propertinya final, kelas ini bisa punya konstruktor const — yang berarti Flutter bisa melewatkan pembangunan ulang ketika konfigurasinya tidak berubah.

Cara memakainya:

dart
// lib/main.dart
import 'package:flutter/material.dart';
import 'widgets/kartu_profil.dart';

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

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(title: const Text('Tim Kami')),
      body: ListView(
        padding: const EdgeInsets.symmetric(vertical: 8),
        children: const [
          KartuProfil(
            nama: 'Firnanda',
            peran: 'Flutter Developer',
            ikon: Icons.phone_android,
          ),
          KartuProfil(
            nama: 'Ayu',
            peran: 'UI/UX Designer',
            ikon: Icons.brush,
            warna: Colors.pink,
          ),
          KartuProfil(
            nama: 'Bagas',
            peran: 'Backend Engineer',
            ikon: Icons.storage,
            warna: Colors.teal,
          ),
        ],
      ),
    );
  }
}

Bandingkan keterbacaan versi ini dengan bayangkan menyalin seluruh isi KartuProfil tiga kali. Perbedaannya bukan sekadar jumlah baris — versi ini menjelaskan apa yang ada di halaman, bukan bagaimana kartunya digambar.

Memecah kode ke banyak berkas

Menaruh semuanya di main.dart hanya sanggup bertahan sampai sekitar dua ratus baris. Setelah itu, pecahlah.

Struktur folder yang umum dipakai untuk proyek kecil sampai menengah:

lib/
├── main.dart                  ← hanya main() dan MaterialApp
├── models/                    ← kelas data murni
│   ├── pengeluaran.dart
│   └── kategori.dart
├── screens/                   ← satu berkas per halaman penuh
│   ├── beranda.dart
│   ├── tambah_pengeluaran.dart
│   └── detail.dart
├── widgets/                   ← komponen yang dipakai ulang
│   ├── kartu_profil.dart
│   ├── daftar_pengeluaran.dart
│   └── grafik/
│       ├── grafik.dart
│       └── batang.dart
└── utils/                     ← pembantu, format, konstanta
    └── format_rupiah.dart

Pemisahan screens/ dan widgets/ adalah pembedaan yang berguna: screen adalah sesuatu yang mengisi seluruh layar dan biasanya punya Scaffold sendiri, sedangkan widget adalah potongan yang hidup di dalam screen.

Untuk memakai widget dari berkas lain, impor dengan jalur relatif:

dart
// Dari lib/screens/beranda.dart
import '../widgets/kartu_profil.dart';
import '../models/pengeluaran.dart';

Atau dengan jalur paket, yang lebih tahan terhadap pemindahan berkas:

dart
import 'package:belajar_flutter/widgets/kartu_profil.dart';

Nama paket (belajar_flutter) diambil dari kolom name: di pubspec.yaml.

Latihan Terpandu: buat widget kartu statistik

Sebelum lanjut, coba kerjakan ini sendiri. Buat widget bernama KartuStatistik yang menampilkan sebuah ikon, angka besar, dan label kecil di bawahnya — mirip kartu ringkasan di aplikasi dashboard.

Kerangkanya sudah disiapkan di bawah. Bagian yang perlu kamu isi adalah isi build().

dart
// lib/widgets/kartu_statistik.dart
import 'package:flutter/material.dart';

class KartuStatistik extends StatelessWidget {
  const KartuStatistik({
    super.key,
    required this.ikon,
    required this.nilai,
    required this.label,
    required this.warna,
  });

  final IconData ikon;
  final String nilai;
  final String label;
  final Color warna;

  @override
  Widget build(BuildContext context) {
    // TODO: kembalikan sebuah Card berisi Column dengan:
    //   1. Icon berwarna `warna` berukuran 32
    //   2. Jarak 8 piksel
    //   3. Text `nilai` — besar dan tebal
    //   4. Text `label` — kecil dan abu-abu
    throw UnimplementedError();
  }
}

Kalau sudah, bandingkan dengan versi ini:

Lihat solusi
dart
@override
Widget build(BuildContext context) {
  return Card(
    elevation: 1,
    shape: RoundedRectangleBorder(
      borderRadius: BorderRadius.circular(12),
    ),
    child: Padding(
      padding: const EdgeInsets.symmetric(vertical: 20, horizontal: 16),
      child: Column(
        mainAxisSize: MainAxisSize.min,
        children: [
          Icon(ikon, size: 32, color: warna),
          const SizedBox(height: 8),
          Text(
            nilai,
            style: const TextStyle(
              fontSize: 24,
              fontWeight: FontWeight.bold,
            ),
          ),
          const SizedBox(height: 2),
          Text(
            label,
            style: TextStyle(
              fontSize: 13,
              color: Colors.grey.shade600,
            ),
          ),
        ],
      ),
    ),
  );
}

Perhatikan mainAxisSize: MainAxisSize.min. Tanpa itu, Column akan mencoba memenuhi seluruh tinggi yang tersedia. Properti ini dan saudara-saudaranya dibahas tuntas di Bab 4.

Latihan Mandiri

Kerjakan salah satu, beberapa, atau semuanya secara berurutan untuk melatih pemahamanmu sampai benar-benar lekat.

Variasi 1: Baris Profil — ⭐ · 15–20 menit

Tantangan: Bangun widget BarisProfil yang menampilkan foto bulat di kiri (cukup CircleAvatar berisi inisial), nama dan jabatan bertumpuk di tengah, dan satu ikon di kanan. Widget ini harus bisa dipakai berkali-kali dengan data berbeda dalam satu layar.

Bayangan tampilan:

text
╭───────────────────────────────────╮
│ 9:41                      ▂▄▆ 🔋   │
│                                     │
│  ┌─────────────────────────────┐  │
│  │ ╭────╮                      │  │
│  │ │ AB │  Ade Bramantyo   (i)  │  │
│  │ ╰────╯  Product Designer    │  │
│  └─────────────────────────────┘  │
│  ┌─────────────────────────────┐  │
│  │ ╭────╮                      │  │
│  │ │ CD │  Citra Dewi      (i)  │  │
│  │ ╰────╯  Backend Engineer    │  │
│  └─────────────────────────────┘  │
│  ┌─────────────────────────────┐  │
│  │ ╭────╮                      │  │
│  │ │ EF │  Eka Fajar W...  (i)  │  │
│  │ ╰────╯  Nama Sangat Panjang │  │
│  └─────────────────────────────┘  │
│                                     │
│              ▬▬▬▬                  │
╰───────────────────────────────────╯

Kriteria selesai:

  • Seluruh properti final, dan konstruktornya const.
  • Widget dipakai minimal tiga kali di satu layar dengan data yang berbeda — tanpa menyalin-tempel kodenya.
  • Berada di berkasnya sendiri di dalam folder widgets/, bukan menumpang di main.dart.
  • Warna dan ukuran teks diambil dari tema, bukan ditulis sebagai angka dan Colors.* langsung di dalam build().

Petunjuk: Struktur yang kamu butuhkan adalah Row berisi tiga hal, dengan bagian tengah dibungkus sesuatu yang memaksanya mengambil sisa ruang — kalau tidak, nama yang panjang akan meluber dan kamu mendapat garis kuning-hitam. Nama dan jabatan yang bertumpuk berarti ada Column di dalam Row itu, dan Column di dalam Row hampir selalu butuh crossAxisAlignment yang disetel ke kiri. Untuk gaya teks, Theme.of(context).textTheme sudah menyediakan pasangan yang cocok untuk judul dan keterangan di bawahnya.

Variasi 2: Lencana Status Pesanan — ⭐ · 20–30 menit

Tantangan: Buat widget LencanaStatus yang menampilkan status pesanan (diproses, dikirim, tiba, dibatalkan) sebagai pil berwarna dengan ikon kecil di kiri teksnya. Warna dan ikon ditentukan oleh statusnya, bukan dioper dari luar.

Bayangan tampilan:

text
╭───────────────────────────────────╮
│ 9:41                      ▂▄▆ 🔋   │
│                                     │
│  ┌─────────────────────────────┐  │
│  │ Pesanan #1024    [✓ Tiba]   │  │
│  └─────────────────────────────┘  │
│  ┌─────────────────────────────┐  │
│  │ Pesanan #1023  [🚚 Dikirim] │  │
│  └─────────────────────────────┘  │
│  ┌─────────────────────────────┐  │
│  │ Pesanan #1022  [⏳ Diproses] │  │
│  └─────────────────────────────┘  │
│  ┌─────────────────────────────┐  │
│  │ Pesanan #1021 [✗ Dibatalkan]│  │
│  └─────────────────────────────┘  │
│        ... total 8 pesanan         │
│                                     │
│              ▬▬▬▬                  │
╰───────────────────────────────────╯

Kriteria selesai:

  • Status adalah enum, dan widget memilih warna serta ikonnya sendiri lewat switch tanpa cabang default.
  • Konstruktornya const, dan lencana dipakai di dalam sebuah daftar berisi minimal delapan pesanan dengan status bercampur.
  • Warna diambil dari ColorScheme, bukan dari konstanta Colors.*.

Petunjuk: Godaan pertama adalah mengoper warna dan ikon sebagai parameter, tetapi itu memindahkan keputusan tampilan ke setiap pemanggil — dan kamu akan menemukan dua tempat yang memberi warna berbeda untuk status yang sama. Membiarkan widget memutuskan sendiri dari satu enum membuat tampilannya konsisten di seluruh aplikasi secara otomatis. Untuk bentuk pil, sudut yang sangat membulat pada dekorasi sudah cukup — kamu belum butuh ClipRRect di sini.

Variasi 3: Tiket Kereta — ⭐⭐ · 30–45 menit

Tantangan: Bangun widget tiket kereta yang bisa dipakai ulang: stasiun asal dan tujuan di kiri dan kanan dengan panah di antaranya, jam keberangkatan dan kedatangan di bawah masing-masing, durasi perjalanan di tengah, dan nomor kursi di bagian bawah yang dipisahkan garis putus-putus.

Bayangan tampilan:

text
╭───────────────────────────────────╮
│ 9:41                      ▂▄▆ 🔋   │
│                                     │
│  ┌─────────────────────────────┐  │
│  │ JKT                     BDG │  │
│  │ 08:00 ──45 menit──▶   08:45 │  │
│  │ - - - - - - - - - - - - - - │  │
│  │        Kursi: 12A           │  │
│  └─────────────────────────────┘  │
│  ┌─────────────────────────────┐  │
│  │ BDG                     JKT │  │
│  │ 14:10 ──45 menit──▶   14:55 │  │
│  │ - - - - - - - - - - - - - - │  │
│  │       (tanpa kursi)         │  │
│  └─────────────────────────────┘  │
│                                     │
│              ▬▬▬▬                  │
╰───────────────────────────────────╯

Kriteria selesai:

  • Widget dipecah menjadi minimal tiga widget kustom yang lebih kecil, bukan satu build() raksasa.
  • Nama stasiun yang panjang dipotong dengan rapi, bukan meluber.
  • Seluruh widget anak berkonstruktor const di tempat yang memungkinkan.
  • Tiket tampil benar untuk nomor kursi yang kosong (tiket tanpa kursi).

Petunjuk: Ketika sebuah build() mulai sulit dibaca, tanda pertamanya biasanya adalah kamu harus menggulir untuk melihat kurung penutup yang berpasangan. Pecah pada batas yang punya nama alami — "kolom stasiun", "penanda durasi", "baris kursi" — bukan pada kedalaman indentasi sembarang. Untuk teks yang berpotensi panjang, overflow pada Text menentukan apa yang terjadi, tetapi ia hanya bekerja kalau widget itu punya batas lebar yang jelas.

Variasi 4: Panel Statistik Tersusun — ⭐⭐ · 30–45 menit

Tantangan: Bangun satu panel dashboard yang tersusun dari widget-widget kecil yang kamu buat sendiri, di mana panel besar tidak mengetahui apa pun tentang isi setiap kartunya — ia hanya menerima daftar widget dan menatanya.

Bayangan tampilan:

text
╭───────────────────────────────────╮
│ 9:41                      ▂▄▆ 🔋   │
│                                     │
│  ┌───────────┐ ┌────────────┐     │
│  │   1.234   │ │  ▂▄▆█▅▃▁   │     │
│  │ Total Sale│ │  Grafik    │     │
│  └───────────┘ └────────────┘     │
│  ┌─────────────────────────────┐  │
│  │ • Order #001                │  │
│  │ • Order #002                │  │
│  │ • Order #003                │  │
│  └─────────────────────────────┘  │
│                                     │
│              ▬▬▬▬                  │
╰───────────────────────────────────╯

Kriteria selesai:

  • Panel menerima List<Widget> sebagai anak, dan bisa menampung jumlah kartu berapa pun tanpa perubahan kode.
  • Ada minimal tiga jenis kartu yang bentuk isinya berbeda (angka besar, grafik batang sederhana dari Container, dan daftar ringkas).
  • Menukar urutan kartu cukup dengan menukar urutan di daftar, tanpa menyentuh kode panel.

Petunjuk: Ini latihan tentang komposisi, dan aturannya sederhana: widget induk yang menerima children bertipe Widget tidak boleh tahu jenis anaknya. Begitu kamu tergoda menulis if (anak is KartuAngka), komposisinya sudah bocor. Semua widget tata letak bawaan Flutter mengikuti aturan ini — itulah alasan Column bisa menampung apa pun tanpa pernah diperbarui.

Variasi 5: Kartu Cuaca Bervarian — ⭐⭐⭐ · 45–60 menit

Tantangan: Buat satu widget kartu cuaca yang punya tiga varian tampilan — ringkas (satu baris), sedang (kartu persegi), dan lebar (kartu memanjang dengan ramalan) — semuanya dari satu kelas widget, dipilih lewat konstruktor bernama.

Bayangan tampilan:

text
╭───────────────────────────────────╮
│ 9:41                      ▂▄▆ 🔋   │
│                                     │
│  .ringkas                          │
│  ┌─────────────────────────────┐  │
│  │ ☀ 28°C   Cerah               │  │
│  └─────────────────────────────┘  │
│  .sedang                           │
│  ┌───────────────┐                │
│  │      ☀        │                │
│  │     28°C      │                │
│  │     Cerah     │                │
│  └───────────────┘                │
│  .lebar                            │
│  ┌─────────────────────────────┐  │
│  │ ☀ 28°C  Cerah                │  │
│  │ 09:00☀ 10:00⛅ 11:00🌧 12:00☀ │  │
│  └─────────────────────────────┘  │
│                                     │
│              ▬▬▬▬                  │
╰───────────────────────────────────╯

Kriteria selesai:

  • Ketiga varian dibuat lewat konstruktor bernama (KartuCuaca.ringkas, .sedang, .lebar), bukan tiga kelas terpisah.
  • Bagian yang sama di ketiga varian (ikon, suhu, label) berada di satu tempat, tidak disalin tiga kali.
  • Ketiganya tampil bersamaan di satu layar demo, dan semuanya const di titik pemakaian.

Petunjuk: Konstruktor bernama yang menyetel satu properti privat bertipe enum adalah cara paling sederhana; build() kemudian memilih tata letak berdasarkan properti itu. Yang membuat latihan ini bernilai adalah kriteria kedua: begitu kamu menyalin blok ikon-dan-suhu ke tiga cabang, kamu sudah menciptakan tiga tempat yang harus diperbarui bersamaan. Ekstrak dulu bagian bersamanya menjadi widget sendiri, baru susun ketiga variannya dari situ.

Ikhtisar

  • Widget adalah deskripsi, bukan piksel. Ia murah dibuat dan memang dirancang untuk dibuang serta dibangun ulang berkali-kali.
  • Huruf besar = widget atau kelas. Huruf kecil = argumen. Ini pintasan visual untuk membaca kode Flutter dengan cepat.
  • child menampung satu anak, children menampung banyak. Konsisten di seluruh framework.
  • MaterialApp membungkus seluruh aplikasi; Scaffold membungkus satu halaman dan menyediakan slot untuk app bar, body, FAB, drawer, dan bottom nav.
  • Tidak semua kelas berhuruf besar adalah widget — BoxDecoration, TextStyle, EdgeInsets, dan LinearGradient adalah objek konfigurasi yang hanya masuk ke properti.
  • Pecah build() yang panjang menjadi widget kustom demi keterbacaan, penggunaan ulang, dan performa.
  • Semua properti widget harus final, sehingga konstruktornya bisa const — dan widget const dilewati saat pembangunan ulang.
  • Pisahkan berkas ke models/, screens/, widgets/, dan utils/ sebelum main.dart menjadi terlalu besar untuk dinavigasi.

Berikutnya: Bab 4 — Layout: Menyusun Antarmuka, tempat kita membahas seluruh perkakas untuk mengatur posisi dan ukuran.

Transkrip asli

Disintesis dari 1_flutter_core/2_basics.md (video 8–25) dan 1_flutter_zero-to-hero/1_key-concepts.md serta 3_master-layout.md. Lihat PDF Flutter Core dan PDF Zero to Hero.

Rangkuman pembelajaran pribadi, disusun ulang dari beberapa kursus Flutter.