Three Trees
Flutter ใช้ 3 trees ทำงานภายใน — เข้าใจสิ่งนี้ทำให้เข้าใจว่าทำไม setState() ถึงเร็ว และทำไม Keys ถึงสำคัญ
3 Trees คืออะไร
Widget Tree Element Tree Render Tree
(โค้ดที่เราเขียน) (Flutter จัดการ) (วาดจริงบนจอ)
1. Widget Tree — สิ่งที่เราเขียน
- Immutable — ถูกสร้างใหม่ทุกครั้งที่
setState() - เป็นแค่ configuration/blueprint บอกว่า UI ควรหน้าตายังไง
- ถูก (cheap) ในการสร้าง — Flutter ออกแบบให้สร้างใหม่ได้บ่อยๆ
2. Element Tree — ตัวกลาง
- Flutter จัดการเอง — ไม่ถูกสร้างใหม่ ทุกครั้ง
- จับคู่ (match) Widget กับ Render Object
- เมื่อ Widget Tree สร้างใหม่ → Element Tree เปรียบเทียบ กับ Widget ใหม่ แล้วอัปเดตเฉพาะส่วนที่เปลี่ยน
3. Render Tree — สิ่งที่วาดจริง
- วาด pixel จริงบนหน้าจอ
- แก้เฉพาะส่วนที่เปลี่ยน — ไม่ redraw ทั้งจอ
- แพง (expensive) ในการสร้าง — Flutter จึง reuse ให้มากที่สุด
Flow เมื่อ setState()
setState() ถูกเรียก
↓
Widget Tree สร้างใหม่ทั้งต้น (ราคาถูก)
↓
Element Tree เปรียบเทียบ Widget ใหม่ กับ Widget เก่า
↓
ส่วนที่เหมือนกัน → reuse Element + Render Object เดิม
ส่วนที่ต่าง → อัปเดต Render Object
↓
จอแสดงผลเฉพาะส่วนที่เปลี่ยน
ผลลัพธ์: แม้ build() จะทำงานบ่อย แต่จอจริงอัปเดตแค่ส่วนน้อย → เร็ว
Widget Optimization — StatefulWidget ให้เล็กที่สุด
setState() เรียก build() ของ ทั้ง Widget → ถ้า StatefulWidget ใหญ่ จะ rebuild ลูกทุกตัวโดยไม่จำเป็น
ก่อน (rebuild ทั้งหมด): หลัง (rebuild แค่ส่วนที่มี state):
BigStatefulWidget BigStatelessWidget
├── Title ├── Title
├── Description ├── Description
├── Button ← state อยู่ตรงนี้ └── DemoButtons (StatefulWidget เล็กๆ)
└── ConditionalText ├── Button
└── ConditionalText
วิธีทำ: ย้าย state ไป Widget ลูกแยก → เปลี่ยน parent เป็น StatelessWidget → rebuild แค่ลูกตัวเล็ก
Keys — ช่วย Element Tree จับคู่ถูกต้อง
State ผูกกับ Element ไม่ใช่ Widget
State object ไม่ได้ผูกกับ Widget โดยตรง — แต่ ผูกกับ Element ที่เชื่อมไปยัง Widget อีกที
Flutter จับคู่ Widget กับ Element ด้วย type + ตำแหน่ง → ถ้า list reorder แล้ว type เดิม ตำแหน่งเดิม → Element (และ State) อยู่ที่เดิม แต่ Widget สลับไปแล้ว:
ก่อน reorder:
Widget A ←→ Element 0 ←→ State A (checked ✅)
Widget B ←→ Element 1 ←→ State B
Widget C ←→ Element 2 ←→ State C
หลัง reorder (ไม่มี Key):
Widget C ←→ Element 0 ←→ State A (checked ✅) ❌ C ได้ state ของ A!
Widget B ←→ Element 1 ←→ State B
Widget A ←→ Element 2 ←→ State C
แก้ด้วย Key
Key = ป้ายชื่อ ให้ Flutter จับคู่ด้วย type + key แทน type + ตำแหน่ง:
CheckableTodoItem(
key: ValueKey(todo.text), // ← Key ช่วยจับคู่ถูกต้อง
todo: todo,
)หลัง reorder (มี Key):
Widget C (key:C) ←→ Element C ←→ State C ✅
Widget B (key:B) ←→ Element B ←→ State B ✅
Widget A (key:A) ←→ Element A ←→ State A ✅ (checked ย้ายมาด้วย!)
ValueKey vs ObjectKey
ValueKey(todo.id) // ใช้ค่าเดียว — เบากว่า (แนะนำ)
ObjectKey(todo) // ใช้ทั้ง object — หนักกว่าค่าที่ใส่ต้อง unique และ ผูกกับ data โดยตรง (ห้ามใช้ random ที่เปลี่ยนทุกครั้ง)
เมื่อไหร่ต้องใส่ Key
- StatefulWidget ใน list ที่มีการเพิ่ม/ลบ/เรียงใหม่ → ต้องใส่
- StatelessWidget ใน list → ไม่จำเป็น (ไม่มี state ให้สับสน)
- Widget ที่ไม่อยู่ใน list → ไม่จำเป็น
super.keyควรรับใน constructor ทุก Widget → พร้อมใส่ Key ได้ทุกเมื่อ
var vs final vs const (มุมมอง Performance)
var | final | const | |
|---|---|---|---|
| Widget Tree | สร้างใหม่ทุกครั้ง | สร้างใหม่ทุกครั้ง | สร้างครั้งเดียว reuse |
| Performance | ปกติ | ปกติ | ดีที่สุด |
const widget ไม่ถูกสร้างใหม่เมื่อ setState() → ประหยัดทั้ง memory และ CPU
Key Points
- Flutter ใช้ 3 trees: Widget (blueprint), Element (ตัวกลาง), Render (วาดจริง)
setState()สร้าง Widget Tree ใหม่ แต่ reuse Element + Render Tree → เร็ว- State ผูกกับ Element ไม่ใช่ Widget — เวลา reorder ถ้าไม่มี Key state จะผิดตัว
- Keys ช่วย Element Tree จับคู่ Widget ถูกตัวเมื่อ list เปลี่ยน (
ValueKeyเบากว่าObjectKey) - StatefulWidget ควรเล็กที่สุด — extract ส่วนที่มี state ออกเป็น Widget แยก
constwidgets ถูก reuse โดยสมบูรณ์ → performance ดีที่สุด
Related
- Flutter — framework ที่ใช้ Three Trees
- Widgets — Widget Tree คือ tree ของ Widgets
- State Management — setState triggers Widget Tree rebuild