Flutter

Flutter Widget Lifecycle: initState'den dispose'a

12 dk okuma9 Şubat 2026Güncellendi 9 Mar 2026
Flutter lifecycleinitState disposeStatefulWidget lifecycleFlutter memory leakFlutter setState after disposeFlutter widget rebuildFlutter stateful widgetFlutter best practices

StatefulWidget yaşam döngüsünü doğru anlamak, Flutter geliştiriciliğinde belirleyici becerilerden biri. Doğru yönetirseniz ekranlarınız hızlı, kararlı ve sızıntısız çalışır. Yanlış yönetirseniz gece yarısı anlaşılmaz hata loglarıyla boğuşursunuz. Bu yazıda her yaşam döngüsü metodunu gerçek kodlarla ve yıllar içinde edindiğim deneyimlerle birlikte detaylıca ele alıyorum.

Yaşam Döngüsüne Genel Bakış

Her bir metoda girmeden önce, StatefulWidget'in doğumundan ölümüne kadar izlediği akışı görelim:

  createState()
       |
       v
  initState()
       |
       v
  didChangeDependencies()
       |
       v
  build()  <------------------+
       |                      |
       v                      |
  [Widget ekranda yaşıyor]   |
       |                      |
       v                      |
  didUpdateWidget()  ---------+
       |
       v  (veya setState() build() üzerinden yeniden çizim tetikler)
       |
  deactivate()
       |
       v
  dispose()

Her StatefulWidget bu yoldan geçer. Kritik nokta şu: build defalarca çağrılabilir, ama initState ve dispose yalnızca bir kez çalışır.

Temel Yaşam Döngüsü Metotları

createState()

Giriş noktası. Flutter, StatefulWidget üzerinde bu metodu çağırarak State nesnesini üretir. Bir kez çalışır ve State döndürmekten başka iş yapmamalıdır.

dart
class ProfilEkrani extends StatefulWidget {
  final String kullaniciId;

  const ProfilEkrani({super.key, required this.kullaniciId});

  @override
  State<ProfilEkrani> createState() => _ProfilEkraniState();
}

initState()

State nesnesi oluşturulduktan hemen sonra bir kez çağrılır. Controller kurulumları, animasyon başlatmaları, tek seferlik veri yüklemeleri ve stream abonelikleri burada yapılır.

dart
@override
void initState() {
  super.initState();
  _tabController = TabController(length: 3, vsync: this);
  _scrollController = ScrollController();
  _animasyonController = AnimationController(
    duration: const Duration(milliseconds: 300),
    vsync: this,
  );
  _kullaniciBilgileriniYukle();
}

Kurallar:

  • super.initState() her zaman ilk satırda çağrılmalıdır.
  • Burada context üzerinden InheritedWidget okunamaz; widget henüz ağaca tam monte edilmemiştir. Bu tür işlemler didChangeDependencies içinde yapılır.
  • Metod senkron olmalıdır. Asenkron iş gerekiyorsa ayrı bir async metod çağırın, initState'i async yapmayın.

didChangeDependencies()

initState'ten hemen sonra ve ardından bu State'in bağımlı olduğu herhangi bir InheritedWidget değiştiğinde tekrar çağrılır. Theme.of(context), MediaQuery.of(context) veya Provider okumalarını ilk kez yapacağınız yer burasıdır.

dart
@override
void didChangeDependencies() {
  super.didChangeDependencies();
  // Context burada güvenle kullanılabilir
  final tema = Theme.of(context);
  _arkaplanRengi = tema.colorScheme.surface;

  final dil = Localizations.localeOf(context);
  if (_mevcutDil != dil) {
    _mevcutDil = dil;
    _yerellestirilmisIcerikYenidenYukle();
  }
}

Code review'larda sık gördüğüm bir hata: geliştiriciler MediaQuery.of(context) çağrısını initState içine koyuyor ve neden hata aldıklarını anlamıyorlar. Widget ağacı o noktada henüz tamamlanmamıştır. Bu tür okumaları didChangeDependencies metoduna taşıyın.

build()

Yaşam döngüsünde en sık çağrılan metod. State değiştiğinde, parent yeniden çizildiğinde veya InheritedWidget bağımlılığı değiştiğinde Flutter build'i çağırır. Saf ve hızlı olmalıdır: yan etki yok, ağır hesaplama yok, ağ isteği yok.

dart
@override
Widget build(BuildContext context) {
  return Scaffold(
    appBar: AppBar(title: Text(_kullaniciAdi)),
    body: _yukleniyor
        ? const Center(child: CircularProgressIndicator())
        : ListView.builder(
            controller: _scrollController,
            itemCount: _elemanlar.length,
            itemBuilder: (context, index) => ElemanKarti(eleman: _elemanlar[index]),
          ),
  );
}

