Flutter’da İlk Ay: Widget Ağacına Alışmak
Altı yıl Swift, Java ve Kotlin ile yerel uygulama yazdıktan sonra Flutter’a geçtim. Dilin sözdizimi değil, arayüzü kurma biçimi zorladı: değiştirilemez widget’lar, sürekli çağrılan build metodu, setState’in kapsadığı alan ve BuildContext’in ağaçtaki yeri. İlk ayda kafamı karıştıran noktaları ve yaptığım hataları sırayla anlatıyorum.
2026’nın başında, altı yıl boyunca Swift, Java ve Kotlin ile yerel uygulama yazdıktan sonra Flutter’a geçtim. Vakti Geçmeden ve Hadi Yapalım’ı bu şekilde yayınladım, Sinepia şu an mağaza onay sürecinde. İlk ayda beni zorlayan şey dilin kendisi olmadı — Dart, Swift ve Kotlin bilen birine fazlasıyla tanıdık geliyor. Zorlayan şey, arayüzü kurma biçiminin kafamdaki modele hiç benzememesiydi.
Yerelden gelirken ilk duvar: ekran değil, ağaç
UIKit’te ya da Android’de bir görünüme referans tutar, üstünde oynarsınız: label.text = ..., button.isEnabled = false. Ekran bir nesnedir, siz o nesnenin özelliklerini değiştirirsiniz. Flutter’da böyle bir şey yok. Elinizde değiştirebileceğiniz bir görünüm nesnesi tutmuyorsunuz; “şu anki duruma göre ekran şöyle görünmeli” diye bir tarif yazıyorsunuz ve çerçeve o tarifi tekrar tekrar çalıştırıyor.
İlk ekranımı bir ViewController’ı satır satır çevirir gibi yazdım. Sonuç işledi ama yanlış yazılmıştı: her şeyi tek bir State sınıfında topladım, en ufak değişiklikte bütün ekranı yeniden kurdum. Neyin neye karşılık geldiğini oturtmam bir haftamı aldı.
| Yerelde | Flutter’daki karşılığı |
|---|---|
| UIViewController / Activity | Bir widget ve Navigator üzerinde bir rota |
| UIView / View | Widget — ama görünümün kendisi değil, tarifi |
| Auto Layout / ConstraintLayout | Kısıt aşağı iner, boyut yukarı çıkar, konumu ebeveyn verir |
| viewDidLoad / onCreate | initState |
| deinit / onDestroy | dispose |
| UITableView / RecyclerView + adapter | ListView.builder |
“Her şey widget” cümlesinin altında ne var
Bu cümleyi her yerde duyuyorsunuz ama tek başına hiçbir şey anlatmıyor. İşe yarar hâli şu: yazdığınız widget’lar değiştirilemez birer yapılandırma nesnesi. Ekranda duran şey onlar değil. Flutter arka planda ikinci bir ağaç daha tutuyor — element ağacı. Element’ler kalıcı; durumu onlar taşıyor, kısıt ve çizim işini ise üçüncü katman olan render nesneleri yapıyor.
Bir yeniden kurulum olduğunda çerçeve eski widget ile yeni widget’ı aynı konumda karşılaştırıyor. Tipleri ve anahtarları aynıysa element yerinde kalıyor, sadece yapılandırmasını güncelliyor. Bu yüzden build içinde onlarca widget nesnesi üretmek pahalı bir iş değil; pahalı olan kısım, o üretimin ardından gerçekten yeniden hesaplanan yerleşim ve çizim.
Bunu anladıktan sonra Container’ı kullanmayı da bıraktım. Container tek bir şey yapmıyor; dolgu, hizalama, dekorasyon gibi widget’ları paketleyen bir kolaylık. Ne istediğimi biliyorsam doğrudan onu yazmak hem daha okunur oluyor hem de const yapmama izin veriyor.
class FiyatEtiketi extends StatelessWidget {
const FiyatEtiketi({super.key, required this.tutar});
final int tutar;
@override
Widget build(BuildContext context) {
return DecoratedBox(
decoration: BoxDecoration(
color: Theme.of(context).colorScheme.surfaceContainerHighest,
borderRadius: BorderRadius.circular(8),
),
child: Padding(
padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 8),
child: Text('$tutar TL'),
),
);
}
}
Stateless mi Stateful mı: yanlış sorduğum soru
İlk günlerde her ekran için “bu stateful olmalı mı?” diye sordum. Yanlış soruydu. Doğrusu şu: bu veri nerede duruyor ve değiştiğinde ağacın ne kadarı yeniden kurulmalı?
Stateless bir widget, verdiğiniz alanlarla ne çizeceğini bilir; alanlar değişirse yeni bir widget nesnesi gelir, o kadar. Stateful’da ise iki ayrı nesne var: widget nesnesi yine değiştirilemez ve her yeniden kurulumda yenisi üretilir, ama State nesnesi aynı yerde kalır. Bu ayrımı kavrayana kadar, ebeveynden gelen değeri initState içinde bir alana kopyalayıp sonra “neden güncellenmiyor” diye saatlerce baktım. Ebeveynin gönderdiği güncel değere widget.alanAdi ile erişmek gerekiyor; gerçekten karşılaştırma yapmam gereken durumlarda ise didUpdateWidget var.
Listelerde bir de anahtar meselesi çıktı. Stateful satırlardan oluşan bir listeyi sıraladığımda satırların durumları birbirine karıştı; çünkü çerçeve eşleştirmeyi konuma göre yapıyor. Satırlara kalıcı bir anahtar verince düzeldi.
build metodu sandığımdan çok daha sık çalışıyor
Yerel tarafta viewDidLoad bir kez çalışır ve siz de ona göre yazarsınız. build öyle değil. Kendi setState’inizle, herhangi bir üst widget’ın yeniden kurulmasıyla, tema değişiminde, ekran ölçüleri değiştiğinde, sayfa geçiş animasyonu sırasında her karede çağrılabilir.
Bunu en net klavye açılırken gördüm. Klavye açılınca ekranın güvenli alan ölçüleri değişiyor, buna bağlı widget’lar yeniden kuruluyor ve build içine koyduğum sıralama işlemi her seferinde baştan çalışıyordu. Aynı hatayı Firestore sorgusunu build içinde kurarak da yaptım.
Ağ isteği başlatmak, dinleyici kurmak, dosya okumak, ağır hesap yapmak build’in işi değil. build yalnızca mevcut durumu tarif etmeli ve kaç kez çağrılırsa çağrılsın aynı sonucu vermeli.
setState’in maliyeti ve const’un sessiz katkısı
setState sihirli bir güncelleme yapmıyor. Yaptığı iş, o State’e ait element’i kirli olarak işaretlemek; bir sonraki karede o element’in build metodu baştan çalışıyor. Yani maliyeti, o metodun altında kalan ağacın büyüklüğü kadar. Bütün ekranı tek bir State içine koyup bir sayaç için setState çağırırsanız, sayaçla ilgisi olmayan her şeyi de yeniden kurdurmuş olursunuz.
Çözüm, değişen parçayı ayrı bir widget sınıfına çıkarmak. Yardımcı bir metoda çıkarmak işe yaramıyor; metot ayrı bir element oluşturmadığı için sınır çizmiyor. Ayrı sınıf ise hem yeniden kurulumu kendi içinde sınırlıyor hem de const kurucuya izin veriyor. Aynı const örneği aynı yerde tekrar geldiğinde çerçeve o alt ağacı hiç güncellemiyor.
class SayacSatiri extends StatefulWidget {
const SayacSatiri({super.key});
@override
State<SayacSatiri> createState() => _SayacSatiriState();
}
class _SayacSatiriState extends State<SayacSatiri> {
int _adet = 0;
@override
Widget build(BuildContext context) {
return Row(
children: [
const UrunBasligi(), // const: yeniden kurulmuyor
Text('$_adet'),
IconButton(
icon: const Icon(Icons.add),
onPressed: () => setState(() => _adet++),
),
],
);
}
}
BuildContext: ağaçtaki adresin
BuildContext’i uzun süre “Android’deki Context gibi bir şey” sandım. Değil. O aslında widget’ınızın element ağacındaki yeri. Theme.of(context) yazdığınızda çerçeve o noktadan yukarı doğru çıkıp aradığı widget’ı buluyor. Yani bulacağı şey, hangi context’i verdiğinize bağlı.
Klasik hatayı ben de yaptım: Scaffold’u döndüren build metodunun kendi context’i ile bir alt sayfa açmaya çalıştım. O context Scaffold’un üstünde olduğu için arama başarısız oldu. Alt ağaçta bir Builder kullanıp oradaki context’i vermek meseleyi çözdü.
İki not daha: initState içinde bu tür yukarı aramaları yapmak güvenli değil, doğru yer didChangeDependencies. Ve bir await’ten sonra context’i kullanmadan ya da setState çağırmadan önce mounted kontrolü şart — kullanıcı beklerken ekrandan çıkarsa uygulama hata veriyor.
Future, Stream ve FutureBuilder
Dart’ın async/await’i Swift ve Kotlin’den gelene tanıdık geliyor, ama altındaki model farklı. Dart tek bir olay döngüsünde çalışıyor; await arka planda iş parçacığı açmıyor, sadece sırayı bırakıyor. Gerçekten ağır bir hesap varsa — büyük bir JSON’u çözmek gibi — bunu ayrı bir isolate’e vermek gerekiyor ve isolate’ler belleği paylaşmıyor, aralarında mesaj geçiyor. Java tarafındaki paylaşımlı iş parçacığı alışkanlığından sonra bunu ayrıca öğrenmek gerekti.
Future tek seferlik bir sonuç, Stream ise akan bir dizi. Firebase kullandığım için çoğu ekranda doğrudan Stream’le çalışıyorum: Firestore’un anlık görüntü akışını StreamBuilder’a veriyorum, dinleyiciyi kurma ve kapatma işini o üstleniyor. Yerel tarafta elle yazdığım dinleyici temizliği burada kendiliğinden hallolmuş oluyor.
FutureBuilder’da ise ilk haftanın en sinsi hatasını yaptım. Future’ı doğrudan build içinde kurunca, her yeniden kurulumda yeni bir istek başlıyor ve ekran sürekli yükleniyor durumuna dönüyor. Future bir kez kurulup saklanmalı.
// Yanlış: her build'de yeni bir istek başlıyor
Widget build(BuildContext context) {
return FutureBuilder<Kullanici>(
future: kullaniciGetir(widget.uid),
builder: (context, anlik) => ...,
);
}
// Doğru: bir kez kur, sakla
late final Future<Kullanici> _kullanici;
@override
void initState() {
super.initState();
_kullanici = kullaniciGetir(widget.uid);
}
Bir de snapshot.hasData kontrolüyle yetinmemek gerekiyor. Hata durumunu ayrıca ele almazsanız, istek başarısız olduğunda kullanıcı sonsuza kadar dönen bir çemberle baş başa kalıyor.
Durum yönetimi ihtiyacı nereden çıkıyor
Flutter’a başlamadan önce durum yönetimi tartışmalarını okuyup “neden bu kadar konuşuluyor” diye düşünmüştüm. İlk ayın sonunda anladım. Sepetteki ürün sayısını hem alt taraftaki butonun hem de üst çubuktaki rozetin bilmesi gerektiğinde, veriyi ortak ataya taşımaktan başka yolunuz yok. Taşıdığınız anda da iki sorun geliyor: değer ara katmanlardan elden ele geçiyor ve tepedeki setState bütün ekranı yeniden kurduruyor.
Bir paket seçmeden önce bu sorunu çerçevenin kendi araçlarıyla yaşamayı tercih ettim. Değeri taşıyan bir dinlenebilir nesne tutup yalnızca onu kullanan küçük widget’ı ona abone etmek, yeniden kurulum sınırını daraltmaya yetiyor. Kalanının çoğunu zaten Firestore akışları hallediyor: veri tek kaynaktan geliyor, ekran ona bakıyor.
Flutter’da öğrenilecek asıl şey widget listesi değil, şu üç soru: bu durum nerede duruyor, değiştiğinde ağacın ne kadarı yeniden kuruluyor ve bu context ağacın neresine bakıyor.
Yerel tarafta yazdığım yıllar boşa gitmedi; yaşam döngüsü, bellek ve arka plan işleri konusundaki alışkanlıklar aynen işe yarıyor. Değişen tek şey, arayüzü “değiştirilen bir nesne” olarak görmeyi bırakıp “durumun bir fonksiyonu” olarak görmek. O eşiği geçtikten sonra geri kalanı hızlandı.
- Flutter
- Dart
- Mobil Geliştirme
- Widget
- Durum Yönetimi
Demir Taşdemir
Mobil Uygulama & Web Geliştirici
2018'den beri yazılım geliştiriyorum. App Store ve Google Play'de 11 uygulama yayınladım; şu an 6 mobil uygulama, 1 e-ticaret platformu ve 1 masaüstü oyun üzerinde çalışıyorum.
Projenizi konuşalım
Flutter, Swift veya Kotlin tarafında bir uygulama fikriniz ya da devralınması gereken bir kod tabanınız varsa yazın; nasıl bir yol izleyeceğimizi birlikte çıkaralım.