Layout #

Sistem layout Flutter dibangun di atas satu prinsip yang konsisten: constraints go down, sizes go up, parent sets position. Semua widget layout — Row, Column, Stack, Expanded, Flexible — adalah ekspresi dari prinsip ini. Memahami cara kerjanya dari dalam membuat kamu mampu memprediksi perilaku layout dengan tepat, bukan menebak-nebak.

Constraint System — Fondasi Segalanya #

Setiap widget di Flutter menerima BoxConstraints dari parent-nya: batas minimum dan maksimum lebar serta tinggi yang diizinkan. Widget kemudian memilih ukurannya sendiri dalam batas tersebut.

Parent memberikan constraints ke child:
  BoxConstraints(
    minWidth: 0,    maxWidth: 360,
    minHeight: 0,   maxHeight: 640,
  )

Child memilih ukuran dalam batas itu:
  Size(width: 200, height: 100)  // harus dalam range constraints

Parent menetapkan posisi child:
  Offset(x: 80, y: 270)  // child tidak bisa memilih posisinya sendiri

Tiga Kategori RenderBox #

Dalam Flutter, widget dirender oleh RenderBox yang menentukan cara menangani constraints yang diterima. Ada tiga kategori: widget yang mencoba sebesar mungkin (seperti Center dan ListView), widget yang mencoba sama ukurannya dengan children (seperti Transform dan Opacity), dan widget yang mencoba ukuran tertentu (seperti Image dan Text).

// 1. Mencoba sebesar mungkin (greedy)
Center(child: ...)        // ambil semua ruang yang tersedia
ColoredBox(color: red)    // ambil semua ruang constraints

// 2. Mengikuti ukuran child (wrap content)
Padding(padding: ..., child: ...)   // ukuran = child + padding
Transform.rotate(...)               // ukuran = child

// 3. Ukuran spesifik (fixed)
SizedBox(width: 100, height: 50)   // tepat 100x50
Image.asset('...')                  // ukuran dari gambar

Tight vs Loose Constraints #

Tight constraint: min == max
  BoxConstraints.tight(Size(360, 640))
  --> child DIPAKSA berukuran 360x640
  --> Contoh: Screen memberikan constraint ke MaterialApp

Loose constraint: min < max (biasanya min == 0)
  BoxConstraints(maxWidth: 360, maxHeight: 640)
  --> child BEBAS memilih ukuran hingga 360x640
  --> Contoh: Column memberikan constraint ke child-nya

Column dan Row — Layout Linear #

Column dan Row adalah widget layout paling dasar — menyusun children secara vertikal dan horizontal. Keduanya adalah implementasi dari Flex dan berbagi API yang hampir identik.

Column (vertikal):          Row (horizontal):

  ┌───────────┐               ┌──┬──┬──┐
  │  Child 1  │               │1 │2 │3 │
  ├───────────┤               └──┴──┴──┘
  │  Child 2  │
  ├───────────┤             main axis  → horizontal
  │  Child 3  │             cross axis ↕ vertical
  └───────────┘
  main axis  ↕ vertikal
  cross axis → horizontal

MainAxisAlignment #

Mengontrol distribusi children di sepanjang main axis:

Column(
  mainAxisAlignment: MainAxisAlignment.start,      // default: atas
  // .center    -- tengah
  // .end       -- bawah
  // .spaceBetween  -- jarak merata, ujung menempel tepi
  // .spaceAround   -- jarak merata, ujung setengah jarak
  // .spaceEvenly   -- jarak sama rata termasuk ujung
  children: [...],
)
spaceBetween:  [A]──────[B]──────[C]
spaceAround:   ──[A]────[B]────[C]──
spaceEvenly:   ───[A]───[B]───[C]───

CrossAxisAlignment #

Mengontrol penyelarasan children di cross axis:

Row(
  crossAxisAlignment: CrossAxisAlignment.center,   // default: tengah
  // .start     -- atas (untuk Row) / kiri (untuk Column)
  // .end       -- bawah / kanan
  // .stretch   -- rentangkan penuh di cross axis
  // .baseline  -- sejajarkan baseline teks
  children: [...],
)

MainAxisSize #

Secara default, Row atau Column menggunakan ruang sebanyak mungkin di sepanjang main axis. Namun jika ingin mengemas children sedekat mungkin, atur mainAxisSize ke MainAxisSize.min.

// Default: .max -- ambil semua ruang di main axis
Column(
  mainAxisSize: MainAxisSize.max,   // Column setinggi parent
  children: [...],
)

// .min -- hanya sebesar children (wrap content)
Column(
  mainAxisSize: MainAxisSize.min,   // Column setinggi total children
  children: [...],
)

Expanded dan Flexible — Membagi Ruang Sisa #

Ketika ada ruang sisa di dalam Row atau Column, Expanded dan Flexible mengontrol bagaimana children membagi ruang tersebut.

Expanded — Isi Semua Ruang Sisa #

Expanded membuat child dari Row, Column, atau Flex mengambil semua ruang tersedia di sepanjang main axis. Jika beberapa children di-expand, ruang yang tersedia dibagi berdasarkan flex factor.

