Flutter Widget Lifecycle: initState'den dispose'a

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

# Flutter Widget Yaşam Döngüsü: initState'ten dispose'a

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: class="code-number">3, vsync: this);
  _scrollController = ScrollController();
  _animasyonController = AnimationController(
    duration: const Duration(milliseconds: class="code-number">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();
      class=class="code-string">"code-comment">// 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) {
        class=class="code-string">"code-comment">// 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() {
      class=class="code-string">"code-comment">// 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;
    
      class=class="code-string">"code-comment">// class="code-number">1. initState — tek seferlik kurulum
      @override
      void initState() {
        super.initState();
        _animController = AnimationController(
          duration: const Duration(milliseconds: class="code-number">400),
          vsync: this,
        );
        _scrollController = ScrollController();
        _kanalaAboneOl(widget.kanalId);
      }
    
      class=class="code-string">"code-comment">// class="code-number">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: class="code-number">600)
            : const Duration(milliseconds: class="code-number">400);
      }
    
      class=class="code-string">"code-comment">// class="code-number">3. build — saf UI tanımı
      @override
      Widget build(BuildContext context) {
        return Scaffold(
          appBar: AppBar(title: Text(class="code-string">'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()),
                    );
                  },
                ),
        );
      }
    
      class=class="code-string">"code-comment">// class="code-number">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);
        }
      }
    
      class=class="code-string">"code-comment">// class="code-number">5. deactivate — ağaçtan geçici çıkarma
      @override
      void deactivate() {
        super.deactivate();
      }
    
      class=class="code-string">"code-comment">// class="code-number">6. dispose — kalıcı temizlik
      @override
      void dispose() {
        _pinolanmis = false;
        _veriAboneligi?.cancel();
        _animController.dispose();
        _scrollController.dispose();
        super.dispose();
      }
    
      class=class="code-string">"code-comment">// 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(class="code-string">'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
    class=class="code-string">"code-comment">// KÖTÜ: abonelik asla iptal edilmiyor
    @override
    void initState() {
      super.initState();
      FirebaseFirestore.instance
          .collection(class="code-string">'mesajlar')
          .snapshots()
          .listen((snapshot) {
        setState(() {
          _mesajlar = snapshot.docs;
        });
      });
    }
    
    class=class="code-string">"code-comment">// İYİ: aboneliği saklayıp disposeclass="code-string">'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
    class=class="code-string">"code-comment">// KÖTÜ: controller sızıyor
    late final AnimationController _ctrl;
    
    @override
    void initState() {
      super.initState();
      _ctrl = AnimationController(vsync: this, duration: Durations.medium1);
      _ctrl.repeat();
    }
    
    class=class="code-string">"code-comment">// İ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
    class=class="code-string">"code-comment">// KÖTÜ: koruma yok
    Future<void> _verileriYukle() async {
      final veri = await repository.elemanlarGetir();
      setState(() {
        _elemanlar = veri;
      });
    }
    
    class=class="code-string">"code-comment">// İ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 = class="code-number">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
    class=class="code-string">"code-comment">// StatefulWidgetclass="code-string">'in doğru kullanımı: yerel UI durumu
    class GenisleyenKart extends StatefulWidget { ... }
    class _GenisleyenKartState extends State<GenisleyenKart> {
      bool _genisledi = false;
      class=class="code-string">"code-comment">// 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(class="code-string">'Hata: $h'),
        );
      }
    }

    Karar Akışı

  • Widget'in değişken duruma ihtiyacı var mı? Hayır -> **StatelessWidget**.
  • Bu durum Riverpod tarafından mı yönetiliyor? Evet -> **ConsumerWidget** (veya yaşam döngüsü metotlarına da ihtiyaç varsa **ConsumerStatefulWidget**).
  • 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
    class=class="code-string">"code-comment">// 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(class="code-string">'${widget.runtimeType} build çağrıldı');
      class=class="code-string">"code-comment">// ...
    }

    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(class="code-string">'[YasamDongusu] initState');
    }
    
    @override
    void didChangeDependencies() {
      super.didChangeDependencies();
      debugPrint(class="code-string">'[YasamDongusu] didChangeDependencies');
    }
    
    @override
    void didUpdateWidget(covariant MyWidget eskiWidget) {
      super.didUpdateWidget(eskiWidget);
      debugPrint(class="code-string">'[YasamDongusu] didUpdateWidget');
    }
    
    @override
    void deactivate() {
      debugPrint(class="code-string">'[YasamDongusu] deactivate');
      super.deactivate();
    }
    
    @override
    void dispose() {
      debugPrint(class="code-string">'[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ç