Tüm yazılar Yazılım

Java ile Android: İlk Uygulamamdan Kalan Dersler

2020'de Java ile ilk Android uygulamamı yazarken ekranı döndürünce verilerin uçmasından tutun da bellek sızıntılarına kadar her tuzağa tek tek düştüm. Bu yazıda yaşam döngüsünü, context sızıntılarını, AsyncTask döneminden bugüne kalan alışkanlıkları ve null kontrolünü kendi hatalarım üzerinden anlatıyorum.

DT
Demir Taşdemir Mobil Uygulama & Web Geliştirici
— dk okuma

2020'de Java ile ilk Android uygulamamı yazarken, ekranı yatay çevirdiğimde formda doldurduğum her şeyin uçtuğunu gördüm. O an bunun bir hata olduğunu sandım. Değildi; sistemin tasarımı buydu ve ben onu bilmiyordum. Bu yazı, o dönemde tek tek düştüğüm çukurların listesi.

Python'dan geliyordum. Orada bir betik baştan sona akar, ben nerede duracağına karar veririm. Android'de ise kodu ben çağırmıyordum, sistem beni çağırıyordu. Bu tersine dönüşü anlamak, Java sözdizimini öğrenmekten çok daha uzun sürdü.

Yaşam döngüsü bir öneri değil, sözleşme

Bir Activity'nin onCreate, onStart, onResume, onPause, onStop, onDestroy geri çağrıları var. İlk başta bunları “açılırken çalışan yerler” diye okudum ve her şeyi onCreate içine yığdım. Sonuç: kullanıcı başka bir uygulamaya geçip döndüğünde uygulamam ölü bir ekran gösteriyordu.

Doğru okuma şu: bu geri çağrılar, sistemin bana “kaynak alabilirsin” ve “kaynağı bırak” dediği anlar. Kameraya, konuma, bir dinleyiciye ihtiyacım varsa onStart veya onResume içinde alırım; karşılığını onStop veya onPause içinde bırakırım. Aldığım yerin simetriğinde bırakmazsam, uygulama arka plandayken pil yakmaya devam eder.

Bunu kafama kazıyan somut olay şuydu: Firestore dinleyicisini onCreate içinde açtım, kapatmayı hiç yazmadım. Kullanıcı ekranlar arasında gidip geldikçe aynı dinleyiciden birden fazla kopya birikti ve tek bir veri değişiminde arayüz aynı işi üst üste yaptı.

@Override
protected void onStart() {
    super.onStart();
    listener = db.collection("ilanlar")
        .addSnapshotListener((snapshot, error) -> {
            if (error != null) return;
            if (snapshot == null) return;
            guncelle(snapshot.getDocuments());
        });
}

@Override
protected void onStop() {
    super.onStop();
    if (listener != null) {
        listener.remove();
        listener = null;
    }
}

Kural basit: bir şeyi açtıysan, onu kapatacağın geri çağrıyı aynı anda yaz. Sonra yazarım deme, sonra yazmıyorsun.

Konfigürasyon değişikliği: ekranı döndürmek uygulamayı yeniden doğurur

Android'de ekran döndürme, dil değişimi, koyu temaya geçiş gibi durumlarda Activity varsayılan olarak yok edilip yeniden oluşturulur. Yani onDestroy ve ardından yeni bir onCreate. Alan değişkenlerinde tuttuğun her şey sıfırlanır.

İlk çözümüm manifest'e android:configChanges yazıp yeniden oluşturmayı engellemekti. Çalıştı ama yanlıştı; çünkü o zaman farklı ekran boyutlarına göre yüklenmesi gereken düzen dosyaları da devreye girmiyordu. Yatay tasarımım hiç görünmedi.

Doğrusu, durumu kaybolmayacak bir yerde tutmak. Küçük ve serileştirilebilir veriler için onSaveInstanceState yeterli:

@Override
protected void onSaveInstanceState(@NonNull Bundle outState) {
    super.onSaveInstanceState(outState);
    outState.putString("aramaMetni", aramaMetni);
    outState.putInt("sayfa", sayfa);
}

@Override
protected void onCreate(Bundle savedInstanceState) {
    super.onCreate(savedInstanceState);
    setContentView(R.layout.activity_liste);
    if (savedInstanceState != null) {
        aramaMetni = savedInstanceState.getString("aramaMetni", "");
        sayfa = savedInstanceState.getInt("sayfa", 0);
    }
}
Bundle bir veri deposu değil

