Flutter Lifecycle เชิงลึก
State<T> class ของ Flutter เปิดเผย lifecycle methods หลายชุดที่ทำงานตามลำดับที่กำหนดไว้แน่นอน ถ้าคุณเคยใช้ React hooks หรือ class components ทุกอย่างมีเทียบเท่าโดยตรง เมื่อคุณเข้าใจ mapping คุณจะรู้ว่าต้องวาง logic แต่ละชิ้นไว้ที่ไหน
ตาราง mapping สมบูรณ์
หัวข้อที่มีชื่อว่า “ตาราง mapping สมบูรณ์”| Flutter method | เมื่อไหร่ที่ทำงาน | เทียบเท่า React |
|---|---|---|
createState() | ครั้งเดียว — widget ถูกแทรกเข้า tree ครั้งแรก | Component function first call / constructor() |
initState() | ครั้งเดียว — State object ถูกสร้าง ก่อน build() แรก | useEffect(() => { /* setup */ }, []) — mount phase |
didChangeDependencies() | หลัง initState() และเมื่อ InheritedWidget ที่ State depend on เปลี่ยน | useEffect(() => { /* context */ }, [contextValue]) |
build() | ทุกครั้งที่ widget ต้อง redraw | Component function body / render() |
didUpdateWidget(oldWidget) | Parent rebuild และส่ง widget configuration ใหม่ | useEffect(() => { /* sync */ }, [prop]) |
setState(() { ... }) | เรียกโดย code ของคุณเพื่อ mutate state และ trigger rebuild | const [x, setX] = useState(...) setter call |
deactivate() | Widget ถูกลบออกจาก tree ชั่วคราว (ไม่ค่อยใช้) | (ไม่มีเทียบเท่าโดยตรง — ไม่ค่อยต้องการ) |
dispose() | State ถูกทำลายถาวร | useEffect(() => { return () => cleanup(); }, []) |
ขั้นตอน lifecycle ทีละขั้น
หัวข้อที่มีชื่อว่า “ขั้นตอน lifecycle ทีละขั้น”createState() ↓initState() ← mount: เริ่ม controllers, subscriptions, fetch data เริ่มต้น ↓didChangeDependencies() ← เรียกครั้งแรก แล้วเรียกอีกถ้า InheritedWidget เปลี่ยน ↓build() ← pure UI description; ทำงานทุก rebuild ↑ ↓setState() ← วนกลับไป build() จนกว่า widget จะถูกลบ ↓didUpdateWidget() ← ทำงานเมื่อ parent ส่ง widget config ใหม่ (new props) ↓deactivate() ← ลบออกจาก tree (มักชั่วคราว) ↓dispose() ← teardown ถาวร: cancel timers, ปิด streams, dispose controllersการเปรียบเทียบแบบ side-by-side
หัวข้อที่มีชื่อว่า “การเปรียบเทียบแบบ side-by-side”import { useState, useEffect, useRef } from 'react';
function TimerWidget({ label }) { const [seconds, setSeconds] = useState(0); const intervalRef = useRef(null);
// Mount — เทียบเท่า initState useEffect(() => { intervalRef.current = setInterval(() => { setSeconds(s => s + 1); }, 1000);
// Unmount cleanup — เทียบเท่า dispose return () => clearInterval(intervalRef.current); }, []);
// Prop change — เทียบเท่า didUpdateWidget useEffect(() => { console.log('label changed to', label); // re-sync logic ที่ depend on label }, [label]);
return ( <div> <p>{label}: {seconds}s</p> </div> );}class TimerWidget extends StatefulWidget { final String label; const TimerWidget({super.key, required this.label});
@override State<TimerWidget> createState() => _TimerWidgetState();}
class _TimerWidgetState extends State<TimerWidget> { int _seconds = 0; Timer? _timer;
// Mount — ทำงานครั้งเดียวก่อน build() แรก @override void initState() { super.initState(); _timer = Timer.periodic(const Duration(seconds: 1), (_) { if (mounted) setState(() => _seconds++); }); }
// Prop change — old config ดูได้ผ่าน oldWidget @override void didUpdateWidget(TimerWidget oldWidget) { super.didUpdateWidget(oldWidget); if (oldWidget.label != widget.label) { debugPrint('label changed to ${widget.label}'); } }
// Unmount — teardown ถาวร @override void dispose() { _timer?.cancel(); super.dispose(); }
@override Widget build(BuildContext context) { return Text('${widget.label}: $_seconds s', style: const TextStyle(fontSize: 24)); }}ตัวอย่างที่รันได้ — Timer กับ lifecycle สมบูรณ์
หัวข้อที่มีชื่อว่า “ตัวอย่างที่รันได้ — Timer กับ lifecycle สมบูรณ์”import 'dart:async';
import 'package:flutter/material.dart';
void main() => runApp(const MyApp());
class MyApp extends StatelessWidget {
const MyApp({super.key});
@override
Widget build(BuildContext context) {
return MaterialApp(
theme: ThemeData(colorScheme: ColorScheme.fromSeed(seedColor: Colors.deepPurple)),
home: const LifecycleDemoScreen(),
);
}
}
class LifecycleDemoScreen extends StatefulWidget {
const LifecycleDemoScreen({super.key});
@override
State<LifecycleDemoScreen> createState() => _LifecycleDemoScreenState();
}
class _LifecycleDemoScreenState extends State<LifecycleDemoScreen> {
bool _showTimer = true;
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(title: const Text('Lifecycle Demo')),
body: Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
if (_showTimer) const TimerWidget(label: 'Elapsed'),
const SizedBox(height: 32),
FilledButton(
onPressed: () => setState(() => _showTimer = !_showTimer),
child: Text(_showTimer ? 'Unmount timer (dispose)' : 'Mount timer (initState)'),
),
],
),
),
);
}
}
// Widget ที่ demo lifecycle สมบูรณ์
class TimerWidget extends StatefulWidget {
final String label;
const TimerWidget({super.key, required this.label});
@override
State<TimerWidget> createState() => _TimerWidgetState();
}
class _TimerWidgetState extends State<TimerWidget> {
int _seconds = 0;
Timer? _timer;
final List<String> _log = [];
// ═══════════════ initState ═══════════════
// เทียบเท่า: useEffect(() => { start timer }, [])
@override
void initState() {
super.initState();
_log.add('initState — timer started');
_timer = Timer.periodic(const Duration(seconds: 1), (_) {
if (mounted) {
setState(() {
_seconds++;
_log.add('setState — rebuild #$_seconds');
});
}
});
}
// ═══════════════ didChangeDependencies ═══════════════
// ทำงานหลัง initState และเมื่อ InheritedWidget เปลี่ยน
@override
void didChangeDependencies() {
super.didChangeDependencies();
// ปลอดภัยที่จะเรียก Theme.of(context) ที่นี่
_log.add('didChangeDependencies');
}
// ═══════════════ didUpdateWidget ═══════════════
// เทียบเท่า: useEffect(() => { sync }, [label])
@override
void didUpdateWidget(TimerWidget oldWidget) {
super.didUpdateWidget(oldWidget);
if (oldWidget.label != widget.label) {
_log.add('didUpdateWidget — label changed');
}
}
// ═══════════════ dispose ═══════════════
// เทียบเท่า: useEffect(() => { return () => cleanup() }, [])
@override
void dispose() {
_timer?.cancel();
// หมายเหตุ: ไม่สามารถเรียก setState ที่นี่ได้ — widget กำลังถูก tear down
super.dispose();
}
@override
Widget build(BuildContext context) {
final scheme = Theme.of(context).colorScheme;
return Column(
mainAxisSize: MainAxisSize.min,
children: [
Text(
'${widget.label}: $_seconds s',
style: TextStyle(
fontSize: 48,
fontWeight: FontWeight.bold,
color: scheme.primary,
),
),
const SizedBox(height: 16),
Container(
width: 320,
height: 160,
padding: const EdgeInsets.all(12),
decoration: BoxDecoration(
color: scheme.surfaceContainerHighest,
borderRadius: BorderRadius.circular(8),
),
child: SingleChildScrollView(
reverse: true,
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: _log
.map((entry) => Text(entry,
style: TextStyle(fontSize: 12, color: scheme.onSurface)))
.toList(),
),
),
),
],
);
}
}