Kurallar:

  • build içinde asla setState çağırmayın.
  • Burada I/O veya ağ isteği yapmayın.
  • Pahalı hesaplamalar varsa sonucu bir alanda önbelleğe alın ve setState içinde güncelleyin.

didUpdateWidget()

Parent widget yeniden çizilip bu widget'a yeni konfigürasyon gönderdiğinde çağrılır. Framework, eski widget'ı parametre olarak verir; karşılaştırıp gerekli tepkiyi verebilirsiniz.

dart
@override
void didUpdateWidget(covariant ProfilEkrani eskiWidget) {
  super.didUpdateWidget(eskiWidget);
  if (eskiWidget.kullaniciId != widget.kullaniciId) {
    // Kullanıcı değişti — verileri yeniden yükle
    _kullaniciBilgileriniYukle();
  }
}

Animasyonları yeniden başlatmak, veri çekmek veya controller'ları güncellemek için doğru yer burasıdır.

deactivate()

State ağaçtan çıkarıldığında çağrılır. Bu geçici olabilir; mesela widget GlobalKey aracılığıyla ağacın başka bir yerine taşınıyorsa. Çoğu durumda override etmeniz gerekmez, ama ileri düzey senaryolar için mevcuttur.

dart
@override
void deactivate() {
  // Widget'ın ağaçtaki konumuyla ilgili temizlikleri burada yapın
  super.deactivate();
}

dispose()

State nesnesi kalıcı olarak kaldırıldığında bir kez çağrılır. Kaynakları serbest bırakmak için son şansınızdır. Burayı atlamak, Flutter uygulamalarındaki bellek sızıntılarının bir numaralı sebebidir.

dart
@override
void dispose() {
  _tabController.dispose();
  _scrollController.dispose();
  _animasyonController.dispose();
  _abonelik?.cancel();
  _focusNode.dispose();
  super.dispose();
}

Kurallar:

  • super.dispose() her zaman en son çağrılmalıdır.
  • dispose çalıştıktan sonra asla setState çağırmayın. Bu, Flutter'daki meşhur "setState() called after dispose()" hatasının kaynağıdır.

Tam Örnek: Tüm Yaşam Döngüsü Metotları Bir Arada

dart
class CanliVeriEkrani extends StatefulWidget {
  final String kanalId;

  const CanliVeriEkrani({super.key, required this.kanalId});

  @override
  State<CanliVeriEkrani> createState() => _CanliVeriEkraniState();
}

class _CanliVeriEkraniState extends State<CanliVeriEkrani>
    with SingleTickerProviderStateMixin {
  late final AnimationController _animController;
  late final ScrollController _scrollController;
  StreamSubscription<VeriOlayi>? _veriAboneligi;
  List<VeriOlayi> _olaylar = [];
  bool _yukleniyor = true;
  bool _pinolanmis = true;

  // 1. initState — tek seferlik kurulum
  @override
  void initState() {
    super.initState();
    _animController = AnimationController(
      duration: const Duration(milliseconds: 400),
      vsync: this,
    );
    _scrollController = ScrollController();
    _kanalaAboneOl(widget.kanalId);
  }

  // 2. didChangeDependencies — context bağımlı başlatma
  @override
  void didChangeDependencies() {
    super.didChangeDependencies();
    final parlaklik = Theme.of(context).brightness;
    _animController.duration = parlaklik == Brightness.dark
        ? const Duration(milliseconds: 600)
        : const Duration(milliseconds: 400);
  }

  // 3. build — saf UI tanımı
  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(title: Text('Kanal: ${widget.kanalId}')),
      body: _yukleniyor
          ? const Center(child: CircularProgressIndicator())
          : ListView.builder(
              controller: _scrollController,
              itemCount: _olaylar.length,
              itemBuilder: (context, index) {
                return ListTile(
                  title: Text(_olaylar[index].mesaj),
                  subtitle: Text(_olaylar[index].zamanDamgasi.toString()),
                );
              },
            ),
    );
  }

  // 4. didUpdateWidget — parent değişikliklerine tepki
  @override
  void didUpdateWidget(covariant CanliVeriEkrani eskiWidget) {
    super.didUpdateWidget(eskiWidget);
    if (eskiWidget.kanalId != widget.kanalId) {
      _veriAboneligi?.cancel();
      _olaylar = [];
      _kanalaAboneOl(widget.kanalId);
    }
  }

  // 5. deactivate — ağaçtan geçici çıkarma
  @override
  void deactivate() {
    super.deactivate();
  }

  // 6. dispose — kalıcı temizlik
  @override
  void dispose() {
    _pinolanmis = false;
    _veriAboneligi?.cancel();
    _animController.dispose();
    _scrollController.dispose();
    super.dispose();
  }

  // Yardımcı metod
  void _kanalaAboneOl(String kanalId) {
    _veriAboneligi = VeriServisi.aboneOl(kanalId).listen(
      (olay) {
        if (!_pinolanmis) return;
        setState(() {
          _olaylar.add(olay);
          _yukleniyor = false;
        });
      },
      onError: (hata) {
        debugPrint('Stream hatası: $hata');
      },
    );
  }
}

