ข้ามไปยังเนื้อหา

Constraints & Layout

ถ้าคุณจำเรื่อง layout ของ Flutter ได้แค่อย่างเดียว จำอันนี้:

Constraints ลงล่าง size ขึ้นบน parent วางตำแหน่ง

layout คือ pass เดียวที่ไล่ลง tree แล้วกลับขึ้นมา parent ส่ง constraints ให้ child แต่ละตัว — เป็น BoxConstraints ที่มี min/max ของ width และ height child แต่ละตัวต้องเลือก size ที่อยู่ในขอบเขตนั้นแล้วรายงานกลับขึ้นไป จากนั้น parent จึง วางตำแหน่ง child ลงในพื้นที่ของตัวเอง

flowchart TB
  parent["Parent widget"] -->|"constraints:
min/max w, min/max h"| child["Child widget"]
  child -->|"chosen size
(within constraints)"| parent
  parent -->|"offset
(where to place child)"| placed["Child placed"]
layout pass เดียว: ลงไปพร้อม constraints ขึ้นมาพร้อม size

นั่นคือทั้งหมดของ algorithm ที่เหลือเป็นแค่รายละเอียดว่า widget แต่ละตัวเลือก size อย่างไร และส่ง constraints แบบไหน ต่อลงไป

Center(
child: Container(
width: 100,
height: 100,
color: Colors.blue,
),
)

อ่านจากบนลงล่าง:

  1. Center รับ constraints จาก parent (สมมติว่าเป็นทั้งหน้าจอ: ขนาดเท่าหน้าจอพอดี)
  2. Center ส่ง constraints แบบ loose ให้ child — “เธอจะขนาดเท่าไรก็ได้ตั้งแต่ 0 จนถึงเท่าหน้าจอ”
  3. Container อยากมีขนาด 100x100 พอดี ซึ่งอยู่ในขอบเขต จึง size เป็น 100x100 แล้วรายงานขึ้นไป
  4. Center ใหญ่เท่าหน้าจอ แล้ววาง child 100x100 ไว้ตรงกลาง

เปลี่ยน Center เป็น Align, Padding หรือ SizedBox มีแค่ขั้นที่ 2 เท่านั้นที่เปลี่ยน — คือ constraints ที่ส่งลงไป กฎไม่เคยเปลี่ยน

constraint เป็น tight เมื่อ min เท่ากับ max (widget ต้องมีขนาดนั้นเป๊ะ) และเป็น loose เมื่อ min เป็น 0 (widget จะเป็นขนาดเท่าไรก็ได้จนถึง max)

// SizedBox บังคับ constraint แบบ tight — child ต้องเป็น 50x50
SizedBox(width: 50, height: 50, child: coloredBox)
// Center บังคับ constraint แบบ loose — child จะใหญ่ได้ถึงขนาด parent
Center(child: coloredBox)

นี่คือเหตุผลที่ widget เดียวกันทำงานต่างกันภายใต้ parent ต่างกัน: Container(color: Colors.red) ที่ไม่ระบุ size จะเต็ม parent ภายใต้ constraint แบบ tight แต่หดจนหายภายใต้แบบ loose widget ไม่ได้เปลี่ยน — constraints ต่างหากที่เปลี่ยน

widget บางตัวส่ง constraints แบบ unbounded — max เป็น infinity — แปลว่า “เธอจะใหญ่แค่ไหนก็ได้” ListView แนวตั้งเช่น ให้ child มี height แบบ unbounded (ทิศทาง scroll ยาวได้ไม่จำกัด)

เอา Column ไปใส่ใน ListView นั้นแล้วคุณจะเจอ error คลาสสิก: Column พยายามสูงเท่าที่ constraints ยอมให้ แต่ constraint เป็น infinite จึงเลือก height ที่จำกัดไม่ได้

ListView(
children: [
Column( // ❌ RenderFlex: unbounded height
children: [ Text('a'), Text('b') ],
),
],
)

วิธีแก้คือให้ child มี height ที่ bounded — wrap child ไว้, ใช้ shrinkWrap หรือปรับโครงสร้างให้ตัว scrollable เป็น widget ชั้นนอก พออ่าน constraints เป็น error พวกนี้อ่านเหมือนประโยคภาษาคน ไม่ใช่ปริศนา

กฎหลักของ layout ใน Flutter คืออะไร?
"tight" constraint คืออะไร?
ทำไมเอา Column ไปใส่ใน ListView แนวตั้งตรง ๆ ถึง error?
ทำไม Container ที่ไม่ระบุ size ถึงเต็ม parent หนึ่ง แต่หดภายใต้อีก parent?