Widget เป็นแนวคิดหลักของเฟรมเวิร์ก Flutter ที่อธิบายการกำหนดค่าขององค์ประกอบอินเทอร์เฟซผู้ใช้ ทุกคอมโพเนนต์ที่มองเห็นได้ ตั้งแต่ปุ่มจนถึงแอนิเมชันที่ซับซ้อน ล้วนเป็น Widget ต่างจากเฟรมเวิร์กอื่นที่ UI ถูกอธิบายด้วยไฟล์ XML แยกต่างหากหรือถูกวาดแบบ Imperative Flutter สร้างอินเทอร์เฟซผ่านการประกอบ Widget — การรวมองค์ประกอบขนาดเล็กที่แบ่งแยกไม่ได้เข้าเป็นลำดับชั้นแบบต้นไม้ ตาม เอกสาร Flutter (2025) ไลบรารี Flutter SDK มี Widget ในตัวมากกว่า 260 ตัวที่ครอบคลุม Material Design, Cupertino และสไตล์ที่กำหนดเอง
ประเด็นสำคัญ
Widget ใน Flutter คือคำอธิบายที่ไม่สามารถเปลี่ยนแปลงได้ (immutable) ของส่วนหนึ่งของอินเทอร์เฟซผู้ใช้ แต่ละ Widget มีคุณสมบัติการกำหนดค่า: ขนาด, สี, ตำแหน่ง, ข้อความ, ตัวจัดการเหตุการณ์ และ Widget ลูก Widget ไม่ได้ถูกเรนเดอร์โดยตรง — มันเป็นพิมพ์เขียว (blueprint) ที่ Flutter Engine ใช้สร้าง RenderObject ซึ่งเป็นวัตถุกราฟิกจริงบนหน้าจอ
ปรัชญาของ Flutter กล่าวว่า: “Everything is a Widget” ซึ่งหมายความว่าไม่เพียงแต่องค์ประกอบที่มองเห็นได้ (Text, Image, Button) เท่านั้นที่เป็น Widget แต่ยังรวมถึงบล็อกโครงสร้าง (Padding, Center, Column, Stack), บล็อกพฤติกรรม (GestureDetector, AnimatedBuilder) และแม้แต่แอปพลิเคชันเอง (MaterialApp, CupertinoApp) วิธีนี้รับประกันความสอดคล้อง: องค์ประกอบหน้าจอใดๆ สามารถรวมกับองค์ประกอบอื่นใดได้ผ่านการซ้อนอย่างง่าย
ตาม Google I/O 2024 — Flutter Widgets Deep Dive แอปพลิเคชันทั่วไปบน Flutter มี 200 ถึง 1500 Widget ในเวลาใดก็ตาม แม้จะมีจำนวนนี้ Flutter ยังคงรักษา 60 FPS ได้แม้บนอุปกรณ์ราคาประหยัด ต้องขอบคุณการปรับให้เหมาะสมที่ระดับเอนจิน C++ Skia/Impeller Widget เป็นวัตถุที่มีน้ำหนักเบา (40–80 ไบต์ต่อชิ้น) ดังนั้นการสร้างจึงไม่เป็นคอขวดด้านประสิทธิภาพ
import 'package:flutter/material.dart';
void main() {
runApp(const MyApp());
}
class MyApp extends StatelessWidget {
const MyApp({super.key});
@override
Widget build(BuildContext context) {
return const MaterialApp(
home: Scaffold(
body: Center(
child: Text('สวัสดี, Flutter!'),
),
),
);
}
}
เพื่อเข้าใจว่า Widget ทำงานอย่างไร จำเป็นต้องเข้าใจสถาปัตยกรรม Flutter ที่ประกอบด้วยสามต้นไม้ที่เชื่อมต่อกัน ต้นแรก — Widget tree — อธิบายการกำหนดค่า UI เป็นต้นไม้ที่มีน้ำหนักเบาที่สามารถสร้างใหม่ทั้งหมดได้ทุกเฟรม (ตัวเก็บขยะจะลบ Widget เก่าและสร้างใหม่) Widget ไม่สามารถเปลี่ยนแปลงได้ (immutable): หากสีข้อความเปลี่ยนไป จะสร้าง Text Widget ใหม่ที่มีสีใหม่ ส่วนอันเก่าจะถูกทิ้ง
ต้นที่สอง — Element tree — เป็นตัวเชื่อมระหว่าง Widget และ RenderObject Element มีการอ้างอิงไปยัง Widget (การกำหนดค่า) และ RenderObject (การเรนเดอร์) เมื่อ Widget เปลี่ยนแปลง Flutter จะเปรียบเทียบ Widget ใหม่กับ Element เก่าและตัดสินใจ: อัปเดต RenderObject ที่มีอยู่ (หาก Widget เป็นชนิดเดียวกัน) หรือสร้างใหม่ (หากชนิดของ Widget เปลี่ยนไป) กระบวนการนี้เรียกว่า Reconciliation และคล้ายกับ React Virtual DOM
ต้นที่สาม — RenderObject tree — รับผิดชอบการเรนเดอร์จริงบนหน้าจอ RenderObject มีขนาด ตำแหน่ง และวิธีการวาดที่เฉพาะเจาะจง Flutter Engine (C++ Skia หรือ Impeller) จะเดินผ่าน RenderObject tree และเรนเดอร์แต่ละโหนด RenderObject tree เป็นต้นไม้ที่หนักที่สุด ดังนั้น Flutter จึงลดการเปลี่ยนแปลงให้เหลือน้อยที่สุดโดยใช้ RenderObject ซ้ำเมื่อเปลี่ยนไปใช้ Widget ชนิดเดียวกัน
| ต้นไม้ | วัตถุประสงค์ | ไม่เปลี่ยนแปลง? | วงจรชีวิต |
|---|---|---|---|
| Widget | การกำหนดค่า UI (พิมพ์เขียว) | ใช่ | ถูกสร้างใหม่ทุก build |
| Element | การเชื่อมต่อ Widget ↔ RenderObject | ไม่ | มีอยู่ตราบเท่าที่วิดเจ็ตอยู่ในต้นไม้ |
| RenderObject | การเรนเดอร์และเลย์เอาต์ | ไม่ | หนัก ถูกใช้ซ้ำเมื่อเป็นไปได้ |
Flutter แบ่ง Widget ออกเป็นสองชนิดพื้นฐาน: StatelessWidget และ StatefulWidget StatelessWidget คือวิดเจ็ตที่ไม่มีสถานะที่เปลี่ยนแปลงได้ ลักษณะของ StatelessWidget ถูกกำหนดโดยคอนสตรัคเตอร์อย่างสมบูรณ์และไม่สามารถเปลี่ยนแปลงได้หลังจากการเรนเดอร์ ตัวอย่าง: Text, Icon, Divider, Padding คุณสมบัติทั้งหมดของ StatelessWidget ถูกประกาศเป็น final ในคอนสตรัคเตอร์และเป็นแบบอ่านอย่างเดียว
StatefulWidget คือวิดเจ็ตที่มีสถานะที่เปลี่ยนแปลงได้ ประกอบด้วยสองคลาส: Widget เอง (การกำหนดค่าที่ไม่เปลี่ยนแปลง เหมือน StatelessWidget) และ State (สถานะที่เปลี่ยนแปลงได้) การแยก Widget ออกจาก State เป็นการตัดสินใจทางสถาปัตยกรรมที่สำคัญใน Flutter Widget ถูกสร้างใหม่ทุก build แต่วัตถุ State ยังคงอยู่ตลอดวงจรชีวิตของวิดเจ็ตในต้นไม้ โดยคงสถานะไว้
เมื่อเรียก setState() Flutter จะทำเครื่องหมาย State ว่า “สกปรก” และในเฟรมถัดไปจะเรียกเมธอด build() เพื่อสร้างทรีย่อยใหม่ สำคัญ: setState() ไม่ได้สร้าง Widget ใหม่ — มันเพียงเรียก build() บน State ที่มีอยู่ ซึ่งหมายความว่า StatefulWidget สามารถอัปเดต UI โดยไม่สูญเสียสถานะของ Widget ลูก ตราบใดที่คีย์ (Key) ขององค์ประกอบลูกยังคงเสถียร
// StatelessWidget — ลักษณะไม่เปลี่ยนแปลง
class GreetingWidget extends StatelessWidget {
const GreetingWidget({super.key, required this.name});
final String name;
@override
Widget build(BuildContext context) {
return Text('สวัสดี, $name');
}
}
// StatefulWidget — ตัวนับที่มีสถานะเปลี่ยนแปลงได้
class CounterWidget extends StatefulWidget {
const CounterWidget({super.key});
@override
State<CounterWidget> createState() => _CounterWidgetState();
}
class _CounterWidgetState extends State<CounterWidget> {
int _count = 0;
@override
Widget build(BuildContext context) {
return Column(
children: [
Text('จำนวน: $_count'),
ElevatedButton(
onPressed: () => setState(() => _count++),
child: const Text('เพิ่ม'),
),
],
);
}
}
Widget ทั้งหมดใน Flutter สามารถแบ่งออกเป็นสามหมวดหมู่หลักตามวัตถุประสงค์การทำงาน Layout Widget — รับผิดชอบการจัดวางองค์ประกอบลูกบนหน้าจอ Row และ Column จัดเรียงลูกเป็นแถว Stack ซ้อนทับกัน Expanded และ Flexible กระจายพื้นที่ว่าง Layout Widget ไม่มีการแสดงผลทางภาพของตนเอง — จัดการตำแหน่งและขนาดของวิดเจ็ตลูก
Painting Widget — รับผิดชอบการจัดแต่งทรง视觉 Container รวมการตกแต่ง (สี, การไล่ระดับ, เงา, เส้นขอบ) เข้ากับคุณสมบัติเลย์เอาต์ Padding เพิ่มระยะห่าง DecoratedBox วาดพื้นหลัง Transform ใช้การแปลง (หมุน, ปรับขนาด) Painting Widget เป็นบล็อกการสร้างของสไตล์ภาพและมักใช้ร่วมกับ Layout Widget เพื่อให้ได้ลักษณะที่ต้องการ
Interactive Widget — จัดการอินพุตของผู้ใช้ GestureDetector ตรวจจับท่าทาง (แตะ, ปัด, หยิก) InkWell เพิ่มเอฟเฟกต์ระลอกคลื่น Material TextField รับอินพุตข้อความ Slider และ Switch ให้องค์ประกอบควบคุมมาตรฐาน Interactive Widget ยกเหตุการณ์ผ่านฟังก์ชัน callback ที่ส่งไปยังคอนสตรัคเตอร์หรือจัดการผ่านผู้ให้บริการสถานะ
| หมวดหมู่ | ตัวอย่าง Widget | วัตถุประสงค์ |
|---|---|---|
| Layout | Row, Column, Stack, Expanded, Flexible, Align | การจัดวางและขนาดขององค์ประกอบลูก |
| Painting | Container, Padding, DecoratedBox, RotatedBox | สี, พื้นหลัง, เส้นขอบ, เงา, การแปลง |
| Interactive | GestureDetector, InkWell, TextField, Slider | การจัดการสัมผัส, อินพุต, ท่าทาง |
| Platform | MaterialApp, CupertinoApp, Theme, MediaQuery | การรวมแพลตฟอร์ม, ชุดรูปแบบ, การปรับตัว |
| Async | FutureBuilder, StreamBuilder, ValueListenableBuilder | การอัปเดตเชิงโต้ตอบจากข้อมูลอะซิงโครนัส |
ตาม Flutter Widget of the Week (Google, 2025) ชุมชน Flutter ใช้การผสมผสานของ layout + painting + interactive Widget อย่างแข็งขันเพื่อสร้างอินเทอร์เฟซใดๆ ก็ตาม ตัวอย่างเช่น ปุ่ม: InkWell (interactive) + Container (painting) + Text (static) + Padding (layout) ความเป็นโมดูลนี้ช่วยให้สามารถใช้บล็อกมาตรฐานซ้ำในบริบทต่างๆ โดยไม่ต้องทำซ้ำโค้ด
การประกอบ Widget คือกระบวนการสร้าง UI โดยการซ้อน Widget บางส่วนเข้าไปใน Widget อื่น ต่างจากการสืบทอดแบบคลาสสิก (extends) ที่คลาสลูกสืบทอดพฤติกรรมของแม่ Flutter ใช้การรวม: แต่ละ Widget มี Widget อื่นผ่านพารามิเตอร์ child (สำหรับหนึ่ง) หรือ children (สำหรับหลาย) วิธีนี้ให้ความยืดหยุ่นและการใช้ซ้ำที่มากขึ้น
BuildContext เป็นแนวคิดที่สำคัญที่สุดเป็นอันดับสองรองจาก Widget BuildContext เป็นตัวอธิบายตำแหน่งของ Widget ในต้นไม้องค์ประกอบ ผ่าน BuildContext Widget สามารถเข้าถึงวิดเจ็ตบรรพบุรุษ (Theme.of(context), MediaQuery.of(context), Navigator.of(context)) BuildContext ถูกส่งไปยังเมธอด build() และใช้สำหรับโต้ตอบกับองค์ประกอบแม่และลูก แต่ละ Widget มี BuildContext เพียงหนึ่งเดียว ซึ่งระบุตำแหน่งในต้นไม้ได้อย่างไม่ซ้ำกัน
ตาม ภาพรวมสถาปัตยกรรม Flutter (Google, 2025) BuildContext เป็นพื้นฐานของ InheritedWidget — กลไกที่อนุญาตให้ส่งข้อมูลลงไปในต้นไม้โดยไม่ต้องส่งผ่านคอนสตรัคเตอร์อย่างชัดเจน Theme, MediaQuery, Navigator และ Provider ใช้ InheritedWidget ภายใน Widget ที่ซ้อนลึกใดๆ สามารถเข้าถึงข้อมูลบรรพบุรุษผ่าน BuildContext.dependOnInheritedWidgetOfExactType ทำให้ BuildContext เป็นกุญแจสู่สถาปัตยกรรมเชิงโต้ตอบของ Flutter
// การประกอบ Widget ผ่านการซ้อน
Scaffold(
appBar: AppBar(title: const Text('แอปของฉัน')),
body: Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
const Text('ยินดีต้อนรับสู่ Flutter',
style: TextStyle(fontSize: 24)),
const SizedBox(height: 16),
ElevatedButton(
onPressed: () { /* nav */ },
child: const Text('เริ่มต้น'),
),
],
),
),
)
// เข้าถึงชุดรูปแบบผ่าน BuildContext
Text(
'ข้อความที่มีสไตล์',
style: Theme.of(context).textTheme.headlineMedium,
)
ข้อผิดพลาดแรกและพบบ่อยที่สุดคือ การใช้ StatefulWidget ในที่ที่ StatelessWidget เพียงพอ นักพัฒนา Flutter มือใหม่หลายคนสร้าง StatefulWidget สำหรับวิดเจ็ตทั้งหมด แม้ว่าสถานะจะถูกเก็บไว้ในผู้ให้บริการภายนอก (Provider, Riverpod, BLoC) ซึ่งมากเกินไปและทำให้ประสิทธิภาพลดลง กฎ: ใช้ StatelessWidget หากสถานะถูกจัดการจากภายนอกหรือหากวิดเจ็ตไม่มีสถานะที่เปลี่ยนแปลงได้ของตัวเอง
ข้อผิดพลาดที่สอง — การสร้าง Widget ภายในเมธอด build โดยไม่มีคอนสตรัคเตอร์ const Widget แต่ละตัวที่สร้างโดยไม่มี const จะถูกจัดสรรใหม่ทุก build หากคุณสร้าง Widget ด้วยคอนสตรัคเตอร์ const ภายใน build() Flutter สามารถใช้ instance เดิมซ้ำ ลดภาระของตัวเก็บขยะ เพิ่ม const ทุกที่ที่ทำได้ — โดยเฉพาะสำหรับ Text, Icon, SizedBox, Padding และ Widget ที่ไม่มีสถานะอื่นๆ
ข้อผิดพลาดที่สาม — การทำงานกับคีย์ (Key) ที่ไม่ถูกต้อง Flutter ใช้ Key เพื่อระบุ Widget เมื่อสร้างต้นไม้ใหม่ หากรายการ Widget ถูกสร้างใหม่โดยไม่มี Key Flutter อาจสับสนลำดับขององค์ประกอบ ทำให้เกิดแอนิเมชันที่ไม่ถูกต้องหรือสูญเสียสถานะ เพิ่ม Key (เช่น ValueKey หรือ ObjectKey) สำหรับองค์ประกอบในรายการเสมอ โดยเฉพาะเมื่อใช้ ListView.builder กับข้อมูลไดนามิก
คำถามที่พบบ่อย
StatelessWidget — วิดเจ็ตที่ไม่มีสถานะที่เปลี่ยนแปลงได้ ลักษณะถูกกำหนดโดยคอนสตรัคเตอร์อย่างสมบูรณ์ StatefulWidget — วิดเจ็ตที่มีสถานะที่เปลี่ยนแปลงได้ ซึ่งถูกเก็บในวัตถุ State แยกต่างหากและสามารถอัปเดตผ่าน setState() โดยไม่ต้องสร้างวิดเจ็ตใหม่ ใช้ StatelessWidget ทุกที่ที่ทำได้ StatefulWidget — เมื่อต้องการสถานะเฉพาะที่
ความไม่เปลี่ยนแปลง ของ Widget เป็นการตัดสินใจทางสถาปัตยกรรมของ Flutter เพื่อประสิทธิภาพ หาก Widget เปลี่ยนแปลงได้ Flutter จะไม่สามารถเปรียบเทียบการกำหนดค่าเก่าและใหม่อย่างปลอดภัยทุก build ความไม่เปลี่ยนแปลงช่วยให้ Flutter ระบุได้อย่างรวดเร็วว่า Widget เปลี่ยนไปหรือไม่ (ผ่านตัวดำเนินการ ==) และใช้ RenderObject ที่มีอยู่ซ้ำ ลดการดำเนินการเรนเดอร์ที่มีค่าใช้จ่ายสูง
BuildContext เป็นตัวอธิบายตำแหน่งของ Widget ในต้นไม้องค์ประกอบ ผ่านมัน Widget สามารถเข้าถึงวิดเจ็ตบรรพบุรุษ (Theme, MediaQuery, Navigator) และ InheritedWidget BuildContext ยังใช้สำหรับการนำทาง (Navigator.of(context)), การแสดง SnackBar และการโต้ตอบกับ Provider แต่ละ Widget รับ BuildContext ผ่านเมธอด build() และส่งต่อไปยังลูกหลาน
ใช้ Row สำหรับการจัดวางแนวนอนขององค์ประกอบ Column สำหรับการจัดวางแนวตั้ง Stack สำหรับการซ้อนองค์ประกอบทับกัน Row และ Column ทำงานตามหลักการ flexbox: ลูกใช้พื้นที่ตาม mainAxisSize, mainAxisAlignment และ crossAxisAlignment Stack ใช้ลูกที่กำหนดตำแหน่งเพื่อวางตำแหน่งอย่างแม่นยำสัมพันธ์กับขอบหรือศูนย์กลาง
Flutter บรรลุประสิทธิภาพสูงผ่านสามกลไก: (1) Widget มีราคาถูก — วัตถุที่ไม่เปลี่ยนแปลงน้ำหนักเบา (40–80 ไบต์) การสร้างไม่เป็นภาระต่อ GC (2) การใช้ RenderObject ซ้ำ — เมื่อเปลี่ยนไปใช้ Widget ชนิดเดียวกัน RenderObject ถูกใช้ซ้ำ หลีกเลี่ยงการสร้างใหม่ที่มีค่าใช้จ่ายสูง (3) เอนจิน Skia/Impeller — การเรนเดอร์ที่ระดับ C++ ด้วยการเรียกวาดที่ลดลงผ่านขอบเขตการวาดใหม่
สรุป
เราจะพัฒนาแอปพลิเคชันบนมือถือแบบครบวงจร
IT Sectr สร้างแอปพลิเคชัน iOS และ Android สำหรับสตาร์ทอัพและธุรกิจตั้งแต่ปี 2017 เราจะให้คำแนะนำและเสนอวิธีแก้ปัญหาที่ดีที่สุดแก่คุณ
อ่านเพิ่มเติม