Bellek Sızıntısı Önleme

Flutter'da bellek sızıntılarının neredeyse tamamı tek bir sebepten kaynaklanır: bir şey, dispose edilmiş State nesnesine referans tutmaya devam eder. İşte en yaygın suçlular ve çözümleri.

Stream Abonelik Sızıntısı

dart
// KÖTÜ: abonelik asla iptal edilmiyor
@override
void initState() {
  super.initState();
  FirebaseFirestore.instance
      .collection('mesajlar')
      .snapshots()
      .listen((snapshot) {
    setState(() {
      _mesajlar = snapshot.docs;
    });
  });
}

// İYİ: aboneliği saklayıp dispose'da iptal edin
late final StreamSubscription _mesajAboneligi;

@override
void initState() {
  super.initState();
  _mesajAboneligi = FirebaseFirestore.instance
      .collection('mesajlar')
      .snapshots()
      .listen((snapshot) {
    setState(() {
      _mesajlar = snapshot.docs;
    });
  });
}

@override
void dispose() {
  _mesajAboneligi.cancel();
  super.dispose();
}

AnimationController Sızıntısı

Her AnimationController ticker kaynağı ayırır. Dispose etmeyi unutursanız, ticker widget gitmiş olsa bile kare kare çalışmaya devam eder.

dart
// KÖTÜ: controller sızıyor
late final AnimationController _ctrl;

@override
void initState() {
  super.initState();
  _ctrl = AnimationController(vsync: this, duration: Durations.medium1);
  _ctrl.repeat();
}

// İYİ: controller'ı dispose edin
@override
void dispose() {
  _ctrl.dispose();
  super.dispose();
}

TextEditingController ve FocusNode Sızıntısı

Bunlar hafif görünür ama dahili olarak listener kaydı tutarlar. Dispose etmeyi ihmal etmeyin.

dart
@override
void dispose() {
  _emailController.dispose();
  _sifreController.dispose();
  _emailFocusNode.dispose();
  _sifreFocusNode.dispose();
  super.dispose();
}

Asenkron Boşluk Problemi

"setState called after dispose" hatasının en yaygın kaynağı, kullanıcı sayfadan ayrıldıktan sonra tamamlanan asenkron işlemlerdir.

dart
// KÖTÜ: koruma yok
Future<void> _verileriYukle() async {
  final veri = await repository.elemanlarGetir();
  setState(() {
    _elemanlar = veri;
  });
}

// İYİ: mounted kontrolü
Future<void> _verileriYukle() async {
  final veri = await repository.elemanlarGetir();
  if (!mounted) return;
  setState(() {
    _elemanlar = veri;
  });
}

mounted her State nesnesinde yerleşik olarak bulunur. Her await sonrasında mutlaka kontrol edin.

StatefulWidget vs StatelessWidget vs ConsumerWidget: Hangisini Kullanmalı?

Doğru widget türünü seçmek, okunabilirliği, test edilebilirliği ve performansı doğrudan etkileyen bir tasarım kararıdır.

StatelessWidget

Widget'in hiçbir değişken durumu olmadığında kullanın. Her şeyi constructor'dan alır ve çizer.

dart
class KullaniciAvatari extends StatelessWidget {
  final String resimUrl;
  final double yaricap;

  const KullaniciAvatari({super.key, required this.resimUrl, this.yaricap = 24});

  @override
  Widget build(BuildContext context) {
    return CircleAvatar(
      radius: yaricap,
      backgroundImage: NetworkImage(resimUrl),
    );
  }
}

StatefulWidget

Yerel, widget kapsamında değişken duruma ihtiyaç duyduğunuzda kullanın: animasyon controller'ları, metin düzenleme controller'ları, scroll pozisyonu takibi, form doğrulama durumu veya global store'da yeri olmayan herhangi bir durum.

dart
// StatefulWidget'in doğru kullanımı: yerel UI durumu
class GenisleyenKart extends StatefulWidget { ... }
class _GenisleyenKartState extends State<GenisleyenKart> {
  bool _genisledi = false;
  // Bu durum tamamen UI'a yerel — provider'a gerek yok
}

ConsumerWidget (Riverpod)

Widget'in Riverpod provider'ları tarafından yönetilen durumu okuması veya tepki vermesi gerektiğinde kullanın. ConsumerWidget, StatefulWidget gerektirmeden provider'ları izlemek için ref nesnesi sağlar.