Bundle'a büyük listeler, resimler veya tüm ağ cevabını koyma. Bu alan sistem tarafından sınırlıdır ve taşırsan uygulama çöker. Oraya “kullanıcının nerede kaldığı” bilgisini koy, verinin kendisini değil; veriyi yeniden çek ya da yerel veritabanından oku.

Bunu test etmenin en kolay yolu geliştirici seçeneklerindeki “etkinlikleri saklama” ayarını açmak. Uygulaman arka plana atıldığında sistem onu hemen öldürür, sen de kullanıcıların gerçekte yaşadığı senaryoyu her seferinde görürsün. İlk açtığımda uygulamamın yarısı çöktü, ki bu iyi bir haberdi.

Context sızıntıları: en sinsi hata

Java'da çöp toplayıcı, bir nesneye başka biri hâlâ referans tutuyorsa onu temizlemez. Android'de bu, Activity için ölümcül. Activity yok edilir ama ona referans veren uzun ömürlü bir nesne varsa, o Activity ve altındaki tüm görünüm ağacı bellekte kalır.

En sık yaptığım hata statik alan tutmaktı:

// yanlış
public class Uygulama {
    public static Context context; // Activity buraya atanırsa asla ölmez
}

İkinci sık hata: iç sınıflar. Java'da statik olmayan bir iç sınıf, dış sınıfına gizli bir referans taşır. Bir Handler ya da uzun süren bir görev iç sınıf olarak yazıldığında, iş bitene kadar Activity'yi elinde tutar. Kullanıcı ekranı kapatsa bile.

Bugün alışkanlığım şu: uzun ömürlü bir yere context vereceksem getApplicationContext() veririm; ama arayüz işi yapacaksam (dialog açmak, tema okumak, düzen şişirmek) mutlaka Activity context'i kullanırım. İkisini karıştırmak da ayrı bir hata sınıfı; application context ile dialog açmaya çalışmak çalışma zamanında patlar.

Sızıntıyı gözle göremezsin

Bellek sızıntısı çökme üretmez, yavaşlık üretir. Uygulama uzun kullanımda takılmaya başlar ve sonunda bellek yetmediği için öldürülür. Android Studio'daki bellek profilleyicisinde birkaç kez ekran açıp kapatıp yığın anlık görüntüsü almak, aynı Activity'nin bellekte kaç kopya durduğunu doğrudan gösterir.

AsyncTask döneminden bugüne kalan

Ben Android'e girdiğimde arka plan işi denince akla ilk gelen AsyncTask'ti. Yapısı davetkârdı: doInBackground ile işi yap, onPostExecute ile arayüzü güncelle. Sorun, yaşam döngüsünden habersiz olmasıydı. Activity kapandıktan sonra onPostExecute çalışıyor ve artık var olmayan bir görünümü güncellemeye kalkıyordu. Aldığım hataların bir kısmı buradan geliyordu.

AsyncTask uzun süredir kullanımdan kaldırılmış durumda ve yenisini yazarken kimse ona dönmüyor. Ama ondan kalan ders hâlâ geçerli ve bugün Kotlin coroutine ya da Flutter'daki Future ile çalışırken de aynı: arka plandaki iş bittiğinde, sonucu göstereceğin ekran hâlâ hayatta mı diye sormak zorundasın.

Java tarafında elimde kalan pratik korunma şuydu: sonucu işlemeden önce Activity'nin durumunu kontrol etmek.

private void sonucGeldi(List<Ilan> ilanlar) {
    if (isFinishing() || isDestroyed()) return;
    adapter.setItems(ilanlar);
}

Fragment'te durum daha da ince. Fragment'in kendisi hayatta olabilir ama görünümü yok edilmiş olabilir; getView() null döner ve ona dokunursan çökersin. Bu yüzden Fragment içinde arka plan sonucu işlerken görünümün varlığını ayrıca kontrol etmek gerekiyor. Bu ayrımı öğrenene kadar epey hata raporu topladım.

Null kontrolü: Java'nın en pahalı boşluğu

NullPointerException, Android'de karşılaştığım en yaygın çökme sebebiydi. Java'da bir değişkenin null olup olamayacağı tip sisteminde yazmaz; sen bilirsin ya da bilmezsin. Kotlin'in ? işaretiyle çözdüğü şeyi Java'da disiplinle çözmek zorundasın.

Firebase ile çalışırken bu daha da belirginleşiyor. Firestore'dan gelen bir belgede beklediğin alan olmayabilir; kullanıcı eski sürümle yazmış olabilir, alan hiç yazılmamış olabilir. getString("ad") pekâlâ null döner. Ben bir dönem bunu her çağrı yerinde tek tek kontrol ettim; kod okunmaz hale geldi.

