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

Flutter Lifecycle เชิงลึก

State<T> class ของ Flutter เปิดเผย lifecycle methods หลายชุดที่ทำงานตามลำดับที่กำหนดไว้แน่นอน ถ้าคุณเคยใช้ React hooks หรือ class components ทุกอย่างมีเทียบเท่าโดยตรง เมื่อคุณเข้าใจ mapping คุณจะรู้ว่าต้องวาง logic แต่ละชิ้นไว้ที่ไหน

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 ต้อง redrawComponent function body / render()
didUpdateWidget(oldWidget)Parent rebuild และส่ง widget configuration ใหม่useEffect(() => { /* sync */ }, [prop])
setState(() { ... })เรียกโดย code ของคุณเพื่อ mutate state และ trigger rebuildconst [x, setX] = useState(...) setter call
deactivate()Widget ถูกลบออกจาก tree ชั่วคราว (ไม่ค่อยใช้)(ไม่มีเทียบเท่าโดยตรง — ไม่ค่อยต้องการ)
dispose()State ถูกทำลายถาวรuseEffect(() => { return () => cleanup(); }, [])
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
React
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>
);
}
Flutter
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));
}
}
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(),
            ),
          ),
        ),
      ],
    );
  }
}
Lifecycle method ใดที่เทียบเท่ากับ useEffect(() => { ... }, []) — mount-only effect?
ที่ที่ถูกต้องในการ cancel Timer หรือปิด StreamSubscription คือ?
didUpdateWidget(oldWidget) ทำงานเมื่อ:
ทำไมต้องเรียก super.dispose() ที่ END (ไม่ใช่ start) ของ dispose() override?