Flutter Widget Lifecycle: initState'den dispose'a
# 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.
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.
@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:
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.
@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.
@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:
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.
@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.
@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.
@override
void dispose() {
_tabController.dispose();
_scrollController.dispose();
_animasyonController.dispose();
_abonelik?.cancel();
_focusNode.dispose();
super.dispose();
}Kurallar:
Tam Örnek: Tüm Yaşam Döngüsü Metotları Bir Arada
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ı
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.
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.
@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.
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.
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.
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.
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ışı
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.
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.
@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:
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:
@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 State Management: Riverpod, Provider ve Bloc Karşılaştırması
Flutter'da state management yaklaşımlarını karşılaştırın. Riverpod, Provider ve Bloc için kullanım senaryolarını ve karar kriterlerini netleştirin.
Flutter Performans Optimizasyonu: Kapsamlı Rehber
Flutter uygulamanızın performansını sistematik olarak artırın. Rebuild optimizasyonu, bellek yönetimi, lazy loading ve profiling tekniklerini öğrenin.
Dart Best Practices: Temiz ve Sürdürülebilir Kod Rehberi
Dart projelerinde okunabilir, test edilebilir ve sürdürülebilir kod yazmak için en etkili pratikleri öğrenin.
Flutter Projeniz mi Var?
iOS, Android ve web için yüksek performanslı Flutter uygulamaları geliştiriyorum.
İletişime Geç