Row(
  children: [
    // Widget tanpa Expanded -- ukuran intrinsiknya
    const Icon(Icons.star),                   // ~24x24

    // Expanded -- ambil SEMUA ruang sisa
    Expanded(
      child: Text('Nama yang mungkin panjang'),
    ),

    // Expanded dengan flex factor
    Expanded(
      flex: 2,  // dua kali lipat dari Expanded lain
      child: Container(color: Colors.blue),
    ),
  ],
)
Sebelum Expanded:
  [Icon 24px][ruang sisa: 300px]

Dengan satu Expanded:
  [Icon 24px][──────── Text 300px ────────]

Dengan dua Expanded (flex: 1 dan 2):
  [Icon 24px][── Text 100px ──][──── Container 200px ────]
             flex: 1             flex: 2 (2x lipat)

Flexible — Ruang Sisa tapi Tidak Dipaksa #

Flexible memberikan child fleksibilitas untuk mengembang mengisi ruang tersedia di main axis, tapi berbeda dari Expanded, Flexible tidak memaksa child mengisi ruang tersebut.

Row(
  children: [
    // Expanded: DIPAKSA mengisi ruang yang dialokasikan
    Expanded(
      child: Text('Teks ini akan mengisi penuh ruang Expanded'),
    ),

    // Flexible: boleh lebih kecil dari ruang yang dialokasikan
    Flexible(
      child: Text('Teks pendek'),  // hanya selebar teksnya
    ),
  ],
)

Expanded vs Flexible — Kapan Memilih? #

EXPANDED (FlexFit.tight):
  ✓ Ingin widget selalu mengisi ruang yang dialokasikan
  ✓ Ingin membagi ruang secara proporsional
  ✗ Jangan jika konten bisa lebih kecil dari ruang yang tersedia

FLEXIBLE (FlexFit.loose):
  ✓ Ingin widget bisa lebih kecil dari ruang alokasi
  ✓ Konten bisa bervariasi ukurannya
  ✗ Tidak untuk pembagian ruang yang ketat dan proporsional

Spacer — Ruang Kosong Fleksibel #

Spacer adalah shorthand untuk Expanded(child: SizedBox.shrink()) — widget tak terlihat yang mengisi ruang sisa:

Row(
  children: [
    const Text('Kiri'),
    const Spacer(),            // dorong ke kanan
    const Text('Kanan'),
  ],
)

Row(
  children: [
    const Text('A'),
    const Spacer(flex: 2),    // dua kali ruang dari Spacer lain
    const Text('B'),
    const Spacer(),
    const Text('C'),
  ],
)
// A ──────────── B ────── C
//    (2x)           (1x)

Stack — Layout Bertumpuk #

Stack menyusun children secara berlapis — children berikutnya di-render di atas children sebelumnya. Berguna untuk overlay, badge, watermark, dan efek visual.

Stack(
  alignment: Alignment.center,   // alignment default untuk non-Positioned children
  children: [
    // Child pertama: paling belakang
    Image.asset('background.jpg', fit: BoxFit.cover),

    // Non-Positioned: mengikuti alignment Stack
    const Text('Teks di tengah'),

    // Positioned: posisi absolut dari tepi Stack
    const Positioned(
      top: 16,
      right: 16,
      child: Badge(label: Text('3')),
    ),

    // Positioned.fill: isi seluruh Stack
    Positioned.fill(
      child: Container(
        color: Colors.black.withOpacity(0.3),
      ),
    ),
  ],
)

Stack Sizing #

// StackFit.loose (default): Stack mengikuti ukuran terbesar non-Positioned child
Stack(
  fit: StackFit.loose,
  children: [...],
)

// StackFit.expand: Stack mengambil constraint yang diberikan parent
// Semua non-Positioned children dipaksa mengisi Stack
Stack(
  fit: StackFit.expand,
  children: [
    Image.asset('bg.jpg', fit: BoxFit.cover),   // isi penuh
    const Align(
      alignment: Alignment.bottomCenter,
      child: Padding(
        padding: EdgeInsets.all(16),
        child: Text('Caption'),
      ),
    ),
  ],
)

Alignment — Positioning di Dalam Widget #

Alignment menggunakan sistem koordinat dari (-1, -1) di pojok kiri atas hingga (1, 1) di pojok kanan bawah, dengan (0, 0) di tengah:

(-1,-1) ──────── (0,-1) ──────── (1,-1)
   |                 |                |
(-1, 0) ──────── (0, 0) ──────── (1, 0)
   |                 |                |
(-1, 1) ──────── (0, 1) ──────── (1, 1)

Alignment.topLeft     = Alignment(-1, -1)
Alignment.center      = Alignment(0, 0)
Alignment.bottomRight = Alignment(1, 1)
// Align -- posisikan satu child di dalam parent
Container(
  width: 200,
  height: 200,
  color: Colors.grey,
  child: const Align(
    alignment: Alignment.bottomRight,
    child: Text('Pojok kanan bawah'),
  ),
)

// Center -- shorthand untuk Align(alignment: Alignment.center)
Center(child: const Text('Tengah'))

