┌──────────────────┐
│ Constructor │ ← Widget object dibuat (MyWidget())
└────────┬─────────┘
▼
┌──────────────────┐
│ createState() │ ← State object dibuat (sekali per mount)
└────────┬─────────┘
▼
┌──────────────────┐
│ initState() │ ← Inisialisasi awal (subscribe, controller, dsb)
└────────┬─────────┘
▼
┌────────────────────────────┐
│ didChangeDependencies() │ ← Dipanggil setelah initState + setiap InheritedWidget berubah
└────────┬───────────────────┘
▼
┌───────────┐ ◄────── setState() menandai state "dirty"
│ (dirty) │ ◄────── didUpdateWidget() juga menandai dirty
└─────┬─────┘
▼
┌──────────────────┐
│ build() │ ← Render widget tree
└────────┬─────────┘
▼
┌───────────┐
│ (clean) │ ← Widget sudah ter-render
└─────┬─────┘
│
┌──────┴──────┐
▼ ▼
(tetap hidup) (dihapus dari tree)
│ │
│ ┌──────┴──────────┐
│ │ deactivate() │
│ └──────┬──────────┘
│ ▼
│ ┌──────────────────┐
│ │ dispose() │ ← Cleanup (unsubscribe, close controller)
│ └──────┬───────────┘
│ ▼
│ ┌──────────────────┐
│ │ (defunct) │ ← State tidak bisa dipakai lagi
│ └──────────────────┘
│
└──→ Kembali ke dirty saat ada trigger baru
MyWidget({required this.title});- Kapan dipanggil: Saat widget di-instantiate (
MyWidget(title: 'Hello')) - Boleh: Set parameter/props
- Jangan: Akses context, panggil setState, atau side effects
@override
State<MyWidget> createState() => _MyWidgetState();- Kapan dipanggil: Sekali, tepat setelah Constructor
- Tugas: Membuat State object
- Catatan: Jika widget di-remove lalu di-add lagi, createState() dipanggil lagi (State baru)
@override
void initState() {
super.initState();
_controller = TextEditingController();
_loadData();
}- Kapan dipanggil: Sekali, setelah createState()
- Tugas: Inisialisasi yang butuh State: buat controller, subscribe stream, load data awal
- Boleh: Akses
widget.xxx(props dari parent) - Jangan: Akses
contextuntuk InheritedWidget (belum ready, pakai didChangeDependencies)
@override
void didChangeDependencies() {
super.didChangeDependencies();
final theme = Theme.of(context); // aman di sini
final mediaQuery = MediaQuery.of(context);
}- Kapan dipanggil:
- Sekali setelah initState() (pertama kali)
- Setiap kali InheritedWidget yang di-depend berubah
- InheritedWidget apa?
Theme.of(context),MediaQuery.of(context),Provider.of<T>(context), dsb - Use case: React terhadap perubahan theme, locale, atau Provider
@override
Widget build(BuildContext context) {
return Scaffold(...);
}- Kapan dipanggil: Setiap kali state berubah (dirty → build → clean)
- Trigger: setState(), didUpdateWidget(), didChangeDependencies()
- Aturan: HARUS pure function — jangan ada side effects, network call, atau setState di sini
- Tip: build() bisa dipanggil 60x per detik saat animasi, jadi harus ringan
@override
void didUpdateWidget(covariant MyWidget oldWidget) {
super.didUpdateWidget(oldWidget);
if (oldWidget.title != widget.title) {
// React terhadap perubahan props
}
}- Kapan dipanggil: Saat parent widget rebuild dan pass props baru ke widget ini
- Parameter:
oldWidget= widget config sebelumnya - Use case: Bandingkan old vs new props, update controller, restart animation
- PENTING: State object TETAP SAMA (tidak dibuat ulang). Hanya widget config yang berubah
void _onTap() {
setState(() {
_counter++;
});
}- Kapan dipanggil: Manual oleh developer
- Efek: Menandai state sebagai "dirty" → trigger build() di frame berikutnya
- Jangan: Panggil di initState(), build(), atau dispose()
@override
void deactivate() {
// Widget dilepas dari tree (tapi mungkin di-attach ulang)
super.deactivate();
}- Kapan dipanggil: Saat widget dilepas dari widget tree
- Catatan: Jarang di-override. Widget bisa di-reattach (misal: GlobalKey move)
- Selalu panggil:
super.deactivate()
@override
void dispose() {
_controller.dispose();
_subscription.cancel();
_focusNode.dispose();
super.dispose();
}- Kapan dipanggil: Saat widget permanent dihapus dari tree
- Tugas: Cleanup semua resources: dispose controller, cancel subscription, close stream
- PENTING: Jika tidak cleanup → memory leak!
- Selalu panggil:
super.dispose()di akhir
Aksi: Buka screen lifecycle demo Yang terjadi di console:
🔨 Constructor → Skenario-1
🏗️ createState → Skenario-1
🟢 initState → Skenario-1
🔗 didChangeDependencies → Skenario-1
🧱 build → Skenario-1
Insight: Urutan selalu sama — Constructor → createState → initState → didChangeDependencies → build
Aksi: Tekan tombol "setState (local +1)" di dalam widget Yang terjadi di console:
⚡ setState → Skenario-2
🧱 build → Skenario-2
Insight: setState() HANYA trigger build(). initState, didChangeDependencies, didUpdateWidget TIDAK dipanggil
Aksi: Tekan "Ubah label" atau "Ubah color" di parent Yang terjadi di console:
🔨 Constructor → Skenario-3 (Props #1) ← Widget object baru dibuat
🔄 didUpdateWidget → Skenario-3 (Props #1) ← State object TETAP SAMA
↳ label changed: "Props #0" → "Props #1"
🧱 build → Skenario-3 (Props #1)
Insight:
- Constructor dipanggil (widget object baru), tapi createState() dan initState() TIDAK
- State object di-reuse, hanya widget config yang update
- Local count TETAP (state preserved)
Aksi: Toggle theme (Light ↔ Dark) Yang terjadi di console:
🔗 didChangeDependencies → Skenario-4
🧱 build → Skenario-4
Insight: Karena widget pakai Theme.of(context) di build(), perubahan Theme (InheritedWidget) trigger didChangeDependencies
Aksi: Tekan "Hide Widget" Yang terjadi di console:
⏸️ deactivate → Skenario-5
🔴 dispose → Skenario-5
↳ Widget telah di-dispose! State hilang.
Aksi: Tekan "Show Widget" (setelah hide)
🔨 Constructor → Skenario-5
🏗️ createState → Skenario-5
🟢 initState → Skenario-5
🔗 didChangeDependencies → Skenario-5
🧱 build → Skenario-5
Insight: Setelah dispose, state hilang total. Show ulang = buat dari awal (local count reset ke 0)
| Method | Trigger | Frekuensi |
|---|---|---|
| Constructor | MyWidget() di-instantiate |
Setiap parent rebuild |
| createState() | Pertama kali mount | Sekali per mount |
| initState() | Setelah createState | Sekali per mount |
| didChangeDependencies() | initState + InheritedWidget berubah | 1x awal + setiap dependency change |
| build() | Setiap state dirty | Bisa sangat sering |
| didUpdateWidget() | Parent pass props baru | Setiap parent rebuild |
| setState() | Dipanggil manual | Sesuai kebutuhan |
| deactivate() | Widget dilepas dari tree | Sekali per unmount |
| dispose() | Widget dihapus permanent | Sekali per unmount |
- initState vs didChangeDependencies: Jika butuh akses
context(Theme, Provider, MediaQuery), pakaididChangeDependencies, bukaninitState - Selalu dispose: Controller, StreamSubscription, AnimationController — semua harus di-dispose
- build() harus ringan: Jangan fetch data atau setState di dalam build()
- didUpdateWidget untuk props: Jika child perlu react terhadap perubahan props dari parent, override didUpdateWidget
- setState setelah dispose = crash: Selalu cek
mountedsebelum setState di async callback:await fetchData(); if (mounted) { setState(() { ... }); }