Sonra yaklaşımı değiştirdim: null kontrolünü sınırda yapıp içeriye temiz veri sokmak. Ağdan gelen ham veriyi modele çevirdiğim tek bir yerde eksikleri varsayılana çekiyorum, o noktadan sonrası null düşünmüyor.

public static Ilan fromDocument(DocumentSnapshot doc) {
    Ilan ilan = new Ilan();
    ilan.id = doc.getId();
    String ad = doc.getString("ad");
    ilan.ad = (ad != null) ? ad : "Isimsiz ilan";
    Long fiyat = doc.getLong("fiyat");
    ilan.fiyat = (fiyat != null) ? fiyat : 0L;
    return ilan;
}

Yanına @NonNull ve @Nullable işaretlerini koymak da işe yarıyor. Derleyiciyi durdurmaz ama Android Studio uyarı verir; bu uyarıların çoğu gerçek hataydı.

Ekip halinde çalışmadığım için öğrendiklerim

Tek başıma çalıştığım için kod incelemesi yapan kimse yoktu. Bunun bedelini şöyle ödedim: aynı hatayı üç ayrı ekranda üç ayrı şekilde yazdım, sonra üçünü de ayrı ayrı düzelttim.

Bunu telafi etmek için kendime iki alışkanlık kazandırdım. Birincisi, bir hatayı düzeltince o düzeltmenin diğer ekranlarda da gerekip gerekmediğini aramak. İkincisi, geliştirici seçeneklerini açık tutmak: “etkinlikleri saklama”, arka plan işlem sınırı, animasyon ölçeğini kapatmak. Bunlar gerçek cihaz davranışını benim geliştirme koşullarımdan daha sert hale getiriyor ve hatalar bana çıkıyor, kullanıcıya değil.

Java hâlâ neden öğrenilmeye değer

Bugün Android tarafında yeni proje başlatan çoğu kişi Kotlin'e gidiyor; ben de Kotlin ile uygulama yayınladım ve 2026'da Flutter'a geçtim. Buna rağmen Java'yı öğrenmiş olmama pişman değilim, üç sebeple.

  • Android'in altı hâlâ büyük ölçüde Java dünyasının kavramlarıyla çalışıyor. Yaşam döngüsü, çöp toplama, ana iş parçacığı kuralı, Intent ve Bundle — bunlar dil değişince kaybolmuyor.
  • Var olan kodun büyük kısmı Java. Bir işe girdiğinizde sıfırdan proje açmıyorsunuz; yıllardır yaşayan bir kod tabanına giriyorsunuz. Onu okuyamıyorsanız katkı da veremezsiniz.
  • Kotlin'in çözdüğü sorunları anlamanın en iyi yolu, o sorunları yaşamış olmak. Null güvenliğinin neden bu kadar önemsendiğini, Java'da bir gece NullPointerException kovaladıktan sonra çok daha iyi anlıyorsun.

Aynı şey benim için Swift ve Flutter tarafında da geçerli oldu. Platform ne olursa olsun soru değişmiyor: bu ekran ne zaman doğuyor, ne zaman ölüyor, ölürken elinde tuttuğu şeyleri bırakıyor mu?

Bugün yeni başlayan birine söyleyeceklerim

Dille değil, yaşam döngüsüyle başla. Bir Activity'nin tüm geri çağrılarına birer günlük satırı koy, uygulamayı aç, döndür, arka plana at, geri getir, kapat. Konsolda çıkan sırayı kendi gözünle gör. Bu on dakikalık deney, bu yazının tamamından daha öğretici.

Sonra tek bir ekran yaz ve o ekranı bilinçli olarak zorla: internet yokken ne oluyor, veri gelmeden kullanıcı geri tuşuna basarsa ne oluyor, ekranı sürekli döndürürsen ne oluyor. Uygulamayı kırmayı öğrenmek, uygulamayı yazmayı öğrenmenin diğer yarısı.

Benim ilk uygulamam iyi bir uygulama değildi. Ama ondan kalan alışkanlıklar — açtığını kapatmak, sonucu kullanmadan önce ekranın hayatta olduğunu doğrulamak, null'ı sınırda temizlemek — bugün Flutter ile yazarken de yanımda duruyor.

  • Java
  • Android
  • Mobil Geliştirme
  • Yaşam Döngüsü
  • Bellek Yönetimi
  • Firebase
Paylaş: LinkedIn X WhatsApp
DT

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.

Mobil tarafta bir işiniz mi var?

Java, Kotlin, Swift ve Flutter ile yayınlanmış uygulamalar geliştirdim. Projenizi konuşmak isterseniz bana yazın.