ทำความเข้าใจ Widget Lifecycle ใน Flutter
ในโลกของการพัฒนาแอปพลิเคชันด้วย Flutter, Widget คือหัวใจสำคัญที่ใช้สร้าง User Interface (UI) ทุกส่วน ตั้งแต่ปุ่มเล็กๆ ไปจนถึงหน้าจอที่ซับซ้อน การทำความเข้าใจ "Widget Lifecycle" หรือ "วงจรชีวิตของ Widget" เป็นสิ่งสำคัญอย่างยิ่งสำหรับนักพัฒนา Flutter ทุกคน เพราะมันช่วยให้เราสามารถควบคุมการทำงานของ Widget ได้อย่างละเอียด และสร้างแอปพลิเคชันที่มีประสิทธิภาพและตอบสนองต่อผู้ใช้ได้อย่างราบรื่น
ในบทความนี้ เราจะมาเจาะลึกถึง Widget Lifecycle ใน Flutter โดยเน้นไปที่ฟังก์ชันหลักๆ ที่เกี่ยวข้อง ได้แก่ initState, build, didUpdateWidget, และ dispose พร้อมทั้งยกตัวอย่างการใช้งานเพื่อให้เห็นภาพที่ชัดเจนยิ่งขึ้น
Widget Lifecycle คืออะไร?
Widget Lifecycle คือลำดับขั้นตอนที่ Widget แต่ละตัวต้องผ่าน ตั้งแต่ถูกสร้างขึ้น (initialized), แสดงผล (built), อัปเดต (updated) ไปจนถึงถูกทำลาย (disposed) การเข้าใจวงจรชีวิตนี้ ช่วยให้เราสามารถเขียนโค้ดที่ทำงานได้อย่างถูกต้องในแต่ละช่วงเวลา และจัดการทรัพยากรได้อย่างเหมาะสม
Flutter แบ่ง Widget ออกเป็นสองประเภทหลักๆ คือ:
- StatelessWidget: Widget ที่ไม่มี state ภายในตัวเอง (immutable) เมื่อข้อมูลเปลี่ยนแปลง StatelessWidget จะถูกสร้างใหม่ทั้งหมด
- StatefulWidget: Widget ที่มี state ภายในตัวเอง (mutable) สามารถเปลี่ยนแปลงได้ระหว่างการใช้งาน และเมื่อ state เปลี่ยนแปลง StatefulWidget จะทำการ rebuild UI ใหม่
Widget Lifecycle จะมีความสำคัญอย่างยิ่งกับ StatefulWidget เพราะ StatefulWidget จะมี State object ที่จัดการข้อมูลและควบคุมการ rebuild UI
ฟังก์ชันหลักใน Widget Lifecycle
ต่อไปนี้คือฟังก์ชันหลักๆ ที่เราจะพูดถึงในบทความนี้:
initState(): เรียกใช้เพียงครั้งเดียวเมื่อ State object ถูกสร้างขึ้นbuild(): เรียกใช้ทุกครั้งที่ต้องการสร้าง UIdidUpdateWidget(): เรียกใช้เมื่อ Widget เปลี่ยนแปลงและต้องอัปเดต Statedispose(): เรียกใช้เมื่อ State object ถูกลบออกจาก Widget tree
1. initState()
initState() เป็นฟังก์ชันแรกที่ถูกเรียกใช้เมื่อ State object ถูกสร้างขึ้น นี่เป็นจุดที่เหมาะสมในการทำการ initialization ต่างๆ เช่น:
- เริ่มต้นค่าเริ่มต้นของ state variables
- สมัครสมาชิก (subscribe) เพื่อรับข้อมูลจาก streams หรือ listeners
- เรียกใช้งาน asynchronous operations (เช่น ดึงข้อมูลจาก API)
ข้อควรจำ: initState() ถูกเรียกใช้เพียงครั้งเดียวเท่านั้น ดังนั้นจึงเหมาะสำหรับงานที่ต้องการทำเพียงครั้งเดียว
ตัวอย่าง:
class MyStatefulWidget extends StatefulWidget {
@override
_MyStatefulWidgetState createState() => _MyStatefulWidgetState();
}
class _MyStatefulWidgetState extends State<MyStatefulWidget> {
int _counter = 0;
@override
void initState() {
super.initState();
// ทำการ initialization ที่นี่
print("initState() ถูกเรียกใช้");
// ตัวอย่าง: ดึงข้อมูลจาก API (จำลอง)
Future.delayed(Duration(seconds: 2), () {
setState(() {
_counter = 10; // อัปเดต state หลังจากดึงข้อมูลเสร็จ
});
});
}
@override
Widget build(BuildContext context) {
return Text('Counter: $_counter');
}
}
ในตัวอย่างนี้ เราเริ่มต้น _counter เป็น 0 ใน initState() จากนั้นเราจำลองการดึงข้อมูลจาก API โดยใช้ Future.delayed() หลังจาก 2 วินาที เราจะอัปเดต _counter เป็น 10 โดยใช้ setState()
2. build()
build() เป็นฟังก์ชันที่สำคัญที่สุดใน Widget Lifecycle มันมีหน้าที่สร้าง UI ของ Widget โดยอิงตาม state ปัจจุบัน ฟังก์ชันนี้จะถูกเรียกใช้หลายครั้ง:
- เมื่อ Widget ถูกสร้างขึ้นครั้งแรก
- เมื่อ
setState()ถูกเรียกใช้ (เพื่ออัปเดต UI) - เมื่อ dependency ของ Widget เปลี่ยนแปลง (เช่น InheritedWidget)
ข้อควรจำ: build() ควรเป็นฟังก์ชันที่รวดเร็วและมีประสิทธิภาพ เพราะมันถูกเรียกใช้บ่อยครั้ง การคำนวณที่ซับซ้อนควรทำในฟังก์ชันอื่น (เช่น initState() หรือ didUpdateWidget()) และเก็บผลลัพธ์ไว้ใน state variables
ตัวอย่าง: (จากตัวอย่างก่อนหน้า)
@override
Widget build(BuildContext context) {
print("build() ถูกเรียกใช้");
return Text('Counter: $_counter');
}
ในตัวอย่างนี้ build() จะสร้าง Text Widget ที่แสดงค่าของ _counter ทุกครั้งที่ _counter เปลี่ยนแปลง (เนื่องจาก setState() ถูกเรียกใช้) build() จะถูกเรียกใช้อีกครั้งเพื่ออัปเดต UI
3. didUpdateWidget()
didUpdateWidget() ถูกเรียกใช้เมื่อ parent Widget สร้าง Widget นี้ขึ้นมาใหม่ (rebuild) และส่ง Widget ตัวใหม่ที่มี type เดียวกันเข้ามา แต่มี configuration ที่แตกต่างกัน (เช่น properties หรือ arguments ที่ส่งเข้ามา)
ฟังก์ชันนี้ช่วยให้เราสามารถเปรียบเทียบ Widget เก่ากับ Widget ใหม่ และอัปเดต state ตามความเหมาะสม
ข้อควรจำ: didUpdateWidget() ไม่ถูกเรียกใช้เมื่อ Widget ถูกสร้างขึ้นครั้งแรก
ตัวอย่าง:
class MyStatefulWidget extends StatefulWidget {
final String message;
MyStatefulWidget({Key? key, required this.message}) : super(key: key);
@override
_MyStatefulWidgetState createState() => _MyStatefulWidgetState();
}
class _MyStatefulWidgetState extends State<MyStatefulWidget> {
String _currentMessage = "";
@override
void initState() {
super.initState();
_currentMessage = widget.message;
}
@override
void didUpdateWidget(covariant MyStatefulWidget oldWidget) {
super.didUpdateWidget(oldWidget);
if (widget.message != oldWidget.message) {
setState(() {
_currentMessage = widget.message;
print("didUpdateWidget() ถูกเรียกใช้: อัปเดต message เป็น ${widget.message}");
});
}
}
@override
Widget build(BuildContext context) {
return Text('Message: $_currentMessage');
}
}
ในตัวอย่างนี้ MyStatefulWidget รับ message เป็น argument เมื่อ message เปลี่ยนแปลง didUpdateWidget() จะถูกเรียกใช้ เราจะเปรียบเทียบ widget.message กับ oldWidget.message หากแตกต่างกัน เราจะอัปเดต _currentMessage และ rebuild UI
การใช้งาน:
MyStatefulWidget(message: "Hello") // สร้างครั้งแรก
MyStatefulWidget(message: "World") // อัปเดตด้วย message ใหม่
4. dispose()
dispose() ถูกเรียกใช้เมื่อ State object ถูกลบออกจาก Widget tree นี่เป็นจุดที่เหมาะสมในการ:
- ยกเลิกการสมัครสมาชิก (unsubscribe) จาก streams หรือ listeners
- ปล่อยทรัพยากรต่างๆ ที่ถูกใช้งาน (เช่น timers, animations)
- ยกเลิกการเชื่อมต่อกับฐานข้อมูล
ข้อควรจำ: หลังจาก dispose() ถูกเรียกใช้ State object จะไม่สามารถใช้งานได้อีกต่อไป
ตัวอย่าง:
import 'dart:async';
import 'package:flutter/material.dart';
class MyStatefulWidget extends StatefulWidget {
@override
_MyStatefulWidgetState createState() => _MyStatefulWidgetState();
}
class _MyStatefulWidgetState extends State<MyStatefulWidget> {
Timer? _timer;
@override
void initState() {
super.initState();
// สร้าง timer
_timer = Timer.periodic(Duration(seconds: 1), (timer) {
print("Timer is running...");
});
}
@override
void dispose() {
super.dispose();
// ยกเลิก timer เมื่อ Widget ถูก dispose
_timer?.cancel();
_timer = null;
print("dispose() ถูกเรียกใช้: Timer ถูกยกเลิก");
}
@override
Widget build(BuildContext context) {
return Text('Timer Example');
}
}
class MyApp extends StatelessWidget {
@override
Widget build(BuildContext context) {
return MaterialApp(
home: Scaffold(
appBar: AppBar(title: Text('Widget Lifecycle Example')),
body: Center(
child: ElevatedButton(
onPressed: () {
Navigator.push(
context,
MaterialPageRoute(builder: (context) => MyStatefulWidget()),
);
},
child: Text('Go to Timer Widget'),
),
),
),
);
}
}
ในตัวอย่างนี้ เราสร้าง Timer ใน initState() ที่จะทำงานทุกๆ 1 วินาที เมื่อ MyStatefulWidget ถูก dispose (เช่น เมื่อออกจากหน้าจอ) dispose() จะถูกเรียกใช้ และเราจะยกเลิก Timer เพื่อป้องกัน memory leak
สรุป
การทำความเข้าใจ Widget Lifecycle ใน Flutter เป็นสิ่งสำคัญสำหรับการพัฒนาแอปพลิเคชันที่มีประสิทธิภาพและตอบสนองต่อผู้ใช้ได้ดี การรู้จักฟังก์ชัน initState(), build(), didUpdateWidget(), และ dispose() ช่วยให้เราสามารถควบคุมการทำงานของ Widget ได้อย่างละเอียด และจัดการทรัพยากรได้อย่างเหมาะสม อย่าลืมนำความรู้ที่ได้จากบทความนี้ไปประยุกต์ใช้ในการพัฒนาแอปพลิเคชัน Flutter ของคุณ