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

Widgets and the Tree

Flutter สร้าง UI ด้วยการ nest widget ไม่ใช่ subclass คุณไม่ extend Button เพื่อเพิ่ม padding — แต่ wrap ด้วย Padding หนึ่งหน้าจอคือ tree ที่ลึกของ widget เล็ก ๆ ที่ทำหน้าที่เดียว ประกอบเข้าด้วยกัน

Widget build(BuildContext context) {
return Center(
child: Padding(
padding: const EdgeInsets.all(16),
child: Column(
children: [
const Text('Hello'),
ElevatedButton(onPressed: () {}, child: const Text('Tap')),
],
),
),
);
}

นี่คือ composition over inheritance แบบสุดโต่ง: widget เล็ก ๆ จำนวนมาก แต่ละตัวทำสิ่งเดียว nest กันจนเป็นภาพรวม Center, Padding, Column ไม่ใช่ของประดับ — widget เหล่านี้ คือ layout

นี่คือ model ที่อธิบายเกือบทุกอย่างเกี่ยวกับ behavior และ performance ของ Flutter ไม่ได้มี tree เดียว — มี สาม tree ที่ถูก sync ให้ตรงกัน:

flowchart LR
  subgraph W["Widget tree"]
    w1["Padding"] --> w2["Text"]
  end
  subgraph E["Element tree"]
    e1["PaddingElement
(อยู่ต่อเนื่อง)"] --> e2["TextElement
(ถือ state)"]
  end
  subgraph R["Render tree"]
    r1["RenderPadding
(layout)"] --> r2["RenderParagraph
(paint)"]
  end
  W -.creates.-> E
  E -.creates.-> R
Widget, Element และ Render tree ทำงานคู่ขนานกัน
  • Widget — description ที่ immutable ที่คุณเขียน rebuild ตลอดและทิ้งไป ถูก
  • Elementliving instance ของ widget ณ ตำแหน่งหนึ่งใน tree element อยู่ต่อเนื่องข้าม rebuild, ถือ State ของ stateful widget และตัดสินว่า widget ใหม่จะ update element นี้ได้ หรือต้องสร้างใหม่แทน
  • RenderObject — ทำ layout, paint และ hit-testing จริง นี่คือ layer ที่แพง Flutter จึงพยายามอย่างหนักที่จะนำ render object กลับมาใช้ซ้ำ

เมื่อคุณ rebuild Flutter จะเดิน widget tree ใหม่และเทียบ widget ใหม่แต่ละตัวกับ element ที่มีอยู่ ถ้า widget ใหม่มี type และ key เดียวกัน กับตัวเก่า element จะถูก update ในที่เดิม — render object ถูกใช้ซ้ำ อัปเดตเฉพาะ property ที่เปลี่ยน เฉพาะเมื่อ type หรือ key ต่างเท่านั้น Flutter จึงรื้อ element กับ render object แล้ว build ใหม่

// การ rebuild สร้าง Text widget ใหม่ทุกครั้ง...
Text(counter.toString())
// ...แต่ TextElement และ RenderParagraph ข้างล่างถูกใช้ซ้ำ
// มีแค่ string ที่เปลี่ยนถูกส่งลงไป นี่คือเหตุผลที่ setState ถูก

การ reconciliation นี้ — widget ใหม่ที่ immutable ถูก diff กับ element ที่อยู่ต่อเนื่อง — คือห้องเครื่องของ Flutter Key (บทเรียนถัดไป) มีไว้ควบคุมการ reconcile นี้เมื่อการ match ด้วย type แบบ default ทำผิด

Flutter เพิ่ม behavior อย่าง padding ให้ widget อย่างไร?
tree ไหนในสาม tree ที่อยู่ต่อเนื่องข้าม rebuild และถือ State?
เมื่อไร Flutter นำ element (และ render object) กลับมาใช้ซ้ำระหว่าง rebuild?
layer ไหนที่ทำ layout และ paint จริง?