// FractionalOffset -- sistem koordinat (0,0) di kiri atas, (1,1) di kanan bawah
const FractionalOffset(0.5, 0.5)  // sama dengan Alignment.center

Unbounded Constraints — Error yang Paling Umum #

Situasi paling umum di mana render box mendapat unbounded constraint adalah di dalam flex box (Row atau Column) dan di dalam scrollable region (seperti ListView). ListView, misalnya, mencoba mengisi ruang tersedia di cross direction-nya. Jika kamu meletakkan ListView vertikal di dalam ListView horizontal, inner list mencoba selebar mungkin, yang berarti lebar tak terbatas.

// ERROR KLASIK: Column di dalam Column tanpa constraint
Column(
  children: [
    Column(  // ERROR! child Column mendapat unbounded height
      children: [
        const Text('A'),
        const Text('B'),
      ],
    ),
  ],
)

// SOLUSI 1: bungkus dengan Expanded (jika di dalam Flex)
Column(
  children: [
    Expanded(
      child: Column(
        children: [
          const Text('A'),
          const Text('B'),
        ],
      ),
    ),
  ],
)

// SOLUSI 2: gunakan mainAxisSize.min
Column(
  children: [
    Column(
      mainAxisSize: MainAxisSize.min,  // wrap content, bukan expanded
      children: [
        const Text('A'),
        const Text('B'),
      ],
    ),
  ],
)

// ERROR KLASIK 2: Expanded di dalam ListView
ListView(
  children: [
    Expanded(  // ERROR! ListView sudah unbounded
      child: Container(height: 100),
    ),
  ],
)

// SOLUSI: gunakan SizedBox dengan ukuran spesifik
ListView(
  children: [
    SizedBox(
      height: 100,
      child: Container(color: Colors.blue),
    ),
  ],
)

Widget Sizing Lainnya #

SizedBox — Ukuran Tetap #

// Ukuran spesifik
const SizedBox(width: 100, height: 50, child: Text('Isi'))

// Hanya lebar atau tinggi
const SizedBox(width: 16)   // spacer horizontal
const SizedBox(height: 24)  // spacer vertikal

// Paksa child mengisi parent
SizedBox.expand(child: ...)

// Widget kosong -- tidak mengambil ruang
const SizedBox.shrink()

ConstrainedBox — Batasan Tambahan #

// Tambahkan constraint di atas constraint yang sudah ada
ConstrainedBox(
  constraints: const BoxConstraints(
    minWidth: 100,
    maxWidth: 300,
    minHeight: 50,
  ),
  child: const Text('Teks dengan constraint'),
)

// Ukuran yang diperluas ke parent jika memungkinkan
ConstrainedBox(
  constraints: const BoxConstraints.expand(),
  child: Container(color: Colors.red),
)

IntrinsicHeight dan IntrinsicWidth #

// Masalah: Row dengan children berbeda tinggi, ingin semua sama tinggi
Row(
  children: [
    Container(height: 100, color: Colors.blue),
    Container(height: 200, color: Colors.red),   // tinggi beda!
    Container(color: Colors.green),               // tinggi mengikuti apa?
  ],
)

// Solusi: IntrinsicHeight -- samakan tinggi berdasarkan child tertinggi
IntrinsicHeight(
  child: Row(
    crossAxisAlignment: CrossAxisAlignment.stretch, // perluas ke tinggi max
    children: [
      Container(height: 100, color: Colors.blue),
      Container(height: 200, color: Colors.red),
      Container(color: Colors.green),              // ikut 200px (tertinggi)
    ],
  ),
)
IntrinsicHeight dan IntrinsicWidth mahal secara performa. Mereka melakukan dua pass layout — pertama untuk menghitung ukuran intrinsik semua children, kemudian baru melakukan layout normal. Gunakan hanya jika benar-benar dibutuhkan dan tidak ada solusi lain.

Ringkasan #

  • Sistem layout Flutter berdasarkan tiga aturan: constraints go down (parent memberikan batas ke child), sizes go up (child menentukan ukuran dalam batas), parent sets position (child tidak bisa memilih posisinya).
  • Column dan Row menyusun children secara vertikal/horizontal. Gunakan mainAxisAlignment untuk distribusi di main axis dan crossAxisAlignment untuk penyelarasan di cross axis.
  • Expanded memaksa child mengisi semua ruang yang dialokasikan (FlexFit.tight). Flexible memperbolehkan child lebih kecil dari ruang alokasi (FlexFit.loose). Spacer adalah Expanded kosong yang mendorong sibling ke ujung.
  • Stack menyusun children berlapis. Gunakan Positioned untuk posisi absolut, Alignment untuk non-Positioned children.
  • Unbounded constraints terjadi saat Expanded atau widget greedy berada di dalam scrollable — solusinya dengan SizedBox berukuran spesifik atau mainAxisSize: MainAxisSize.min.
  • IntrinsicHeight/IntrinsicWidth menyamakan ukuran children tapi mahal secara performa — gunakan hanya jika tidak ada alternatif.

← Sebelumnya: InheritedWidget   Berikutnya: Scrolling →

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