lifecycleinitstatedisposeMenengah4 mnt baca

initState & dispose

Inisialisasi sekali jalan dan bersih-bersih resource: controller, timer, subscription.

Kenapa Ada Dua Method Ini?

Setiap resource yang "hidup" butuh dua momen: saat ia mulai dipakai dan saat ia berhenti dipakai. Controller teks, timer, animation controller, dan stream subscription semuanya begitu: dibuat sekali, dipakai selama widget hidup, lalu wajib dibebaskan. initState adalah momen lahir yang dijamin hanya terjadi sekali, dan dispose adalah momen kematian yang dijamin dipanggil. Tanpa pasangan ini, resource bocor: timer terus berdetak untuk halaman yang sudah ditutup, dan aplikasi makin lama makin lambat tanpa sebab yang jelas.

dart
late final TextEditingController _controller;
Timer? _timer;

@override
void initState() {
  super.initState(); // selalu baris pertama
  _controller = TextEditingController();
  _timer = Timer.periodic(const Duration(seconds: 1), (_) {
    setState(() => _waktu = DateTime.now());
  });
}

@override
void dispose() {
  _timer?.cancel();      // hentikan dulu yang masih berjalan
  _controller.dispose(); // bebaskan yang dipegang
  super.dispose();       // selalu baris terakhir
}

Perhatikan urutannya: super.initState() harus baris pertama, super.dispose() harus baris terakhir. Di dispose, hentikan dulu resource yang aktif (timer, subscription), baru bebaskan yang dipegang (controller). Membalik urutan ini jarang fatal, tapi meniru konvensi membuat kode mudah dibaca semua orang.

Contoh Lengkap: TabController + Stream

Kasus nyata yang menggabungkan beberapa resource sekaligus, pola yang sangat umum di halaman chat atau dashboard:

dart
class _ChatState extends State<Chat> with SingleTickerProviderStateMixin {
  late final TabController _tabController;
  StreamSubscription? _pesanBaru;

  @override
  void initState() {
    super.initState();
    _tabController = TabController(length: 3, vsync: this);
    // dengarkan pesan masuk selama halaman hidup
    _pesanBaru = chatService.aliranPesan.listen((pesan) {
      if (!mounted) return;
      setState(() => _daftarPesan.add(pesan));
    });
  }

  @override
  void dispose() {
    _pesanBaru?.cancel();
    _tabController.dispose();
    super.dispose();
  }

  @override
  Widget build(BuildContext context) {
    return TabBarView(controller: _tabController, children: const [...]);
  }
}

Satu subscription yang lupa di-cancel berarti callback-nya tetap dipanggil selamanya, menahan seluruh State (dan halaman) di memori. Inilah sumber memory leak paling umum di aplikasi Flutter, dan gejalanya samar: aplikasi terasa berat setelah bolak-balik pindah halaman.

Edge Case Penting

initState tidak boleh async. Method ini sinkron menurut kontrak framework. Kalau butuh inisialisasi async, panggil future tanpa await lalu update state saat selesai (dengan cek mounted), atau pakai addPostFrameCallback untuk kode yang butuh context yang sudah siap penuh:

dart
@override
void initState() {
  super.initState();
  _muatAwal(); // async, tanpa await
  WidgetsBinding.instance.addPostFrameCallback((_) {
    // aman memakai context di sini, frame pertama sudah selesai
    ScaffoldMessenger.of(context).showSnackBar(
      const SnackBar(content: Text('Selamat datang!')),
    );
  });
}

Future<void> _muatAwal() async {
  final data = await api.ambil();
  if (!mounted) return;
  setState(() => _data = data);
}

late final bisa meledak. Kalau pembuatan resource di initState melempar exception, field late tidak pernah terisi dan dispose yang mengaksesnya akan crash untuk kedua kalinya. Untuk resource yang pembuatannya berisiko gagal, pakai nullable (Timer?) dengan ?.cancel() yang aman dari null.

Jangan bikin controller di build. build berjalan berkali-kali; membuat TextEditingController di sana berarti controller baru tiap rebuild, teks yang diketik hilang dan memori bocor. Controller selalu dibuat di initState, atau sebagai field final yang diinisialisasi langsung.

Catatan teknis: Nilai yang diturunkan dari konfigurasi widget (misalnya widget.judul) sebaiknya tidak di-cache di initState tanpa penanganan didUpdateWidget, karena parent bisa mengoper nilai baru tanpa initState berjalan lagi. Kalau nilainya bisa berubah, sinkronkan di didUpdateWidget.

Tantangan

Timer jam digital

Buat widget jam digital: Timer.periodic tiap detik mengupdate state waktu. Pastikan timer di-cancel di dispose.