Ders 02 / 15
Activity'ler
Activity'nin ne olduğu, bildirim dosyasında nasıl bildirildiği ve bildirilmemiş ya da dışa kapalı bir activity isteğinin platformca nasıl karşılandığının ölçümü.
Yayın Güncelleme
Bu derste
- Activity'yi kullanıcıyla etkileşen tek ekranlık uygulama bileşeni olarak tanımlar
- Bildirim dosyasında bildirilmeyen activity isteğinin karşılıksız kaldığını ölçer
- Başlatıcı süzgecinin hangi activity'nin açılacağını belirlediğini üç durumla gösterir
- Dışa kapalı bir activity'nin çağıran uygulamaya göre iki farklı karşılama aldığını doğrular
İçindekiler
Önceki derste sekiz isteğin biri karşılıksız kaldı: bildirim dosyasında adı geçmeyen bir activity hiç açılmadı. Bu ders o satırın arkasındaki düzeneği okur. Activity, kullanıcıyla etkileşen ve genellikle tek bir ekranı sahiplenen uygulama bileşenidir. Bir uygulama birden çok activity taşıyabilir: bir giriş ekranı, bir profil ekranı, bir ayarlar ekranı. Platform bunların hangisinin var olduğunu uygulamanın koduna bakarak değil, bildirim dosyasına bakarak öğrenir.
Dersin ölçütü yine karşılamadır: bir activity istendiğinde platform isteği nasıl karşılıyor? Aynı çekirdek kural çalışır. Verilen değer istenene eşitse tam, eşit değilse ikame, platform bir koşulu bekliyorsa bekleyen, reddediyorsa karşılıksızdır.
// Fulfillment.kt — bir istegin platformca nasil karsilandigini siniflandirir
sealed interface Outcome {
data class Given(val value: String) : Outcome
data class Waiting(val on: String) : Outcome
data class Refused(val reason: String) : Outcome
}
class Ask(val label: String, val wanted: String, val outcome: Outcome) {
val fulfillment: String = when (outcome) {
is Outcome.Waiting -> "bekleyen"
is Outcome.Refused -> "karsiliksiz"
is Outcome.Given -> if (outcome.value == wanted) "tam" else "ikame"
}
fun report() {
val detail = when (outcome) {
is Outcome.Given -> "verilen=${outcome.value}"
is Outcome.Waiting -> "bekledigi=${outcome.on}"
is Outcome.Refused -> "neden=${outcome.reason}"
}
println("$label: istenen=$wanted | $detail -> $fulfillment")
}
}
fun tally(asks: List<Ask>) {
for (name in listOf("tam", "ikame", "bekleyen", "karsiliksiz")) {
println("$name: ${asks.count { it.fulfillment == name }}")
}
}
Bildirim Dosyasındaki Activity
Her activity bildirim dosyasında bir <activity> öğesiyle bildirilir. Platform bir activity’yi başlatmadan önce bu bildirime bakar. Bildirilmemişse çağrı bir istisnayla sona erer ve activity açılmaz. Başlatıcı ekranı da aynı dosyadan gelir: MAIN eylemini ve LAUNCHER kategorisini taşıyan bir intent süzgeci, activity’yi başlatıcıda görünür kılar.
<application> <activity android:name=".MainActivity" android:exported="true"> <intent-filter> <action android:name="android.intent.action.MAIN" /> <category android:name="android.intent.category.LAUNCHER" /> </intent-filter> </activity> <activity android:name=".ProfileActivity" android:exported="false" /> </application>
Kodda bir activity’yi başlatmak tek satırdır. Platform çağrıyı bildirim dosyasıyla karşılaştırır.
// Platform kodu: ayni uygulamadaki bir activity'yi acik intent ile baslatir
import android.content.Intent
fun openProfile() {
startActivity(Intent(this, ProfileActivity::class.java))
}
Model, platformun bu kuralını kurar. Aşağıdaki ölçüm bildirilen üç activity ve istenen beş activity ile çalışır. launchActivity yalnız tek bir şeye bakar: istenen ad bildirilenler kümesinde var mı. Varsa istenen verilir, yoksa istek reddedilir ve neden bildirimde-yok olarak yazılır.
// Main.kt — bildirilen ve bildirilmeyen activity istekleri
fun launchActivity(target: String, declared: Set<String>): Outcome =
if (target in declared) Outcome.Given(target) else Outcome.Refused("bildirimde-yok")
fun main() {
val declared = setOf("MainActivity", "ProfileActivity", "DetailActivity")
val asks = listOf(
Ask("ana-ekran", "MainActivity", launchActivity("MainActivity", declared)),
Ask("profil", "ProfileActivity", launchActivity("ProfileActivity", declared)),
Ask("detay", "DetailActivity", launchActivity("DetailActivity", declared)),
Ask("ayarlar", "SettingsActivity", launchActivity("SettingsActivity", declared)),
Ask("yazim-hatasi", "ProfilActivity", launchActivity("ProfilActivity", declared)),
)
for (ask in asks) {
ask.report()
}
tally(asks)
}
ana-ekran: istenen=MainActivity | verilen=MainActivity -> tam profil: istenen=ProfileActivity | verilen=ProfileActivity -> tam detay: istenen=DetailActivity | verilen=DetailActivity -> tam ayarlar: istenen=SettingsActivity | neden=bildirimde-yok -> karsiliksiz yazim-hatasi: istenen=ProfilActivity | neden=bildirimde-yok -> karsiliksiz tam: 3 ikame: 0 bekleyen: 0 karsiliksiz: 2
Beş istekten üçü tam, ikisi karşılıksız çıktı. Bildirilen üç activity istendiği gibi verildi. SettingsActivity sınıfı kodda var olabilir, ama bildirim dosyasında yazılı olmadığı için platform onu tanımaz. ProfilActivity ise bir yazım hatasıdır ve aynı yoldan reddedilir. İkisinin de karşılaması karşılıksızdır; ikame ya da bekleyen çıkmaz, çünkü platform “buna yakın bir activity açayım” ya da “bir koşul sağlansın” demez. Bildirilmemiş bir activity için bekleyecek bir koşul da yoktur.
UB1. Model gerçek çağrıdaki istisnayı fırlatmaz; reddi Refused olarak döndürür. Platformda başlatma çağrısı bir istisnayla sona erer, modelde aynı olay karşılıksız bir satır olur.
Başlatıcı Süzgeci
Kullanıcı uygulama simgesine dokunduğunda hangi activity açılır? Cevabı kod vermez, bildirim dosyasındaki süzgeçler verir. LAUNCHER kategorisini taşıyan activity başlatıcının giriş noktasıdır. Bir uygulama geliştiricinin “ana ekran bu olsun” dediği activity ile başlatıcıda gerçekten açılan activity farklı olabilir: süzgeç yanlış activity’ye konmuşsa ya da hiçbirine konmamışsa karşılama değişir.
Aşağıdaki ölçümde istenen her seferinde MainActivity‘dir; süzgeçlerin yeri değişir. launcherEntry kategorisinde LAUNCHER bulunan activity’leri ada göre sıralar ve ilkini verir. Hiçbiri yoksa isteği baslatici-yok nedeniyle reddeder.
// Main.kt — baslatici sugecinin yerine gore karsilama
fun launcherEntry(filters: Map<String, Set<String>>): Outcome {
val entries = filters.filter { "LAUNCHER" in it.value }.keys.sorted()
return if (entries.isEmpty()) Outcome.Refused("baslatici-yok") else Outcome.Given(entries.first())
}
fun main() {
val onMain = mapOf("MainActivity" to setOf("MAIN", "LAUNCHER"), "ProfileActivity" to emptySet<String>())
val none = mapOf("MainActivity" to setOf("MAIN"), "ProfileActivity" to emptySet<String>())
val onProfile = mapOf("MainActivity" to setOf("MAIN"), "ProfileActivity" to setOf("MAIN", "LAUNCHER"))
val asks = listOf(
Ask("suzgec-main-uzerinde", "MainActivity", launcherEntry(onMain)),
Ask("suzgec-yok", "MainActivity", launcherEntry(none)),
Ask("suzgec-profil-uzerinde", "MainActivity", launcherEntry(onProfile)),
)
for (ask in asks) {
ask.report()
}
tally(asks)
}
suzgec-main-uzerinde: istenen=MainActivity | verilen=MainActivity -> tam suzgec-yok: istenen=MainActivity | neden=baslatici-yok -> karsiliksiz suzgec-profil-uzerinde: istenen=MainActivity | verilen=ProfileActivity -> ikame tam: 1 ikame: 1 bekleyen: 0 karsiliksiz: 1
Aynı istek, süzgecin üç ayrı yerleşiminde üç ayrı karşılama aldı. Süzgeç MainActivity üzerindeyken istek tam karşılandı. Süzgeç hiçbir activity’de yokken uygulama başlatıcıda görünmez ve istek karşılıksız kaldı. Süzgeç ProfileActivity üzerindeyken başlatıcı bir activity açar, ama istenen değil, başkası; bu ikamedir. Üçüncü satır, geliştiricinin ana ekranı sandığı activity ile kullanıcının gördüğü activity’nin neden ayrışabildiğini gösterir.
UB2. Model süzgeçlerin yalnız kategorisine bakar. Gerçek süzgeç eylemi, kategoriyi ve veriyi birlikte eşler; bu eşleme Intent’ler ve Intent Süzgeçleri dersinde ölçülür.
Çağırana Dönen Sonuç
Bir activity başka bir activity’nin isteğiyle açılabilir ve işi bitince çağırana bir sonuç döndürebilir. Seçici ekranlar, onay ekranları ve form ekranları bu biçimde çalışır. Çağıran taraf sonucu bir geri çağrıda alır; sonuç iki parçadan oluşur: bir sonuç kodu ve isteğe bağlı bir veri intent’i. Sonuç kodu iki hazır değerden biridir: RESULT_OK ya da RESULT_CANCELED. Kullanıcı geri tuşuna basarak activity’den çıkarsa platform, activity’nin hiçbir şey ayarlamamış olması hâlinde RESULT_CANCELED döndürür.
// Platform kodu: sonuc isteyen activity baslatma
import androidx.activity.result.contract.ActivityResultContracts
val pick = registerForActivityResult(ActivityResultContracts.StartActivityForResult()) { result ->
if (result.resultCode == RESULT_OK) {
showSelection(result.data)
}
}
Çağıranın istediği şey bir seçimdir. Verilen ise kullanıcının ne yaptığına bağlıdır. Aşağıdaki ölçümde istenen her seferinde tamamdır. Kullanıcı kaydeder, geri tuşuna basar, activity çöker ya da kullanıcı henüz karar vermemiştir. resultOf bu dört durumu dört sonuca çevirir: kaydedilirse tamam, geri tuşuyla iptal, çökmede ret nedeni activity-coktu, karar yoksa kullanici beklenir.
// Main.kt — activity sonucunun dort hali
fun resultOf(event: String): Outcome = when (event) {
"kaydet" -> Outcome.Given("tamam")
"geri" -> Outcome.Given("iptal")
"cokme" -> Outcome.Refused("activity-coktu")
else -> Outcome.Waiting("kullanici")
}
fun main() {
val events = listOf("kaydet", "geri", "cokme", "dusunuyor")
val asks = events.map { Ask("sonuc-$it", "tamam", resultOf(it)) }
for (ask in asks) {
ask.report()
}
tally(asks)
}
sonuc-kaydet: istenen=tamam | verilen=tamam -> tam sonuc-geri: istenen=tamam | verilen=iptal -> ikame sonuc-cokme: istenen=tamam | neden=activity-coktu -> karsiliksiz sonuc-dusunuyor: istenen=tamam | bekledigi=kullanici -> bekleyen tam: 1 ikame: 1 bekleyen: 1 karsiliksiz: 1
Dört durum dört karşılamayı birer kez üretti. Burada ders için asıl bilgi şudur: iptal bir hata değildir. Platform geri tuşuna basan kullanıcıya bir sonuç koduyla yanıt verir ve çağıran kod bunu normal bir yol olarak karşılamak zorundadır. Çağıranın yalnızca RESULT_OK dalını yazması, ikame satırını ele almayan bir kod demektir. Çöken activity ise çağırana bir sonuç bile döndürmez; çağıranın gördüğü şey “sonuç yok”tur ve bu karsiliksiz satırında modellenmiştir. bekleyen satırı gerçek bir sonuç değildir; kullanıcının henüz karar vermediğini söyler.
UB3. Model sonuç kodunu ve verilen değeri tek bir dizgede birleştirir. Gerçek platformdaki ayrık resultCode ve data ikilisi, ve sonucun süreç sonlanmasından sonra çağıran activity yeniden oluşturulduğunda teslim edilmesi ölçülmedi.
exported Bildirimi Eksikse
Sınırlayıcı ölçümde android:exported değeri bir girdi olarak alınır. Gerçek kural bir adım daha içerir: bir activity bir intent süzgeci taşıyorsa exported değeri bildirimde açıkça yazılmak zorundadır. Süzgeç varsa ve değer yazılmamışsa, derleme aracı bildirim dosyasını reddeder ve uygulama yüklenemez. Süzgeç yoksa ve değer yazılmamışsa activity dışa kapalı sayılır.
Kuralın mantığı şudur: süzgeç taşıyan bir activity başkalarınca bulunmak için yazılmıştır, ama bu bulunabilirliğin gerçekten istenip istenmediği yazarın açık kararına bırakılır. Aşağıdaki ölçüm istenen şeyi kurulum olarak alır. Verilen, bildirimin kuralı geçip geçmediğidir.
// Main.kt — exported bildirimi ve suzgec
fun checkEntry(hasFilter: Boolean, exportedWritten: Boolean?): Outcome =
if (hasFilter && exportedWritten == null) Outcome.Refused("exported-eksik")
else Outcome.Given("kurulum")
fun main() {
val asks = listOf(
Ask("suzgec-var-exported-yok", "kurulum", checkEntry(true, null)),
Ask("suzgec-var-exported-true", "kurulum", checkEntry(true, true)),
Ask("suzgec-yok-exported-yok", "kurulum", checkEntry(false, null)),
Ask("suzgec-yok-exported-false", "kurulum", checkEntry(false, false)),
)
for (ask in asks) {
ask.report()
}
tally(asks)
}
suzgec-var-exported-yok: istenen=kurulum | neden=exported-eksik -> karsiliksiz suzgec-var-exported-true: istenen=kurulum | verilen=kurulum -> tam suzgec-yok-exported-yok: istenen=kurulum | verilen=kurulum -> tam suzgec-yok-exported-false: istenen=kurulum | verilen=kurulum -> tam tam: 3 ikame: 0 bekleyen: 0 karsiliksiz: 1
Reddedilen tek durum, süzgeç taşıyıp exported değerini yazmayan activity’dir. Bu ret, uygulama çalışırken değil, kurulum ya da derleme anında gelir; bu yüzden hata, kullanıcının karşısına çıkmadan geliştiricinin önüne çıkar. Bu kuralın getirdiği alışkanlık: her süzgeçli activity için exported değerini bilerek yazmak. Başlatıcı activity’si true olmak zorundadır; çünkü başlatıcı uygulamanın dışındaki bir sistem bileşenidir.
UB4. Model, derleme aracının ve platformun bu kontrolü hangi sürümden itibaren yaptığını ayırt etmez. Kuralın uygulandığı en düşük hedef sürüm ve eski sürümlerdeki örtük varsayılan ölçülmedi.
Sınırlayıcı Ölçüm
Karşılama kuralı nedeni görmez, ama nedenin değiştiği yer vardır: android:exported. Bu öznitelik activity’nin başka uygulamalardan çağrılıp çağrılamayacağını söyler. false ise yalnız aynı uygulamanın kodu activity’yi açabilir. Bildirilmiş ve dışa kapalı bir activity, çağıranın kim olduğuna göre iki farklı karşılama alır. Yani aynı activity, aynı bildirim, farklı çağıran: tam ile karşılıksız arasındaki fark çağıranın paketinden gelir.
Aşağıdaki ölçüm openFrom fonksiyonuyla bunu kurar. Activity bildirimde yoksa ret nedeni bildirimde-yok, dışa kapalıysa ve çağıran başka bir uygulamaysa disa-kapali olur. Bunların dışında istenen activity verilir.
// Main.kt — disa kapali activity ve cagiran uygulama
fun openFrom(target: String, exported: Map<String, Boolean>, caller: String, owner: String): Outcome {
val isExported = exported[target] ?: return Outcome.Refused("bildirimde-yok")
return if (!isExported && caller != owner) Outcome.Refused("disa-kapali") else Outcome.Given(target)
}
fun main() {
val exported = mapOf("ProfileActivity" to false, "ShareActivity" to true)
val owner = "app.ana"
val asks = listOf(
Ask("ayni-uygulama", "ProfileActivity", openFrom("ProfileActivity", exported, "app.ana", owner)),
Ask("baska-uygulama", "ProfileActivity", openFrom("ProfileActivity", exported, "app.diger", owner)),
Ask("disa-acik", "ShareActivity", openFrom("ShareActivity", exported, "app.diger", owner)),
Ask("bildirilmemis", "SettingsActivity", openFrom("SettingsActivity", exported, "app.diger", owner)),
)
for (ask in asks) {
ask.report()
}
println(asks.filter { it.fulfillment == "karsiliksiz" }.map { it.label })
}
ayni-uygulama: istenen=ProfileActivity | verilen=ProfileActivity -> tam baska-uygulama: istenen=ProfileActivity | neden=disa-kapali -> karsiliksiz disa-acik: istenen=ShareActivity | verilen=ShareActivity -> tam bildirilmemis: istenen=SettingsActivity | neden=bildirimde-yok -> karsiliksiz [baska-uygulama, bildirilmemis]
ayni-uygulama ve baska-uygulama aynı activity’yi ister; ilki tam, ikincisi karşılıksızdır ve fark yalnız çağıranın paketidir. disa-acik dışa açık bir activity’yi başka uygulamadan ister ve tam karşılanır. Son satır, iki karşılıksız isteği tek listede toplar: ikisinin karşılaması aynı, nedenleri farklıdır. Birincisi bildirimde bulunan ama dışa kapalı bir activity’den, ikincisi bildirimde hiç bulunmayan bir activity’den gelir. Karşılama kategorisi bu ayrımı taşımaz; ayrım neden= alanındadır.
UB5. Model exported öznitelik değerini bir girdi olarak alır. Gerçek platformda varsayılan değer süzgecin bulunup bulunmamasına bağlıdır ve burada ölçülmedi.
Özet
- Bu dersin ölçütü: karşılama. Activity, kullanıcıyla etkileşen uygulama bileşenidir ve platform onu bildirim dosyasındaki
<activity>öğesinden tanır. Bu dersin karşılaması: karşılıksız; ölçülen istek bildirilmemiş ya da dışa kapalı activity’dir ve platform ona yakın bir activity açmaz. - Bildirilmeyen bir activity isteği karşılıksız kalır; yazım hatası da aynı yoldan reddedilir. Beş istekte 3 tam ve 2 karşılıksız çıktı.
- Başlatıcı süzgecinin yeri karşılamayı belirler:
MainActivityüzerinde tam, hiçbirinde karşılıksız,ProfileActivityüzerinde ikame. android:exported="false"olan activity aynı uygulamadan tam, başka uygulamadan karşılıksız karşılanır.- İki karşılıksız istek farklı nedenlerden gelebilir; kural nedeni görmez, neden ayrı bir alanda tutulur.
Sonraki Adım
Bildirilen bir activity açıldığında yaşamı orada bitmez: kullanıcı ekranı döndürür, başka uygulamaya geçer, geri döner. Sıradaki ders, Activity Yaşam Döngüsü, bu geçişlerde activity’nin durumunun nasıl korunduğunu ve nasıl kaybolduğunu aynı kuralla ölçer.
İlerlemeni kaydetmek ve not almak için Giriş yap
Notlarım
Not almak için giriş yapmalısın.