Skip to content

Latest commit

 

History

History
308 lines (251 loc) · 9.33 KB

File metadata and controls

308 lines (251 loc) · 9.33 KB

StatefulWidget Lifecycle — Panduan Lengkap

Diagram Lifecycle

  ┌──────────────────┐
  │   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

Penjelasan Setiap Method

1. Constructor

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

2. createState()

@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)

3. initState()

@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 context untuk InheritedWidget (belum ready, pakai didChangeDependencies)

4. didChangeDependencies()

@override
void didChangeDependencies() {
  super.didChangeDependencies();
  final theme = Theme.of(context); // aman di sini
  final mediaQuery = MediaQuery.of(context);
}
  • Kapan dipanggil:
    1. Sekali setelah initState() (pertama kali)
    2. 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

5. build()

@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

6. didUpdateWidget()

@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

7. setState()

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()

8. deactivate()

@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()

9. dispose()

@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

5 Skenario Praktis (Demo App)

Skenario 1: First Mount

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


Skenario 2: setState()

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


Skenario 3: didUpdateWidget()

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)

Skenario 4: didChangeDependencies()

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


Skenario 5: dispose()

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)


Ringkasan: Kapan Setiap Method Dipanggil?

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

Tips Penting

  1. initState vs didChangeDependencies: Jika butuh akses context (Theme, Provider, MediaQuery), pakai didChangeDependencies, bukan initState
  2. Selalu dispose: Controller, StreamSubscription, AnimationController — semua harus di-dispose
  3. build() harus ringan: Jangan fetch data atau setState di dalam build()
  4. didUpdateWidget untuk props: Jika child perlu react terhadap perubahan props dari parent, override didUpdateWidget
  5. setState setelah dispose = crash: Selalu cek mounted sebelum setState di async callback:
    await fetchData();
    if (mounted) {
      setState(() { ... });
    }