10 topik besar yang mengubahmu dari "bisa pakai widget" menjadi "bisa menata UI seperti aplikasi Play Store".
Di Bab 5 kamu sudah kenal widget (Text, Icon, Container). Tapi widget sendirian itu seperti perabot yang berserakan. Layout adalah seni menata widget-widget itu agar rapi, enak dilihat, dan berfungsi.
Bayangkan kamu membangun rumah. Setiap ruangan = widget. Cara kamu menata ruangan (mana di kiri, mana di atas, mana yang lebih luas) = layout. Tanpa denah, rumah jadi berantakan!
Di Flutter, layout dilakukan dengan widget layout khusus yang membungkus widget lain:
Row menyusun widget anak-anaknya dari kiri ke kanan dalam satu baris horizontal.
Penonton duduk berjajar dari kiri ke kanan dalam satu baris. Tidak ada yang menumpuk — semua sejajar mendatar. Itulah Row.
Row(
mainAxisAlignment: MainAxisAlignment.center, // perataan horizontal
children: [
Icon(Icons.star, color: Colors.amber),
SizedBox(width: 12), // jarak antar anak
Text('Bintang 5'),
SizedBox(width: 12),
Icon(Icons.favorite, color: Colors.red),
],
)
Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween, // dorong ke 2 ujung
children: [
Text('Rp 25.000',
style: TextStyle(fontSize: 18, fontWeight: FontWeight.bold)),
Row( // Row bisa bersarang di dalam Row
children: const [
Icon(Icons.star, size: 16, color: Colors.amber),
SizedBox(width: 4),
Text('4.8'),
],
),
],
)
Column menyusun widget anak-anaknya dari atas ke bawah dalam satu kolom vertikal.
Buku ditumpuk satu per satu dari bawah ke atas di rak. Setiap buku punya posisi vertikalnya sendiri, tidak berjajar ke samping. Itulah Column.
Column(
mainAxisAlignment: MainAxisAlignment.start, // perataan vertikal (atas)
crossAxisAlignment: CrossAxisAlignment.start, // perataan horizontal (kiri)
children: [
Text('Profil Saya', style: TextStyle(fontSize: 22)),
SizedBox(height: 8),
Text('Pelajar Flutter'),
SizedBox(height: 16),
ElevatedButton(onPressed: () {}, child: Text('Ikuti')),
],
)
Column(
crossAxisAlignment: CrossAxisAlignment.start, // semua rata kiri
children: [
Text('🍔 Burger Spesial',
style: TextStyle(fontSize: 18, fontWeight: FontWeight.bold)),
SizedBox(height: 4),
Text('Daging sapi + keju + saus rahasia',
style: TextStyle(color: Colors.grey)),
SizedBox(height: 12),
Row( // Row di dalam Column
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Text('Rp 30.000', style: TextStyle(color: Colors.amber)),
ElevatedButton(onPressed: () {}, child: Text('+ Keranjang')),
],
),
],
)
Dua "tombol pengatur perataan" pada Row/Column. Coba sendiri di playground interaktif di bawah!
MainAxisAlignment = mengatur barang sepanjang sumbu utama (mis. dirapatkan, diregangkan, diratakan tengah). CrossAxisAlignment = mengatur sumbu menyilang (mis. semua barang rata atas / rata bawah).
Row( mainAxisAlignment: MainAxisAlignment.spaceBetween, // sumbu utama crossAxisAlignment: CrossAxisAlignment.center, // sumbu silang children: [ Box(1), Box(2), Box(3) ], )
Cara membagi ruang sisa di dalam Row/Column secara proporsional.
Tiga orang berbagi meja. Dengan flex: 1, 2, 1 → orang tengah dapat porsi 2× lebih lebar. Expanded memaksa mengisi semua ruang sisa.
Row(
children: [
Expanded(flex: 1, child: Container(color: Colors.purple)),
Expanded(flex: 2, child: Container(color: Colors.amber)), // 2x lebih lebar
Flexible( // boleh lebih kecil dari porsi
fit: FlexFit.loose,
child: Container(color: Colors.orange),
),
],
)
Row(
children: [
Expanded( // kolom pencarian isi semua ruang sisa
child: TextField(
decoration: InputDecoration(hintText: 'Cari produk...'),
),
),
SizedBox(width: 8),
Icon(Icons.filter_list), // tombol filter ukurannya tetap
],
)
Berbeda dari Row/Column yang berjajar, Stack menumpuk widget di atas widget lain — cocok untuk overlay, badge, dan teks di atas gambar.
Seperti kartu yang ditumpuk: kartu paling bawah ditulis pertama, kartu paling atas menutupinya. Positioned menentukan posisi tepat (top/left/right/bottom) tiap layer.
Stack(
children: [
CircleAvatar(radius: 45, child: Text('🦊')), // layer paling bawah
Positioned( // badge online di kanan-bawah
bottom: 4, right: 4,
child: Container(
width: 18, height: 18,
decoration: BoxDecoration(
color: Colors.green,
shape: BoxShape.circle,
border: Border.all(color: Colors.white, width: 3),
),
),
),
],
)
Stack(
children: [
Container( // layer bawah: gambar/gradien
height: 160,
decoration: BoxDecoration(
gradient: LinearGradient(colors: [Colors.purple, Colors.amber]),
borderRadius: BorderRadius.circular(16),
),
),
Positioned( // layer atas: teks di pojok kiri-bawah
left: 16, bottom: 16,
child: Text('Promo Spesial 🔥',
style: TextStyle(color: Colors.white, fontSize: 22,
fontWeight: FontWeight.bold)),
),
],
)
Ketika datamu banyak (kontak, chat, produk), ListView menampilkannya dalam daftar yang bisa di-scroll dengan efisien.
Daftar belanja yang panjang tidak muat di satu layar — kamu menggulungnya ke bawah untuk melihat item berikutnya. ListView.builder membangun item hanya saat akan terlihat (hemat memori).
Gunakan .builder kalau jumlah item banyak atau berasal dari API.
final kontak = ['Andi', 'Budi', 'Citra', 'Dewi'];
ListView.builder(
itemCount: kontak.length,
itemBuilder: (context, index) {
return ListTile(
leading: CircleAvatar(child: Text(kontak[index][0])),
title: Text(kontak[index]),
subtitle: Text('Online'),
trailing: Icon(Icons.chat),
);
},
)
ListView.separated( itemCount: kontak.length, itemBuilder: (context, index) => ListTile(title: Text(kontak[index])), separatorBuilder: (context, index) => Divider(), // pemisah antar item )
Untuk galeri, dashboard, atau katalog produk — GridView menyusun item dalam beberapa kolom sekaligus.
Foto-foto disusun rapi dalam kotak-kotak berbaris dan berkolom — seperti telur di raknya. crossAxisCount menentukan berapa kolom per baris.
GridView.builder(
itemCount: 9,
gridDelegate: SliverGridDelegateWithFixedCrossAxisCount(
crossAxisCount: 3, // 3 kolom per baris
childAspectRatio: 1, // sel berbentuk persegi
mainAxisSpacing: 8,
crossAxisSpacing: 8,
),
itemBuilder: (context, index) {
return Container(
decoration: BoxDecoration(
color: Colors.purple.shade300,
borderRadius: BorderRadius.circular(12),
),
child: Center(child: Text('Foto $index')),
);
},
)
Pernah lihat error kuning-hitam "RenderFlex overflowed"? Ini penyelamatnya saat konten lebih panjang dari layar.
Sebuah Column yang isinya panjang (mis. form pendaftaran) akan "meluber" keluar layar. Bungkus dengan SingleChildScrollView → seperti gulungan kertas yang bisa kamu gulung naik-turun.
Untuk SATU child panjang (mis. Column berisi form). Membangun SEMUA isi sekaligus. Pilih jika item sedikit & tidak repetitif.
Untuk BANYAK item sejenis/berulang. Hanya membangun item yang terlihat → jauh lebih hemat memori untuk data besar.
SingleChildScrollView(
scrollDirection: Axis.vertical, // arah scroll
physics: BouncingScrollPhysics(), // efek pantul ala iOS
child: Column(
children: [
TextField(decoration: InputDecoration(labelText: 'Nama')),
TextField(decoration: InputDecoration(labelText: 'Email')),
TextField(decoration: InputDecoration(labelText: 'Alamat')),
// ... banyak field lain, aman dari overflow
],
),
)
Aplikasi profesional tahu ukuran layarnya. MediaQuery memberi tahu lebar, tinggi, orientasi, dan kerapatan piksel device.
Baju punya label S/M/L/XL agar pas dengan tubuh pemakainya. MediaQuery membaca "ukuran tubuh" device (HP kecil, tablet, desktop) supaya UI bisa menyesuaikan diri.
@override
Widget build(BuildContext context) {
final lebar = MediaQuery.of(context).size.width; // baca lebar layar
final isTablet = lebar > 600;
return Text(
isTablet ? 'Mode Tablet 🖥️' : 'Mode HP 📱',
style: TextStyle(fontSize: isTablet ? 28 : 16),
);
}
LayoutBuilder memberi tahu ukuran ruang yang tersedia (constraints) untuk widget DI dalamnya — bukan seluruh layar.
Sebelum mendesain, arsitek mengukur ruangan dulu. "Kalau ruangan lebar, taruh 3 lemari; kalau sempit, cukup 1." LayoutBuilder mengukur maxWidth lalu memutuskan layout.
Ukuran seluruh layar device. Sama di mana pun widget berada.
Ukuran area lokal tempat widget dipasang. Bisa beda untuk tiap bagian UI.
LayoutBuilder(
builder: (context, constraints) {
if (constraints.maxWidth > 600) {
// ruang lebar -> tampilkan grid 3 kolom
return GridView.count(crossAxisCount: 3, children: items);
} else {
// ruang sempit -> tampilkan daftar 1 kolom
return ListView(children: items);
}
},
)
Satu aplikasi, banyak ukuran layar. UI yang baik menyesuaikan diri dengan elegan — tidak pecah, tidak kosong melompong.
Air selalu pas dengan bentuk wadahnya — gelas, botol, atau mangkuk. UI responsif seperti air: mengisi ruang yang ada dengan pas, di HP maupun tablet.
UI melar/mengecil mulus mengikuti lebar layar (mis. lebar kartu = 30% layar). Tata letak sama, ukuran menyesuaikan.
UI berganti tata letak di titik tertentu (mis. < 600px pakai bottom nav, > 600px pakai side rail). Beda layar, beda struktur.
final lebarLayar = MediaQuery.of(context).size.width;
Container(
width: lebarLayar * 0.3, // selalu 30% dari lebar layar -> ikut mengecil/melar
height: 120,
decoration: BoxDecoration(
color: Colors.purple,
borderRadius: BorderRadius.circular(16),
),
)
Breakpoint = batas lebar tempat layout berubah. Tiga zona umum: mobile, tablet, desktop.
Lintasan lari dibagi zona-zona dengan garis batas. Begitu pelari melewati garis, aturan zona berubah. Breakpoint adalah "garis" tempat layout aplikasi berganti aturan.
Widget pilihLayout(double lebar) {
if (lebar < 600) {
return LayoutMobile(); // 1 kolom + bottom nav
} else if (lebar < 900) {
return LayoutTablet(); // 2 kolom
} else {
return LayoutDesktop(); // 3 kolom + side navigation
}
}
// Dipakai bersama LayoutBuilder atau MediaQuery
LayoutBuilder(
builder: (ctx, c) => pilihLayout(c.maxWidth),
)
Hampir setiap halaman Flutter dibungkus Scaffold. Ia menyediakan "slot" standar: app bar, body, tombol melayang, dan navigasi bawah.
Sebelum mengisi perabot, rumah perlu kerangka: atap, lantai, dinding. Scaffold adalah kerangka layar — kamu tinggal mengisi tiap slotnya.
Scaffold(
appBar: AppBar(title: Text('Beranda')),
body: Center(child: Text('Konten utama di sini')),
floatingActionButton: FloatingActionButton(
onPressed: () {},
child: Icon(Icons.add),
),
bottomNavigationBar: BottomNavigationBar(items: [
BottomNavigationBarItem(icon: Icon(Icons.home), label: 'Home'),
BottomNavigationBarItem(icon: Icon(Icons.person), label: 'Profil'),
]),
)
BottomNavigationBar adalah pola navigasi paling umum di aplikasi mobile. Coba klik tab di bawah!
Di mall, papan petunjuk membantumu pindah area: Food Court, Bioskop, Toko. Bottom nav melakukan hal sama — pindah antar halaman utama aplikasi.
int _index = 0;
final _halaman = [HalamanHome(), HalamanCari(), HalamanProfil()];
Scaffold(
body: _halaman[_index], // tampilkan halaman aktif
bottomNavigationBar: BottomNavigationBar(
currentIndex: _index,
onTap: (i) => setState(() => _index = i), // ganti tab
items: [
BottomNavigationBarItem(icon: Icon(Icons.home), label: 'Home'),
BottomNavigationBarItem(icon: Icon(Icons.search), label: 'Cari'),
BottomNavigationBarItem(icon: Icon(Icons.person), label: 'Profil'),
],
),
)
Dua widget Material Design yang membuat daftar terlihat profesional secara instan: kartu dengan bayangan, dan baris siap pakai.
Kartu nama merangkum info penting dalam wadah rapi bertepi & berbayang. Card memberi elevasi (shadow); ListTile menyediakan struktur baris: ikon kiri, judul, subjudul, aksi kanan.
Card(
elevation: 4, // tinggi bayangan
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(14),
),
child: ListTile(
leading: CircleAvatar(child: Icon(Icons.music_note)),
title: Text('Lo-Fi Beats'),
subtitle: Text('Playlist · 42 lagu'),
trailing: Icon(Icons.play_arrow),
onTap: () {},
),
)
Sebelum coding, mari rancang dulu. Inilah widget tree dashboard yang akan kita bangun — gabungan semua konsep Bab 6.
Kode lengkap di kiri, hasil nyata di "HP" sebelah kanan. Inilah hasil menggabungkan semua konsep Bab 6.
Scaffold(
appBar: AppBar(
title: Text('Dashboard'),
actions: [IconButton(icon: Icon(Icons.notifications), onPressed: () {})],
),
body: SingleChildScrollView( // anti-overflow
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
// 1) HERO dengan Stack overlay
Stack(children: [
Container(height: 120, decoration: BoxDecoration(
gradient: LinearGradient(colors: [Colors.purple, Colors.amber]))),
Positioned(left: 16, bottom: 16,
child: Text('Halo, Sahil 👋',
style: TextStyle(color: Colors.white, fontSize: 20))),
]),
// 2) STATS dengan Row + Expanded
Padding(padding: EdgeInsets.all(12), child: Row(children: [
Expanded(child: _kartuStat('Tugas', '12')),
SizedBox(width: 8),
Expanded(child: _kartuStat('Selesai', '8')),
SizedBox(width: 8),
Expanded(child: _kartuStat('Nilai', '92')),
])),
// 3) FEATURED dengan ListView horizontal
SizedBox(height: 90, child: ListView.builder(
scrollDirection: Axis.horizontal,
itemCount: 5,
itemBuilder: (c, i) => _kartuFeatured(i))),
// 4) KATEGORI responsif dengan LayoutBuilder + GridView
LayoutBuilder(builder: (c, cons) {
final kolom = cons.maxWidth > 600 ? 3 : 2;
return GridView.count(
crossAxisCount: kolom, shrinkWrap: true,
physics: NeverScrollableScrollPhysics(),
children: kategori.map(_kartuKategori).toList());
}),
],
),
),
bottomNavigationBar: BottomNavigationBar(items: const [
BottomNavigationBarItem(icon: Icon(Icons.home), label: 'Home'),
BottomNavigationBarItem(icon: Icon(Icons.grid_view), label: 'Menu'),
BottomNavigationBarItem(icon: Icon(Icons.person), label: 'Profil'),
]),
)
Luar biasa! Kamu telah menempuh lompatan besar dari "memakai widget" menjadi "menata UI profesional". Inilah yang sudah kamu kuasai:
Bangun sebuah UI kartu profil pengguna dengan menggabungkan Row, Column, dan Stack.
Column(
children: [
// TODO 1: Bungkus CircleAvatar dengan Stack + Positioned
// untuk menambahkan badge "Online" di pojok kanan-bawah
Stack(
children: [
CircleAvatar(radius: 50, backgroundColor: Colors.purple),
// TODO: tambahkan Positioned berisi titik hijau di sini
],
),
SizedBox(height: 12),
// TODO 2: Tampilkan nama (besar tebal) dan bio (abu-abu) dengan Column
Text('___'),
SizedBox(height: 20),
// TODO 3: Buat Row 3 statistik, masing-masing dibungkus Expanded
Row(
children: [
// Expanded( child: _stat('Posts', '128') ),
// ...
],
),
],
)
Bangun galeri foto responsif: 3 kolom di layar lebar, 2 kolom di layar sempit, dengan GridView + LayoutBuilder.
LayoutBuilder(
builder: (context, constraints) {
// TODO 1: Tentukan jumlah kolom berdasarkan constraints.maxWidth
final int kolom = /* ___ ? 3 : 2 */ 2;
return GridView.builder(
itemCount: foto.length,
gridDelegate: SliverGridDelegateWithFixedCrossAxisCount(
crossAxisCount: kolom,
childAspectRatio: 0.8,
mainAxisSpacing: 10,
crossAxisSpacing: 10,
),
itemBuilder: (context, index) {
// TODO 2: Kembalikan Card berisi Column [gambar, judul, rating]
return Card(child: Column(children: [ /* ... */ ]));
},
);
},
)
Gabungkan SEMUA konsep Bab 6 menjadi satu aplikasi Dashboard utuh yang responsif.