dart
class KullaniciProfilGorunumu extends ConsumerWidget {
  @override
  Widget build(BuildContext context, WidgetRef ref) {
    final kullanici = ref.watch(kullaniciProvider);
    return kullanici.when(
      data: (veri) => Text(veri.isim),
      loading: () => const CircularProgressIndicator(),
      error: (h, st) => Text('Hata: $h'),
    );
  }
}

Karar Akışı

  1. Widget'in değişken duruma ihtiyacı var mı? Hayır -> StatelessWidget.
  2. Bu durum Riverpod tarafından mı yönetiliyor? Evet -> ConsumerWidget (veya yaşam döngüsü metotlarına da ihtiyaç varsa ConsumerStatefulWidget).
  3. Durum widget'a yerel mi (animasyonlar, controller'lar, form durumu)? Evet -> StatefulWidget.

Code review'larda sık gördüğüm bir hata: geliştiriciler "bir şey yapmaları" gereken her yerde StatefulWidget'e uzanıyor. Çoğu zaman yönettikleri durum aslında bir provider'da olmalı ve widget ConsumerWidget olmalıdır. Diğer durumlarda ise widget tamamen sunumsal olup StatelessWidget çok daha basit ve performanslı olurdu.

Yaşam Döngüsü Sorunlarını Hata Ayıklama

İşler ters gittiğinde Flutter genellikle yararlı hata mesajları verir, ama nereye bakacağınızı bilmek saatler kazandırır.

"setState() called after dispose()"

Bir asenkron callback, stream listener veya animasyon callback'i, artık var olmayan bir widget'ın durumunu güncellemeye çalıştı. Çözüm her zaman aynı: mounted ile koruma ekleyin veya kaynağı iptal edin.

dart
// Asenkron metotlarınıza bunu ekleyin
if (!mounted) return;

Widget Çok Sık Yeniden Çiziliyor

build içinde debugPrint kullanarak ne sıklıkla çağrıldığını görün. Eğer widget her karede yeniden çiziliyorsa, bir üst widget'ın setState'i çok geniş kapsamda çağırıp çağırmadığını kontrol edin.

dart
@override
Widget build(BuildContext context) {
  debugPrint('${widget.runtimeType} build çağrıldı');
  // ...
}

Daha derin analiz için Flutter DevTools widget inspector kullanın. Yeniden çizimleri gerçek zamanlı olarak vurgular.

initState Beklenmedik Şekilde Tekrar Çalışıyor

initState birden fazla kez çalışıyorsa widget'ınız yok edilip yeniden oluşturuluyor demektir. Yaygın sebepler:

  • Üst widget, widget'ınızın key değerini değiştiriyor.
  • Widget'ın ağaçtaki konumu değişiyor (mesela açılıp kapanan bir koşul bloğunun içinde).
  • PageView veya ListView ekran dışı çocukları bellekten atıyor.

Bu durumlarda state'i korumak için AutomaticKeepAliveClientMixin kullanmayı veya state'i bir provider'a taşımayı düşünün.

Tüm Yaşam Döngüsünü Yazdırma

Geliştirme sırasında tam olarak ne olduğunu anlamak için her yaşam döngüsü metoduna print ekleyebilirsiniz:

dart
@override
void initState() {
  super.initState();
  debugPrint('[YasamDongusu] initState');
}

@override
void didChangeDependencies() {
  super.didChangeDependencies();
  debugPrint('[YasamDongusu] didChangeDependencies');
}

@override
void didUpdateWidget(covariant MyWidget eskiWidget) {
  super.didUpdateWidget(eskiWidget);
  debugPrint('[YasamDongusu] didUpdateWidget');
}

@override
void deactivate() {
  debugPrint('[YasamDongusu] deactivate');
  super.deactivate();
}

@override
void dispose() {
  debugPrint('[YasamDongusu] dispose');
  super.dispose();
}

Sonuç

Yaşam döngüsü disiplini göz alıcı bir konu değildir, ama prodüksiyonda sağlam çalışan bir uygulama ile ara sıra gizemli hatalar veren bir uygulama arasındaki farkı belirler. Kurulum için initState'i, context bağımlı başlatma için didChangeDependencies'i, saf çizim için build'i, parent değişikliklerine tepki için didUpdateWidget'ı ve temizlik için dispose'u ustaca kullanın. Her asenkron boşluğu mounted ile koruyun. Her controller'ı dispose edin. Her aboneliği iptal edin.

Karmaşık yaşam döngüsü etkileşimleri olan ekranlarınız varsa, potansiyel sızıntılar ve performans sorunları için inceleme yapabilirim.

İlgili Makaleler

Flutter Projeniz mi Var?

iOS, Android ve web için yüksek performanslı Flutter uygulamaları geliştiriyorum.

İletişime Geç