---
title: 'Mimari Bileşenler ve Veri Katmanı'
source: 'https://academia.sh/tr/kurslar/android-mimari'
language: tr
updated: '2026-10-21T05:02:36+00:00'
license: 'CC BY-SA 4.0'
lessons: 14
outcomes:
    - 'Katmanlı uygulama mimarisini kurar'
    - 'Eş yordam ve akışlarla eşzamansız veri akışı yönetir'
    - 'Yerel ve uzak veri kaynaklarını depo katmanında birleştirir'
    - 'Bir okumanın yanıtının bellekten mi, diskten mi, ağdan mı geldiğini yoksa varsayılan değere mi düştüğünü ayırt eder'
---

# Mimari Bileşenler ve Veri Katmanı

Bir uygulamanın ekranı aynı soruyu birden çok yere sorar ve yanıt her seferinde aynı yerden gelmez: profil adı görünüm modelinin belleğinden, tema ayarı aygıttaki anahtar–değer deposundan, stok bilgisi ağdan gelir; hiçbiri yanıt vermezse kodun yazdığı ilk değer görünür. Kurs her düzeneği tek bir soruyla ölçer — bu okumanın yanıtı nereden geliyor? Katman modeli, görünüm modeli, durum tutucular, gözlemlenebilir durum, tek yönlü veri akışı, bağımlılık enjeksiyonu, depo kalıbı, yerel veritabanı, anahtar–değer saklama, dosya depolama, ağ istemcisi, önbellekleme, sayfalı yükleme ve arka plan işleri aynı çerçeveden geçirilir.

## Uygulama Mimarisi

Arayüz, alan ve veri katmanları, yapılandırma değişikliğine dayanıklı görünüm modeli, durum tutucular, akış tabanlı gözlemlenebilir durum, tek yönlü veri akışı ve bağımlılık enjeksiyonu.

### Katman Modeli

Source: https://academia.sh/tr/kurslar/android-mimari/katman-modeli

Katman modeli uygulamayı arayüz, alan ve veri katmanlarına böler; bağımlılık yalnız içe doğru gider. Ders bir okumanın yanıtının bellekten, diskten, ağdan ya da varsayılandan geldiğini ölçer.

Önceki kurs Compose ile yazılan arayüzde verinin ağaçta hangi yöne gittiğini ölçtü. O kursun ölçü ekseni burada sürmez. Artık veri arayüz ağacında değil, uygulamanın katmanları arasında dolaşıyor ve her okuma için tek bir soru soruyoruz: **bu okumanın yanıtı nereden geliyor?** Yanıtın dört kökeni var: bellek, disk, ağ ve varsayılan.

#### Okumanın Kökeni

Veri katmanında (data layer) bir okuma bir anahtar için basamakları sırayla dener: önce bellek, sonra disk, sonra ağ. Hiçbiri anahtarı bilmiyorsa okuma kendi varsayılanını kullanır. Zincirin her halkasına basamak diyoruz. Aşağıdaki çekirdek, bir okumanın yanıtını hangi basamağın verdiğini sınıflar; boş dizgi de bir yanıttır, yani basamak anahtarı boş dizgiyle bilse bile yanıt o basamaktan gelir.

```kotlin
// Origin.kt — bir okumanin yanitinin hangi basamaktan geldigini siniflandirir
class Tier(val origin: String, private val entries: Map<String, String>) {
    init {
        require(origin in listOf("bellek", "disk", "ag")) { "gecersiz basamak: $origin" }
    }

    fun lookup(key: String): String? = entries[key]
}

class Read(val label: String, val key: String, chain: List<Tier>, fallback: String) {
    private val hit: Tier? = chain.firstOrNull { it.lookup(key) != null }
    val value: String = hit?.lookup(key) ?: fallback
    val origin: String = hit?.origin ?: "varsayilan"

    fun show() {
        println("$label: anahtar=$key | deger=$value -> $origin")
    }
}

fun countOrigins(reads: List<Read>) {
    for (name in listOf("bellek", "disk", "ag", "varsayilan")) {
        println("$name: ${reads.count { it.origin == name }}")
    }
}
```

`Tier` bir basamaktır ve yalnız üç adı kabul eder. `Read` zinciri baştan sona dolaşır, ilk yanıtı alır. `countOrigins` dört kökeni sayar. Kural yalnız sıraya bakar: değerin yeni ya da eski olduğunu görmez.

Zincirin sıralı olmasının nedeni maliyettir, doğruluk değildir. Bellek en hızlı ve en kısa ömürlü basamaktır; disk süreç sonlansa da kalır; ağ en uzaktadır. Okuma ucuz olandan pahalı olana doğru ilerler ve ilk yanıtta durur. Bu düzen hız kazandırır, ama ucuz basamak eskimişse eski değer yanıt olur. Bu derste bunu iki kez ölçeceğiz.

#### Üç Katman

Katman modeli uygulamayı sorumluluğa göre en az iki, bu derste üç parçaya böler. Arayüz katmanı (UI layer) ekranı çizer ve kullanıcının olayını alır. Alan katmanı (domain layer) isteğe bağlıdır ve ikisinin arasında durur; iş kurallarını taşır, ham veriyi ekranın istediği biçime çevirmek burada yapılır. Alan katmanı olmayan uygulamada arayüz doğrudan veri katmanına bağlanır. Veri katmanı verinin nereden okunduğunu ve nereye yazıldığını bilir: bellek, disk ve ağ yalnız bu katmanda görünür.

Bağımlılık yalnız içe doğru gider: arayüz alana, alan veriye bağlıdır; veri katmanı arayüzü bilmez. Platformda bu iskelet şöyle görünür.

```kotlin
// Platform kodu: CartScreen
import androidx.lifecycle.ViewModel

class CartViewModel(private val getCart: GetCartUseCase) : ViewModel() {
    val items = getCart()
}

class GetCartUseCase(private val repository: CartRepository) {
    operator fun invoke() = repository.items
}

class CartRepository(private val local: CartLocalSource, private val remote: CartRemoteSource) {
    val items = local.items
}
```

Arayüz tarafı yalnız `CartViewModel`'i tanır; `CartRemoteSource`'u görmez. Bu parça platform kodudur ve burada çıktı, süre ya da dosya yolu iddia etmiyoruz. Model, platformun bu kuralını kurar; model platformun kendisi değildir. Modelde katmanlar sınıf değil, bir okumanın geçtiği basamaklardır.

Yönün içe doğru olmasının pratik bir sonucu vardır: veri katmanında disk ya da ağ değişirse alan ve arayüz katmanı aynı kalır, çünkü onlar yalnız okumanın sonucunu ister. Tersi de doğrudur: ekran değişirse veri katmanı bundan haberdar olmaz. Her katman kendi sorusuna yanıt verir. Arayüz "ne gösterilecek", alan "hangi kurala göre", veri "nereden alınacak" sorusunu yanıtlar. Bu kurs üçüncü soruyu ölçer.

#### Sekiz Okuma, Dört Köken

Bir alışveriş uygulaması açılıyor. Ağdan gelen ham veri iki işlevle yayılır: `syncTo` ağdaki değerleri diske kopyalar, `promote` diskteki ya da ağdaki değerleri belleğe taşır. İkisi de aldığı eşlemi günceller ve onu döndürür; ölçülen basamaklar bu dönüşlerdir.

```kotlin
// Main.kt
fun syncTo(disk: MutableMap<String, String>, network: Map<String, String>, keys: List<String>): Map<String, String> {
    for (key in keys) network[key]?.let { disk[key] = it }
    return disk
}

fun promote(memory: MutableMap<String, String>, source: Map<String, String>, keys: List<String>): Map<String, String> {
    for (key in keys) source[key]?.let { memory[key] = it }
    return memory
}

fun main() {
    val network = mapOf(
        "profil.ad" to "Deniz",
        "urun.fiyat" to "120",
        "urun.stok" to "7",
        "sepet.adet" to "3",
    )
    val disk = syncTo(mutableMapOf("ayar.tema" to "koyu", "ayar.dil" to "tr"), network, listOf("sepet.adet", "profil.ad"))
    val memory = promote(mutableMapOf("urun.fiyat" to "100"), disk, listOf("profil.ad"))
    println("disk anahtarlari: ${disk.keys.sorted()}")
    println("bellek anahtarlari: ${memory.keys.sorted()}")

    val chain = listOf(Tier("bellek", memory), Tier("disk", disk), Tier("ag", network))
    val reads = listOf(
        Read("profil-adi", "profil.ad", chain, "misafir"),
        Read("urun-fiyati", "urun.fiyat", chain, "0"),
        Read("tema", "ayar.tema", chain, "acik"),
        Read("dil", "ayar.dil", chain, "tr"),
        Read("sepet-adedi", "sepet.adet", chain, "0"),
        Read("stok", "urun.stok", chain, "0"),
        Read("kampanya", "kampanya.kod", chain, "yok"),
        Read("bildirim", "bildirim.acik", chain, "kapali"),
    )
    reads.forEach { it.show() }
    countOrigins(reads)
}
```

```text
disk anahtarlari: [ayar.dil, ayar.tema, profil.ad, sepet.adet]
bellek anahtarlari: [profil.ad, urun.fiyat]
profil-adi: anahtar=profil.ad | deger=Deniz -> bellek
urun-fiyati: anahtar=urun.fiyat | deger=100 -> bellek
tema: anahtar=ayar.tema | deger=koyu -> disk
dil: anahtar=ayar.dil | deger=tr -> disk
sepet-adedi: anahtar=sepet.adet | deger=3 -> disk
stok: anahtar=urun.stok | deger=7 -> ag
kampanya: anahtar=kampanya.kod | deger=yok -> varsayilan
bildirim: anahtar=bildirim.acik | deger=kapali -> varsayilan
bellek: 2
disk: 3
ag: 1
varsayilan: 2
```

Sekiz okuma dört köken verdi: bellek 2, disk 3, ağ 1, varsayılan 2. İki ayrıntı kuralı gösterir. `urun-fiyati` bellekte 100 bulur; ağda 120 durduğu hâlde okuma belleğe döner, çünkü kural sırayı görür, tazeliği görmez. `stok` ağdan gelir: `syncTo` yalnız `sepet.adet` ve `profil.ad`'ı kopyaladı, `urun.stok` diske hiç inmedi. `kampanya` ve `bildirim` hiçbir basamakta yok; okumaların kendi varsayılanları yanıt oldu.

#### Alan Katmanı Kökeni Değiştirmez

Alan katmanı ağdan gelen ham değeri arayüzün istediği biçime çevirir. Ağ fiyatı kuruş olarak verir; ekran lira ister. Dönüştürme işlevi yeni bir ad taşır ve veri katmanının zincirine yalnız çevrilmiş eşlemi verir.

```kotlin
// Domain.kt — alan katmani: agdan gelen kurus degerini arayuz bicimi olan liraya cevirir
fun toDisplay(raw: Map<String, String>): Map<String, String> =
    raw.mapValues { (key, value) -> if (key.endsWith(".fiyat")) (value.toInt() / 100).toString() else value }
```

Arayüz yalnız zinciri görür: değerin ham mi çevrilmiş mi olduğunu bilmez, hangi basamaktan geldiğini ise okumanın kökeninden öğrenir. Bellek `promote` dönüşüyle kurulur: `promote` yalnız kampanya kodunu belleğe taşımak ister, ama ağ yanıtında o anahtar yoktur, bu yüzden bellek boş kalır. Ağ basamağı çevrilmiş eşlemi taşır.

```kotlin
// Main.kt — alan katmani degeri cevirir, koken yine ag
fun promote(memory: MutableMap<String, String>, source: Map<String, String>, keys: List<String>): Map<String, String> {
    for (key in keys) source[key]?.let { memory[key] = it }
    return memory
}

fun main() {
    val raw = mapOf("urun.fiyat" to "12000", "urun.stok" to "7")
    val network = toDisplay(raw)
    println("ham ag verisi: urun.fiyat=${raw["urun.fiyat"]}")
    println("arayuz bicimi: urun.fiyat=${network["urun.fiyat"]}")

    val memory = promote(mutableMapOf(), network, listOf("kampanya.kod"))
    val chain = listOf(Tier("bellek", memory), Tier("disk", emptyMap()), Tier("ag", network))
    val reads = listOf(
        Read("urun-fiyati", "urun.fiyat", chain, "0"),
        Read("stok", "urun.stok", chain, "0"),
        Read("kampanya", "kampanya.kod", chain, "yok"),
    )
    reads.forEach { it.show() }
    countOrigins(reads)
}
```

```text
ham ag verisi: urun.fiyat=12000
arayuz bicimi: urun.fiyat=120
urun-fiyati: anahtar=urun.fiyat | deger=120 -> ag
stok: anahtar=urun.stok | deger=7 -> ag
kampanya: anahtar=kampanya.kod | deger=yok -> varsayilan
bellek: 0
disk: 0
ag: 2
varsayilan: 1
```

Ham değer 12000, arayüz biçimi 120. İki ürün okuması ağdan geldi: bellek boştu, disk boştu. Alan katmanı değeri çevirdi ama yanıtı veren basamağı değiştirmedi.

**UM1.** Model dönüştürmeyi ağ basamağının girdisine uygular. Platformda dönüştürme okumadan sonra, alan katmanında da yapılabilir; bu da kökeni değiştirmez, ama modelde bu ayrım kurulmaz.

#### Sınırlayıcı Ölçüm

Aynı anahtarı belleğe taşıyalım ve sonra ağdaki değeri değiştirelim. İlk okumada `promote` yalnız stoğu taşımak ister; ağda stok olmadığından bellek boş kalır ve okuma ağdan gelir. `promote` fiyatı belleğe taşıyınca ağda fiyat 150 olsa bile ikinci okuma belleğe döner ve eski değeri okur.

```kotlin
// Main.kt — ayni anahtar bellege tasinirsa ayni okuma bellek olur
fun promote(memory: MutableMap<String, String>, source: Map<String, String>, keys: List<String>): Map<String, String> {
    for (key in keys) source[key]?.let { memory[key] = it }
    return memory
}

fun main() {
    val before = toDisplay(mapOf("urun.fiyat" to "12000"))
    val emptyMemory = promote(mutableMapOf(), before, listOf("urun.stok"))
    val first = Read("fiyat-ilk", "urun.fiyat", listOf(Tier("bellek", emptyMemory), Tier("ag", before)), "0")
    first.show()

    val memory = promote(mutableMapOf(), before, listOf("urun.fiyat"))
    val after = toDisplay(mapOf("urun.fiyat" to "15000"))
    val second = Read("fiyat-sonra", "urun.fiyat", listOf(Tier("bellek", memory), Tier("ag", after)), "0")
    second.show()
    println("ag simdi: ${after["urun.fiyat"]}")
    countOrigins(listOf(first, second))
}
```

```text
fiyat-ilk: anahtar=urun.fiyat | deger=120 -> ag
fiyat-sonra: anahtar=urun.fiyat | deger=120 -> bellek
ag simdi: 150
bellek: 1
disk: 0
ag: 1
varsayilan: 0
```

İki okumanın anahtarı aynı, değeri aynı, kökenleri farklı. İlk okuma ağdan gelir; `promote` değeri belleğe alınca ikinci okuma bellekten gelir. Ağ artık 150 söylüyor ama okuma 120 buluyor. Kural hâlâ yalnız sırayı görür: bellek ilk basamak olduğu için eski değer yanıt olur.

**UM2.** Model değerin ne zaman bayatladığını bilmez; bellek basamağı kendiliğinden boşalmaz. Platformda bir geçerlilik süresi ya da geçersiz kılma olabilir; modelde yok.

#### Özet

- Bu dersin kökeni: ağ. Alan katmanı fiyatı kuruştan liraya çevirdi, ama yanıtı veren basamak ağdı: bellek ve disk boştu.
- Dersin ölçütü, bir okumanın yanıtını hangi basamağın verdiği: bellek, disk, ağ ya da varsayılan. Sekiz okumada sayılar bellek 2, disk 3, ağ 1, varsayılan 2 çıktı.
- Arayüz, alan ve veri katmanı arasında bağımlılık içe doğru gider; arayüz basamakları değil, yalnız zincirin yanıtını görür.
- Kural sırayı görür, tazeliği görmez: ağda 120 dururken bellekteki 100 okundu.
- Aynı anahtar belleğe taşınınca aynı okuma ağdan belleğe döndü; ağdaki yeni değer okunmadı.

#### Sonraki Adım

Bu derste bellek, `promote` ile elle doldurulan bir eşlemdi. Platformda belleği dolduran, ekran döndüğünde onu koruyan bir sahip var. Sıradaki ders, `Görünüm Modeli`, o sahibin ekran döndükten sonra okumanın kökenini nasıl değiştirdiğini ölçer.

### Görünüm Modeli

Source: https://academia.sh/tr/kurslar/android-mimari/gorunum-modeli

Görünüm modeli ekran verisini yapılandırma değişikliğinden sağ çıkaracak biçimde tutar, süreç sonlanmasından sağ çıkarmaz. Ders ekran döndüğünde ve süreç sonlandığında bir okumanın kökeninin nasıl değiştiğini ölçer.

Önceki derste okumanın kökenini sınıflayan zinciri kurduk ve belleği elle doldurduk. Platformda belleği dolduran ve ekran döndüğünde onu koruyan bir sahip var: görünüm modeli (ViewModel). Bu derste zincirin ilk basamağına bakıyoruz ve soru aynı kalıyor: **bu okumanın yanıtı nereden geliyor?**

Ölçü aracı önceki dersin çekirdeğidir; değişmeden tekrar veriyoruz.

```kotlin
// Origin.kt — bir okumanin yanitinin hangi basamaktan geldigini siniflandirir
class Tier(val origin: String, private val entries: Map<String, String>) {
    init {
        require(origin in listOf("bellek", "disk", "ag")) { "gecersiz basamak: $origin" }
    }

    fun lookup(key: String): String? = entries[key]
}

class Read(val label: String, val key: String, chain: List<Tier>, fallback: String) {
    private val hit: Tier? = chain.firstOrNull { it.lookup(key) != null }
    val value: String = hit?.lookup(key) ?: fallback
    val origin: String = hit?.origin ?: "varsayilan"

    fun show() {
        println("$label: anahtar=$key | deger=$value -> $origin")
    }
}

fun countOrigins(reads: List<Read>) {
    for (name in listOf("bellek", "disk", "ag", "varsayilan")) {
        println("$name: ${reads.count { it.origin == name }}")
    }
}
```

#### Döndürmeden Sağ Çıkan Bellek

Ekran döndüğünde, dil ya da koyu tema değiştiğinde sistem ekranın arayüzünü baştan kurar. Buna yapılandırma değişikliği (configuration change) denir. Arayüz nesneleri yok olur; görünüm modeli yok olmaz. Sistem aynı görünüm modelini yeni arayüze verir. Görünüm modeli ağdan okunan değeri bir alanda tuttuğu için yeni arayüz aynı değeri tekrar ağa gitmeden okur.

```kotlin
// Platform kodu: CartViewModel
import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope
import kotlinx.coroutines.launch

class CartViewModel(private val repository: CartRepository) : ViewModel() {
    var quantity = 1
        private set

    fun load() {
        viewModelScope.launch { quantity = repository.quantity() }
    }
}
```

`quantity` görünüm modelinin alanıdır. `viewModelScope` içindeki iş, görünüm modeli sonlanana kadar sürer; ekranın döndürülmesi bu işi iptal etmez. Bu parça platform kodudur ve burada çıktı, süre ya da dosya yolu iddia etmiyoruz. Model, platformun bu kuralını kurar; model platformun kendisi değildir.

Önceki kurslarda ömür eksenleri (M10/K02, M11/K02, M13/K03) bir şeyin ne kadar yaşadığını ölçtü. Bu derste de yaşam süresinden söz ediyoruz, ama ölçü o değil. Bellek bir ömür değil, zincirdeki yerdir: ölçü, okumanın yanıtını bellek basamağının verip vermediğidir.

Model üç işlev ister. `fillMemory` ağdan gelen değerleri bir sahibin belleğine koyar. `surviveRotation` döndürmeden sonra hangi sahiplerin belleğinin kaldığını verir. `afterTermination` süreç sonlandıktan sonra bellekte yalnız kaydedilmiş anahtarları bırakır. Üçü de girdisini dönüştürür.

```kotlin
// Lifecycle.kt — ekran donmesi ve surec sonlanmasi sonrasi hangi bellek kalir
fun fillMemory(store: MutableMap<String, String>, network: Map<String, String>, keys: List<String>): Map<String, String> {
    for (key in keys) network[key]?.let { store[key] = it }
    return store
}

fun surviveRotation(stores: Map<String, Map<String, String>>, keepers: Set<String>): Map<String, String> =
    stores.filterKeys { it in keepers }.values.fold(mapOf()) { all, store -> all + store }

fun afterTermination(memory: Map<String, String>, saved: Set<String>): Map<String, String> =
    memory.filterKeys { it in saved }
```

`surviveRotation` korunanlar kümesindeki sahiplerin belleklerini birleştirir; sahibi korunmayanın belleği yeni arayüze ulaşmaz.

Sepet ekranı görünüm modelinin belleğine adedi ve fiyatı yüklemiş olsun. Ekran döndü. Yeni arayüz beş değeri okuyor.

```kotlin
// Main.kt — ekran dondu, gorunum modeli bellegi korundu
fun main() {
    val network = mapOf("sepet.adet" to "3", "urun.fiyat" to "120", "urun.stok" to "7")
    val disk = mapOf("ayar.tema" to "koyu")
    val stores = mapOf("gorunum-modeli" to fillMemory(mutableMapOf(), network, listOf("sepet.adet", "urun.fiyat")))
    val memory = surviveRotation(stores, setOf("gorunum-modeli"))
    println("kalan bellek anahtarlari: ${memory.keys.sorted()}")

    val chain = listOf(Tier("bellek", memory), Tier("disk", disk), Tier("ag", network))
    val reads = listOf(
        Read("sepet-adedi", "sepet.adet", chain, "0"),
        Read("urun-fiyati", "urun.fiyat", chain, "0"),
        Read("tema", "ayar.tema", chain, "acik"),
        Read("stok", "urun.stok", chain, "0"),
        Read("bildirim", "bildirim.acik", chain, "kapali"),
    )
    reads.forEach { it.show() }
    countOrigins(reads)
}
```

```text
kalan bellek anahtarlari: [sepet.adet, urun.fiyat]
sepet-adedi: anahtar=sepet.adet | deger=3 -> bellek
urun-fiyati: anahtar=urun.fiyat | deger=120 -> bellek
tema: anahtar=ayar.tema | deger=koyu -> disk
stok: anahtar=urun.stok | deger=7 -> ag
bildirim: anahtar=bildirim.acik | deger=kapali -> varsayilan
bellek: 2
disk: 1
ag: 1
varsayilan: 1
```

İki okuma bellekten geldi: adet ve fiyat. Ağa gidilmedi. `stok` hiç yüklenmediği için ağdan, `tema` diskten, `bildirim` varsayılandan geldi.

**UM1.** Model döndürmeyi tek bir olay sayar ve görünüm modelinin hiçbir alanını kaybetmez. Platformda görünüm modelinin kendi alanları da, yanlış yazılırsa, arayüz nesnesine tutunarak sızdırılabilir; modelde bu yok.

#### Bellek Kimin

Belleği arayüz nesnesi tutsaydı ne olurdu? Aynı iki değer aynı ekranda aynı ağdan yüklenmiş olurdu, ama sahibi döndürmede yok olurdu. Şimdi iki sahibi yan yana koyalım. Her sahip için aynı yükleme yapılır, aynı döndürme uygulanır ve aynı üç okuma yeniden çalışır.

```kotlin
// Main.kt — ayni yukleme, iki farkli sahip
fun main() {
    val network = mapOf("sepet.adet" to "3", "urun.fiyat" to "120")
    val disk = mapOf("ayar.tema" to "koyu")
    for (owner in listOf("gorunum-modeli", "arayuz")) {
        val stores = mapOf(owner to fillMemory(mutableMapOf(), network, listOf("sepet.adet", "urun.fiyat")))
        val memory = surviveRotation(stores, setOf("gorunum-modeli"))
        val chain = listOf(Tier("bellek", memory), Tier("disk", disk), Tier("ag", network))
        println("sahip: $owner")
        countOrigins(listOf(
            Read("sepet-adedi", "sepet.adet", chain, "0"),
            Read("urun-fiyati", "urun.fiyat", chain, "0"),
            Read("tema", "ayar.tema", chain, "acik"),
        ))
    }
}
```

```text
sahip: gorunum-modeli
bellek: 2
disk: 1
ag: 0
varsayilan: 0
sahip: arayuz
bellek: 0
disk: 1
ag: 2
varsayilan: 0
```

Sahip görünüm modeli olduğunda iki okuma bellekten gelir. Sahip arayüz olduğunda bellek boş kalır ve aynı iki okuma ağa gider; `tema` her iki durumda diskten gelir. Değerler aynı, yükleme aynı, tek fark belleğin döndürmeden sağ çıkıp çıkmadığıdır.

**UM2.** Model korunan sahipleri bir küme olarak alır. Platformda hangi sahibin korunduğunu sistem belirler; kümeyi elle seçmek modelde mümkündür ama platformda değildir.

#### Sınırlayıcı Ölçüm

Görünüm modeli yapılandırma değişikliğinden sağ çıkar, süreç sonlanmasından çıkmaz. Sistem bellek açığında uygulamanın arka plandaki sürecini sonlandırabilir; bellekteki her nesne, görünüm modeli dahil, gider. Yalnız kaydedilmiş durum döner. Bu derste hiçbir anahtarı kaydetmedik; kaydedilmiş durum sonraki dersin konusudur. Aynı belleği iki olaydan geçirelim.

```kotlin
// Main.kt — donme ile surec sonlanmasi ayni okumalari farkli kokene tasir
fun main() {
    val network = mapOf("sepet.adet" to "3", "urun.fiyat" to "120")
    val disk = mapOf("ayar.tema" to "koyu")
    val viewModelMemory = fillMemory(mutableMapOf("ekran.sekme" to "ozet"), network, listOf("sepet.adet", "urun.fiyat"))
    val events = listOf(
        "donme" to surviveRotation(mapOf("gorunum-modeli" to viewModelMemory), setOf("gorunum-modeli")),
        "surec-sonlanmasi" to afterTermination(viewModelMemory, emptySet()),
    )
    for ((event, memory) in events) {
        println("olay: $event")
        val chain = listOf(Tier("bellek", memory), Tier("disk", disk), Tier("ag", network))
        val reads = listOf(
            Read("sepet-adedi", "sepet.adet", chain, "0"),
            Read("urun-fiyati", "urun.fiyat", chain, "0"),
            Read("tema", "ayar.tema", chain, "acik"),
            Read("ekran-sekmesi", "ekran.sekme", chain, "ilk"),
        )
        reads.forEach { it.show() }
        countOrigins(reads)
    }
}
```

```text
olay: donme
sepet-adedi: anahtar=sepet.adet | deger=3 -> bellek
urun-fiyati: anahtar=urun.fiyat | deger=120 -> bellek
tema: anahtar=ayar.tema | deger=koyu -> disk
ekran-sekmesi: anahtar=ekran.sekme | deger=ozet -> bellek
bellek: 3
disk: 1
ag: 0
varsayilan: 0
olay: surec-sonlanmasi
sepet-adedi: anahtar=sepet.adet | deger=3 -> ag
urun-fiyati: anahtar=urun.fiyat | deger=120 -> ag
tema: anahtar=ayar.tema | deger=koyu -> disk
ekran-sekmesi: anahtar=ekran.sekme | deger=ilk -> varsayilan
bellek: 0
disk: 1
ag: 2
varsayilan: 1
```

Döndürmede dört okumadan üçü bellekten geldi. Süreç sonlanmasında aynı dört okumanın sonucu değişti: adet ve fiyat ağdan okundu, `tema` diskte kaldı, `ekran-sekmesi` hiçbir basamakta bulunmadığı için varsayılana düştü. Bellekteki sekme seçimi kaydedilmediği için kaybolmuş sayılır.

**UM3.** Model süreç sonlanmasında ağın ulaşılabilir kaldığını varsayar. Platformda bağlantı yoksa adet ve fiyat da varsayılana düşerdi; modelde bu durum kurulmaz.

#### Özet

- Bu dersin kökeni: bellek. Ekran döndüğünde görünüm modelinin belleği zincirin ilk basamağıdır ve adet ile fiyat oradan okunur.
- Dersin ölçütü, okumanın yanıtını bellek basamağının verip vermediği. Döndürmeden sonra beş okumada bellek 2, disk 1, ağ 1, varsayılan 1 çıktı.
- Aynı yükleme için belleğin sahibi görünüm modeli olunca bellek 2, ağ 0; sahibi arayüz olunca bellek 0, ağ 2 çıktı.
- Süreç sonlanmasında bellek boşalır: dört okumada bellek 3'ten 0'a düştü, ağ 0'dan 2'ye, varsayılan 0'dan 1'e çıktı.
- Kaydedilmemiş sekme seçimi varsayılana düştü; kaydedilmiş durum sonraki dersin konusudur.

#### Sonraki Adım

Bu derste kaydedilen anahtar kümesi boştu ve sekme seçimi kayboldu. Sıradaki ders, `Durum Tutucular`, ekranın durumunu tek bir yerde toplayıp kaydedilen alanı diskten nasıl geri yüklediğini ölçer.

### Durum Tutucular

Source: https://academia.sh/tr/kurslar/android-mimari/durum-tutucular

Durum tutucu, bir ekranın durumunu tek yerde toplar; kaydedilmiş durum süreç sonlansa da dönen alanları seçer. Ders bir ekranın alanlarının hangi kökenden, hangi kaydedilen alanlarla geri geldiğini ölçer.

Önceki derste ekran döndüğünde bellek korundu, süreç sonlandığında boşaldı ve kaydedilmemiş sekme seçimi varsayılana düştü. Bu derste ekranın durumunu tek bir yerde topluyoruz ve hangi alanlarının süreç sonlandıktan sonra geri döneceğini seçiyoruz. Soru aynı: **bu okumanın yanıtı nereden geliyor?**

Ölçü aracı önceki derslerin çekirdeğidir; değişmeden tekrar veriyoruz.

```kotlin
// Origin.kt — bir okumanin yanitinin hangi basamaktan geldigini siniflandirir
class Tier(val origin: String, private val entries: Map<String, String>) {
    init {
        require(origin in listOf("bellek", "disk", "ag")) { "gecersiz basamak: $origin" }
    }

    fun lookup(key: String): String? = entries[key]
}

class Read(val label: String, val key: String, chain: List<Tier>, fallback: String) {
    private val hit: Tier? = chain.firstOrNull { it.lookup(key) != null }
    val value: String = hit?.lookup(key) ?: fallback
    val origin: String = hit?.origin ?: "varsayilan"

    fun show() {
        println("$label: anahtar=$key | deger=$value -> $origin")
    }
}

fun countOrigins(reads: List<Read>) {
    for (name in listOf("bellek", "disk", "ag", "varsayilan")) {
        println("$name: ${reads.count { it.origin == name }}")
    }
}
```

#### Tek Doğruluk Kaynağı

Bir ekranın durumu birkaç alandan oluşur: sepetteki adet, arama kutusundaki metin, açık sekme, sıralama. Alanlar arayüz nesnelerinde dağınık dururlarsa aynı bilginin birden çok kopyası olur ve kopyalar ayrışabilir. Çözüm her alanın yazılabilir tek bir sahibi olmasıdır; buna tek doğruluk kaynağı (single source of truth) denir. Arayüz bu sahibi okur, alanı kendi kopyasında tutmaz.

Durumu ve onu değiştiren mantığı taşıyan sınıfa durum tutucu (state holder) denir. Önceki dersin görünüm modeli bir durum tutucudur, ama durum tutucu olmak için görünüm modeli olmak gerekmez. Düz bir sınıf da durum tutucu olabilir; fark, onun ömrünü kimin belirlediğidir.

```kotlin
// Platform kodu: CartStateHolder
data class CartState(val quantity: Int = 1, val filter: String = "")

class CartStateHolder {
    var state = CartState()
        private set

    fun setQuantity(value: Int) {
        state = state.copy(quantity = value)
    }
}
```

`CartStateHolder` durumu `state` alanında tutar ve yalnız kendisi yazar. Bu parça platform kodudur ve burada çıktı, süre ya da dosya yolu iddia etmiyoruz. Model, platformun bu kuralını kurar; model platformun kendisi değildir.

#### Kaydedilen Alanlar

Süreç sonlandığında bellekteki durum tutucu gider. Geri dönecek alanlar için platform kaydedilmiş durum (saved state) sunar: durum tutucu hangi alanlarını kaydedeceğini kendisi seçer.

```kotlin
// Platform kodu: CartViewModel
import androidx.lifecycle.SavedStateHandle
import androidx.lifecycle.ViewModel

class CartViewModel(private val handle: SavedStateHandle) : ViewModel() {
    val filter: String = handle["filter"] ?: ""

    fun setFilter(value: String) {
        handle["filter"] = value
    }
}
```

`filter` anahtarı kaydedilmiş duruma yazılır ve süreç sonlandıktan sonra yeni görünüm modeline geri verilir. Kaydedilmeyen alan yalnız bellekte kalır.

Modelde durum tutucu alanları ve kaydedilecek anahtarlar kümesini taşır. `memory` anlık alanları verir; `snapshot` yalnız kaydedilenleri süzer. `screenReads` ekranın dört okumasını kurar.

```kotlin
// Holder.kt — durum tutucu: ekran alanlari tek yerde, kaydedilecek olanlar ayri isaretli
class Holder(private val fields: Map<String, String>, private val saved: Set<String>) {
    fun memory(): Map<String, String> = fields.toMap()
    fun snapshot(): Map<String, String> = fields.filterKeys { it in saved }
}

fun screenReads(chain: List<Tier>): List<Read> = listOf(
    Read("sepet-adedi", "sepet.adet", chain, "0"),
    Read("filtre-metni", "filtre.metin", chain, "yok"),
    Read("ekran-sekmesi", "ekran.sekme", chain, "ilk"),
    Read("siralama", "siralama", chain, "ad"),
)
```

Önce süreç sonlanmadan önceki hâl. Durum tutucunun dört alanı da bellektedir.

```kotlin
// Main.kt — durum tutucu ekran alanlarini tek yerde tutar
fun main() {
    val holder = Holder(
        mapOf("sepet.adet" to "3", "filtre.metin" to "kulaklik", "ekran.sekme" to "ozet", "siralama" to "fiyat"),
        setOf("filtre.metin", "ekran.sekme"),
    )
    println("kaydedilecek anahtarlar: ${holder.snapshot().keys.sorted()}")
    val reads = screenReads(listOf(Tier("bellek", holder.memory())))
    reads.forEach { it.show() }
    countOrigins(reads)
}
```

```text
kaydedilecek anahtarlar: [ekran.sekme, filtre.metin]
sepet-adedi: anahtar=sepet.adet | deger=3 -> bellek
filtre-metni: anahtar=filtre.metin | deger=kulaklik -> bellek
ekran-sekmesi: anahtar=ekran.sekme | deger=ozet -> bellek
siralama: anahtar=siralama | deger=fiyat -> bellek
bellek: 4
disk: 0
ag: 0
varsayilan: 0
```

Dört okuma bellekten geldi. İki anahtar kaydedilecek olarak işaretli: arama metni ve açık sekme. Sepet adedi ağda zaten durduğu için, sıralama ise yalnız arayüz tercihi olduğu için işaretli değil.

#### Süreç Sonlanınca

Süreç sonlandı. Bellek boş; yeni durum tutucu kaydedilmiş durumu alıyor. Modelde kaydedilmiş durumu disk basamağı olarak kuruyoruz: süreç sonlansa da kalan yer. Ağda yalnız sepet adedi var.

```kotlin
// Main.kt — surec sonlandi, kaydedilmis durum diskten geri gelir
fun main() {
    val network = mapOf("sepet.adet" to "3")
    val holder = Holder(
        mapOf("sepet.adet" to "3", "filtre.metin" to "kulaklik", "ekran.sekme" to "ozet", "siralama" to "fiyat"),
        setOf("filtre.metin", "ekran.sekme"),
    )
    val chain = listOf(Tier("bellek", emptyMap()), Tier("disk", holder.snapshot()), Tier("ag", network))
    val reads = screenReads(chain)
    reads.forEach { it.show() }
    countOrigins(reads)
}
```

```text
sepet-adedi: anahtar=sepet.adet | deger=3 -> ag
filtre-metni: anahtar=filtre.metin | deger=kulaklik -> disk
ekran-sekmesi: anahtar=ekran.sekme | deger=ozet -> disk
siralama: anahtar=siralama | deger=ad -> varsayilan
bellek: 0
disk: 2
ag: 1
varsayilan: 1
```

Arama metni ve sekme diskten geri geldi. Adet kaydedilmediği için ağdan okundu. Sıralama hiçbir basamakta olmadığı için varsayılana düştü. Aynı ekranda aynı anda üç köken: disk 2, ağ 1, varsayılan 1.

**UM1.** Model kaydedilmiş durumu disk basamağı olarak kurar. Platformda bu paketin nerede saklandığını model söylemez; modelin ayırdığı tek şey, paketin süreç sonlanmasından sağ çıkmasıdır.

**UM2.** Model kaydedilecek anahtarlar kümesini durum tutucuya elle verir. Platformda kaydedilen değerin küçük ve dizileştirilebilir olması gerekir; modelde bu kısıt yok.

#### Sınırlayıcı Ölçüm

Kaydedilmeyen alan varsayılana ya da ağa düşer. Peki kaydedilen küme değişince kökenler nasıl değişir? Aynı alanlarla üç küme deneyelim: hiçbiri, iki alan, hepsi.

```kotlin
// Main.kt — kaydedilen alan kumesi kokenlerin dagilimini degistirir
fun main() {
    val network = mapOf("sepet.adet" to "3")
    val fields = mapOf("sepet.adet" to "3", "filtre.metin" to "kulaklik", "ekran.sekme" to "ozet", "siralama" to "fiyat")
    val savedSets = listOf(
        "hicbiri" to emptySet<String>(),
        "iki-alan" to setOf("filtre.metin", "ekran.sekme"),
        "hepsi" to fields.keys,
    )
    for ((name, saved) in savedSets) {
        val snapshot = Holder(fields, saved).snapshot()
        val chain = listOf(Tier("bellek", emptyMap()), Tier("disk", snapshot), Tier("ag", network))
        println("kaydedilen: $name")
        countOrigins(screenReads(chain))
    }
}
```

```text
kaydedilen: hicbiri
bellek: 0
disk: 0
ag: 1
varsayilan: 3
kaydedilen: iki-alan
bellek: 0
disk: 2
ag: 1
varsayilan: 1
kaydedilen: hepsi
bellek: 0
disk: 4
ag: 0
varsayilan: 0
```

Hiçbir alan kaydedilmediğinde disk sıfırdır: adet ağdan gelir, üç alan varsayılana düşer. İki alan kaydedildiğinde önceki ölçümün dağılımı çıkar. Hepsi kaydedildiğinde dört okumanın dördü diskten gelir ve ağa gidilmez. Ekranı yeniden kuran sistem aynı, değişen tek şey durum tutucunun hangi alanları işaretlediğidir.

**UM3.** Model kaydetmeyi alanın anahtarıyla işaretler. Platformda kaydetmek her yazmada yapılır; modelde kaydedilen küme sabittir ve anlık güncellenmez.

#### Özet

- Bu dersin kökeni: disk. Süreç sonlandıktan sonra kaydedilen arama metni ve sekme seçimi kaydedilmiş durumdan, modelde disk basamağından geri geldi.
- Dersin ölçütü, süreç sonlandıktan sonra bir alanın hangi kökenden geri geldiği. Aynı ekranda dört okuma disk 2, ağ 1, varsayılan 1 verdi.
- Durum tutucu alanları tek yerde toplar; sürece sağ çıkacak olanları kaydedilen anahtarlar kümesi belirler.
- Kaydedilen küme boşken disk 0, ağ 1, varsayılan 3; hepsi kaydedilince disk 4 çıktı.
- Kaydedilmeyen alan bellekle birlikte gider; ağda ya da varsayılanda yeniden kurulur.

#### Sonraki Adım

Bu derste durum tutucunun alanları süreç sonlandıktan sonra geri geldi, ama tutucu henüz yüklemenin bitip bitmediğini bilmiyor. Sıradaki ders, `Gözlemlenebilir Durum`, durum akışının ilk değeri ile yüklenen değer arasındaki farkı ölçer.

### Gözlemlenebilir Durum

Source: https://academia.sh/tr/kurslar/android-mimari/gozlemlenebilir-durum

Gözlemlenebilir durum, değeri değiştikçe toplayıcılara yeni değer yayan bir durum akışıdır; akışın her zaman bir ilk değeri vardır. Ders bir toplayıcının yükleme bitmeden ve bittikten sonra okuduğu değerin kökenini ölçer.

Önceki derste durum tutucunun alanları süreç sonlandıktan sonra geri geldi, ama tutucu yüklemenin bitip bitmediğini söylemedi. Arayüz değeri nasıl öğrenir? Değer değiştikçe haber veren bir yapı kullanır: gözlemlenebilir durum (observable state). Soru aynı: **bu okumanın yanıtı nereden geliyor?**

Ölçü aracı önceki derslerin çekirdeğidir; değişmeden tekrar veriyoruz.

```kotlin
// Origin.kt — bir okumanin yanitinin hangi basamaktan geldigini siniflandirir
class Tier(val origin: String, private val entries: Map<String, String>) {
    init {
        require(origin in listOf("bellek", "disk", "ag")) { "gecersiz basamak: $origin" }
    }

    fun lookup(key: String): String? = entries[key]
}

class Read(val label: String, val key: String, chain: List<Tier>, fallback: String) {
    private val hit: Tier? = chain.firstOrNull { it.lookup(key) != null }
    val value: String = hit?.lookup(key) ?: fallback
    val origin: String = hit?.origin ?: "varsayilan"

    fun show() {
        println("$label: anahtar=$key | deger=$value -> $origin")
    }
}

fun countOrigins(reads: List<Read>) {
    for (name in listOf("bellek", "disk", "ag", "varsayilan")) {
        println("$name: ${reads.count { it.origin == name }}")
    }
}
```

#### Akışın İlk Değeri

Platformda gözlemlenebilir durum çoğunlukla bir durum akışıdır (StateFlow). Akış her zaman bir değer taşır: kurulurken verilen ilk değer. Durumu değiştiren taraf yeni değeri akışa yazar; değeri okuyan taraf, yani toplayıcı, akışı toplar (collect) ve her yeni değeri alır. Toplama bir eş yordamda koşar; akış başka bir kaynaktan türüyorsa stateIn ile durum akışına çevrilir ve çoğunlukla bir ilk değer verilerek kurulur.

```kotlin
// Platform kodu: CartViewModel
import androidx.lifecycle.ViewModel
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.StateFlow

class CartViewModel : ViewModel() {
    private val _state = MutableStateFlow(CartUiState())
    val state: StateFlow<CartUiState> = _state

    fun onLoaded(items: List<CartItem>) {
        _state.value = CartUiState(items)
    }
}
```

Arayüz akışı toplar ve her değerde yeniden çizilir.

```kotlin
// Platform kodu: CartScreen
import androidx.compose.runtime.getValue

@Composable
fun CartScreen(viewModel: CartViewModel) {
    val state by viewModel.state.collectAsState()
    Text("Urun: ${state.items.size}")
}
```

`CartUiState()` akışın ilk değeridir: sepet henüz yüklenmemiştir ve liste boştur. Bu iki parça platform kodudur ve burada çıktı, süre ya da dosya yolu iddia etmiyoruz. Model, platformun bu kuralını kurar; model platformun kendisi değildir.

Modelde akışı düz bir liste olarak kuruyoruz: ilk değer listenin ilk elemanıdır, her yayın önceki son değerin üstüne yazılan yeni bir değer ekler. `latest` son değeri, `fastCollector` her yayını, `slowCollector` yalnız son yayını verir. `collectReads` bir toplayıcının dört okumasını kurar; toplayıcının zincirinde akıştan başka basamak yoktur.

```kotlin
// Feed.kt — gozlemlenebilir durum: ilk deger vardir, her yayin listeye eklenir
class Feed(initial: Map<String, String>) {
    private val emitted = mutableListOf(initial)

    fun emit(update: Map<String, String>) {
        emitted.add(emitted.last() + update)
    }

    fun latest(): Map<String, String> = emitted.last()
    fun fastCollector(): List<Map<String, String>> = emitted.toList()
    fun slowCollector(): List<Map<String, String>> = listOf(emitted.last())
}

fun collectReads(value: Map<String, String>): List<Read> {
    val chain = listOf(Tier("bellek", value))
    return listOf(
        Read("baslik", "ekran.baslik", chain, "bos"),
        Read("urun-fiyati", "urun.fiyat", chain, "0"),
        Read("stok", "urun.stok", chain, "0"),
        Read("kampanya", "kampanya.kod", chain, "yok"),
    )
}
```

Yükleme başlamadan toplayıcı akışı toplar. Akışın ilk değerinde yalnız ekran başlığı var.

```kotlin
// Main.kt — toplayicinin ilk okumasi yukleme bitmeden yapilir
fun main() {
    val feed = Feed(mapOf("ekran.baslik" to "Sepet"))
    val reads = collectReads(feed.latest())
    reads.forEach { it.show() }
    countOrigins(reads)
}
```

```text
baslik: anahtar=ekran.baslik | deger=Sepet -> bellek
urun-fiyati: anahtar=urun.fiyat | deger=0 -> varsayilan
stok: anahtar=urun.stok | deger=0 -> varsayilan
kampanya: anahtar=kampanya.kod | deger=yok -> varsayilan
bellek: 1
disk: 0
ag: 0
varsayilan: 3
```

Başlık akışın ilk değerinde olduğu için bellekten geldi. Fiyat, stok ve kampanya yüklenmediği için akışta yok: üç okuma varsayılana düştü. Ağ yanıtı yolda; yanıt vermemiş bir basamak toplayıcının zincirinde değildir.

**UM1.** Model akışı düz liste olarak kurar ve toplayıcının zincirine yalnız akışı koyar. Platformda yükleme aynı anda disk ya da ağdan da başlayabilir; toplayıcı yine yalnız akışa bakar.

#### Yavaş Toplayıcı

Akış iki yayın yapsın: önce eski bir fiyat, sonra yeni fiyat. Her yayını gören toplayıcıya hızlı, yalnız sonuncuyu gören toplayıcıya yavaş diyelim. Durum akışı değeri birleştirdiği için yavaş toplayıcı ara değeri hiç görmez.

```kotlin
// Main.kt — yavas toplayici ara degeri gormez
fun main() {
    val feed = Feed(mapOf("ekran.baslik" to "Sepet"))
    feed.emit(mapOf("urun.fiyat" to "100"))
    feed.emit(mapOf("urun.fiyat" to "120"))
    println("hizli toplayici:")
    for (value in feed.fastCollector()) {
        println("  fiyat=${value["urun.fiyat"] ?: "yok"}")
    }
    println("yavas toplayici:")
    for (value in feed.slowCollector()) {
        println("  fiyat=${value["urun.fiyat"] ?: "yok"}")
    }
    countOrigins(collectReads(feed.latest()))
}
```

```text
hizli toplayici:
  fiyat=yok
  fiyat=100
  fiyat=120
yavas toplayici:
  fiyat=120
bellek: 2
disk: 0
ag: 0
varsayilan: 2
```

Hızlı toplayıcı üç değer gördü: fiyat yok, 100, 120. Yavaş toplayıcı yalnız 120'yi gördü. İki toplayıcı da sonunda aynı değeri okudu ve o değerin kökeni aynı: akışın içindeki her değer bellektir. Son değerle yapılan dört okumada iki bellek, iki varsayılan çıktı.

**UM2.** Model yavaşlığı iki sabit toplayıcı türüyle kurar. Platformda toplayıcının yavaşlığı zamanlamaya bağlıdır ve modelde zaman yoktur.

#### Sınırlayıcı Ölçüm

Aynı akışı iki anda okuyalım: yükleme sürerken ve yükleme bitince. Yükleme bitince fiyat ve stok akışa yazılır; yani anahtarlar belleğe girer.

```kotlin
// Main.kt — ayni akis iki anda iki koken verir
fun main() {
    val feed = Feed(mapOf("ekran.baslik" to "Sepet"))
    println("yukleme: surerken")
    countOrigins(collectReads(feed.latest()))

    feed.emit(mapOf("urun.fiyat" to "120", "urun.stok" to "7"))
    println("yukleme: bitince")
    val after = collectReads(feed.latest())
    after.forEach { it.show() }
    countOrigins(after)
}
```

```text
yukleme: surerken
bellek: 1
disk: 0
ag: 0
varsayilan: 3
yukleme: bitince
baslik: anahtar=ekran.baslik | deger=Sepet -> bellek
urun-fiyati: anahtar=urun.fiyat | deger=120 -> bellek
stok: anahtar=urun.stok | deger=7 -> bellek
kampanya: anahtar=kampanya.kod | deger=yok -> varsayilan
bellek: 3
disk: 0
ag: 0
varsayilan: 1
```

Yükleme sürerken bir okuma bellek, üç okuma varsayılan verdi. Yükleme bitince fiyat ve stok akışa girdi: bellek üçe çıktı, varsayılan bire indi. Aynı akış, aynı toplayıcı, aynı dört okuma; değişen tek şey akışın hangi anda okunduğu. `kampanya` yükleme bitse de akışa girmediği için varsayılan kalır.

**UM3.** Model yüklemenin bittiğini tek bir yayınla işaretler. Platformda yükleme parça parça biter ve her parça ayrı bir yayın olabilir; modelde bu ayrım yok.

#### Özet

- Bu dersin kökeni: varsayılan. Toplayıcı yükleme bitmeden okuduğunda fiyat, stok ve kampanya akışta olmadığı için okumaların kendi varsayılanları yanıt oldu.
- Dersin ölçütü, toplayıcının okuduğu değerin akışın hangi anındaki değerden geldiği. Yükleme sürerken bellek 1, varsayılan 3; bitince bellek 3, varsayılan 1 çıktı.
- Akışın ilk değeri bir yanıttır: ekran başlığı bellekten geldi, çünkü ilk değerde vardı.
- Yavaş toplayıcı ara değer olan 100'ü görmedi, yalnız 120'yi gördü; akıştaki her değerin kökeni bellektir.
- Akışın zincirinde yanıt vermemiş bir basamak yoktur; toplayıcı yalnız akışı görür.

#### Sonraki Adım

Bu derste akış yüklemeyi dışarıdan alıp yayınladı. Olayın akışı nasıl değiştirdiği ayrı bir sorudur. Sıradaki ders, `Tek Yönlü Veri Akışı`, bir olayın durumu bellekte nasıl yeniden kurduğunu ve değerin arayüzden yukarı değil aşağı gittiğini ölçer.

### Tek Yönlü Veri Akışı

Source: https://academia.sh/tr/kurslar/android-mimari/tek-yonlu-veri-akisi

Tek yönlü veri akışında olay yukarı çıkar, durum aşağı iner ve arayüz durumu doğrudan yazmaz. Ders, olayı işleyen bir indirgeyicinin kurduğu yeni durumun okunduğunda yanıtı hangi basamağın verdiğini ölçer.

Önceki derste arayüz, değişen değeri bir akıştan topladı. Değerin akışa nasıl girdiği ayrı bir sorudur: kim yazıyor? Tek yönlü veri akışında (unidirectional data flow) arayüz durumu yazmaz. Kullanıcının dokunuşu bir olaya (event) dönüşür, olay durum tutucuya gider, tutucu yeni durumu kurar, arayüz yeni durumu okur. Soru yine aynı: **bu okumanın yanıtı nereden geliyor?**

Ölçü aracı önceki derslerin çekirdeğidir; değişmeden tekrar veriyoruz.

```kotlin
// Origin.kt — bir okumanin yanitinin hangi basamaktan geldigini siniflandirir
class Tier(val origin: String, private val entries: Map<String, String>) {
    init {
        require(origin in listOf("bellek", "disk", "ag")) { "gecersiz basamak: $origin" }
    }

    fun lookup(key: String): String? = entries[key]
}

class Read(val label: String, val key: String, chain: List<Tier>, fallback: String) {
    private val hit: Tier? = chain.firstOrNull { it.lookup(key) != null }
    val value: String = hit?.lookup(key) ?: fallback
    val origin: String = hit?.origin ?: "varsayilan"

    fun show() {
        println("$label: anahtar=$key | deger=$value -> $origin")
    }
}

fun countOrigins(reads: List<Read>) {
    for (name in listOf("bellek", "disk", "ag", "varsayilan")) {
        println("$name: ${reads.count { it.origin == name }}")
    }
}
```

#### Döngü: Olay Yukarı, Durum Aşağı

Platformda olay çoğunlukla kapalı bir tür olarak yazılır ve görünüm modeli onu tek bir giriş işleviyle alır. Giriş işlevi mevcut durumdan yeni durumu üretir ve akışa yazar. Arayüz yalnız iki şey yapar: durumu çizer ve olayı gönderir.

```kotlin
// Platform kodu: CartViewModel
import androidx.lifecycle.ViewModel
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.flow.update

sealed interface CartEvent {
    data class QuantityChanged(val quantity: Int) : CartEvent
    object Cleared : CartEvent
}

class CartViewModel : ViewModel() {
    private val _state = MutableStateFlow(CartUiState())
    val state: StateFlow<CartUiState> = _state

    fun onEvent(event: CartEvent) {
        _state.update { current ->
            when (event) {
                is CartEvent.QuantityChanged -> current.copy(quantity = event.quantity)
                CartEvent.Cleared -> CartUiState()
            }
        }
    }
}
```

```kotlin
// Platform kodu: CartScreen
@Composable
fun CartScreen(viewModel: CartViewModel) {
    val state by viewModel.state.collectAsState()
    Button(onClick = { viewModel.onEvent(CartEvent.QuantityChanged(state.quantity + 1)) }) {
        Text("Adet: ${state.quantity}")
    }
}
```

Bu iki parça platform kodudur ve burada çıktı, süre ya da dosya yolu iddia etmiyoruz. Model, platformun bu kuralını kurar; model platformun kendisi değildir.

M31/K03'te arayüzü bir ağaç olarak kurmuş ve verinin o ağaçta hangi yöne gittiğini ölçmüştük. Burada soru ağacın yönü değildir: aşağı inen durumun okunduğunda yanıtı hangi basamağın verdiğini soruyoruz. Yön, bu kursta kategori değildir; okumanın kökeni ölçülür.

Modelde indirgeyici (reducer) iki girdi alan bir işlevdir: mevcut durum ve olay. Dönüşü, olayın değerleriyle üstü yazılmış yeni durumdur. `applyAll` olayları sırayla işler. `stateReads` yeni durumu bellek basamağına koyar ve dört okuma kurar.

```kotlin
// Reducer.kt — olay mevcut durumdan yeni durumu uretir
class Event(val name: String, val values: Map<String, String>)

fun reduce(state: Map<String, String>, event: Event): Map<String, String> =
    state + event.values

fun applyAll(initial: Map<String, String>, events: List<Event>): Map<String, String> =
    events.fold(initial) { current, event -> reduce(current, event) }

fun stateReads(state: Map<String, String>): List<Read> {
    val chain = listOf(Tier("bellek", state))
    return listOf(
        Read("adet", "sepet.adet", chain, "0"),
        Read("toplam", "sepet.toplam", chain, "0"),
        Read("indirim", "sepet.indirim", chain, "yok"),
        Read("not", "sepet.not", chain, "bos"),
    )
}
```

#### Olay İşlenir, Durum Bellekte Kurulur

Sepet bir adetle başlasın. Kullanıcı adedi ikiye çıkarır, ardından toplam hesaplanır. İki olay indirgeyiciden geçer ve yeni durum okunur.

```kotlin
// Main.kt — iki olay islenir, yeni durum bellekte kurulur
fun main() {
    val initial = mapOf("sepet.adet" to "1")
    val events = listOf(
        Event("adet-ayarla", mapOf("sepet.adet" to "2")),
        Event("toplam-hesapla", mapOf("sepet.toplam" to "240")),
    )
    val state = applyAll(initial, events)
    val reads = stateReads(state)
    reads.forEach { it.show() }
    countOrigins(reads)
}
```

```text
adet: anahtar=sepet.adet | deger=2 -> bellek
toplam: anahtar=sepet.toplam | deger=240 -> bellek
indirim: anahtar=sepet.indirim | deger=yok -> varsayilan
not: anahtar=sepet.not | deger=bos -> varsayilan
bellek: 2
disk: 0
ag: 0
varsayilan: 2
```

Adet ve toplam, indirgeyicinin kurduğu durumdan okundu: iki okuma bellek. İndirim ve not hiçbir olayla yazılmadı; zincirin tek basamağı bellek olduğu için onlar varsayılana düştü. Yani durumun yeni kurulmuş olması okumayı bellek yapar, ama kurulmayan alan yine varsayılandır.

**UM1.** Model durumu düz bir harita olarak kurar ve arayüzün olay göndermesini bir liste elemanına indirger. Platformda olay zamanla gelir ve akış her değişiklikte arayüzü yeniden çizer; modelde çizim yoktur.

#### Olay Sırası Değişir, Köken Değişmez

Aynı iki olay ters sırada gelirse ne olur? Önce adet beşe, sonra ikiye ayarlansın; ardından aynı olaylar ters sırada işlensin. İndirgeyici üstüne yazdığı için son olay kazanır.

```kotlin
// Main.kt — olay sirasi degeri degistirir, koken ayni kalir
fun main() {
    val first = Event("adet-bes", mapOf("sepet.adet" to "5"))
    val second = Event("adet-iki", mapOf("sepet.adet" to "2"))

    val inOrder = stateReads(applyAll(mapOf(), listOf(first, second)))
    val reversed = stateReads(applyAll(mapOf(), listOf(second, first)))

    println("sira: bes, iki")
    inOrder[0].show()
    println("sira: iki, bes")
    reversed[0].show()
}
```

```text
sira: bes, iki
adet: anahtar=sepet.adet | deger=2 -> bellek
sira: iki, bes
adet: anahtar=sepet.adet | deger=5 -> bellek
```

Aynı okuma iki değer verdi: 2 ve 5. İkisi de bellekten geldi. Kural değeri değil basamağı görür: sıra değişince değer değişti, köken değişmedi.

**UM2.** Model "son olay kazanır" kuralını üstüne yazma ile kurar. Platformda aynı alana yazan iki olay birleştirilebilir, sıralanabilir ya da atılabilir; bu karar uygulamaya aittir ve modelde yoktur.

#### Sınırlayıcı Ölçüm

Sınır, olayın henüz işlenmediği an. Durum boşken okunan alan, olay işlendikten sonra okunan aynı alanla karşılaştırılır.

```kotlin
// Main.kt — olay islenmeden okuma varsayilan verir
fun main() {
    val before = stateReads(mapOf())
    println("olay islenmeden:")
    countOrigins(before)

    val after = stateReads(
        applyAll(
            mapOf(),
            listOf(
                Event("adet-ayarla", mapOf("sepet.adet" to "2")),
                Event("indirim-ekle", mapOf("sepet.indirim" to "kod-10")),
            ),
        )
    )
    println("olaylar islenince:")
    after.forEach { it.show() }
    countOrigins(after)
}
```

```text
olay islenmeden:
bellek: 0
disk: 0
ag: 0
varsayilan: 4
olaylar islenince:
adet: anahtar=sepet.adet | deger=2 -> bellek
toplam: anahtar=sepet.toplam | deger=0 -> varsayilan
indirim: anahtar=sepet.indirim | deger=kod-10 -> bellek
not: anahtar=sepet.not | deger=bos -> varsayilan
bellek: 2
disk: 0
ag: 0
varsayilan: 2
```

Olay işlenmeden dört okumanın dördü de varsayılan verdi. İki olaydan sonra adet ve indirim bellekten geldi; toplam ve not hâlâ varsayılan. Durumu yazan olaydır; olay gelmedikçe arayüzün okuyacağı şey zincirde değil, kodun yazdığı varsayılandır.

**UM3.** Model boş durumu boş haritayla kurar. Platformda ilk durum çoğunlukla yüklemeden önce gelen bir ilk değerdir; Gözlemlenebilir Durum dersinde gördüğümüz gibi.

#### Özet

- Bu dersin kökeni: bellek. Olayı işleyen indirgeyici yeni durumu bellekte kurdu ve iki olaydan sonra adet ve toplam bellek basamağından okundu.
- Dersin ölçütü, okumanın yeni kurulan durumdan mı yoksa hiç yazılmamış bir alandan mı geldiği. Olay işlenmeden bellek 0, varsayılan 4; iki olaydan sonra bellek 2, varsayılan 2 çıktı.
- Olay sırası değişince aynı okuma 2 ve 5 verdi, ama ikisinin kökeni de bellek: kural değeri değil basamağı görür.
- Arayüz durumu yazmaz; durumu yazan olaydır. Olay gelmeyen alan varsayılan kalır.
- Yön bu kursta kategori değildir; ölçülen, yanıtı hangi basamağın verdiğidir.

#### Sonraki Adım

Şimdiye kadar zincirin basamaklarını biz kurduk. Gerçek uygulamada zinciri kim kurar? Sıradaki ders, `Bağımlılık Enjeksiyonu`, bir nesnenin ihtiyaç duyduğu basamakların dışarıdan nasıl verildiğini ve üretim zincirinde yanıtın nereden geldiğini ölçer.

### Bağımlılık Enjeksiyonu

Source: https://academia.sh/tr/kurslar/android-mimari/bagimlilik-enjeksiyonu

Bağımlılık enjeksiyonunda bir sınıf ihtiyaç duyduğu nesneleri kendisi kurmaz, dışarıdan alır. Ders, konteynerin kurduğu üretim zincirinde okumanın yanıtını hangi basamağın verdiğini ve testte zincir değişince yanıtın nasıl değiştiğini ölçer.

Önceki derslerde zincirin basamaklarını doğrudan biz kurduk. Gerçek uygulamada bu işi bir yapı üstlenir: bağımlılık enjeksiyonu (dependency injection). Bir sınıf ihtiyaç duyduğu nesneleri kendisi oluşturmaz, kurucusunda dışarıdan alır. Nesneleri kuran ve dağıtan yapıya konteyner (container) denir. Soru yine aynı: **bu okumanın yanıtı nereden geliyor?**

Ölçü aracı önceki derslerin çekirdeğidir; değişmeden tekrar veriyoruz.

```kotlin
// Origin.kt — bir okumanin yanitinin hangi basamaktan geldigini siniflandirir
class Tier(val origin: String, private val entries: Map<String, String>) {
    init {
        require(origin in listOf("bellek", "disk", "ag")) { "gecersiz basamak: $origin" }
    }

    fun lookup(key: String): String? = entries[key]
}

class Read(val label: String, val key: String, chain: List<Tier>, fallback: String) {
    private val hit: Tier? = chain.firstOrNull { it.lookup(key) != null }
    val value: String = hit?.lookup(key) ?: fallback
    val origin: String = hit?.origin ?: "varsayilan"

    fun show() {
        println("$label: anahtar=$key | deger=$value -> $origin")
    }
}

fun countOrigins(reads: List<Read>) {
    for (name in listOf("bellek", "disk", "ag", "varsayilan")) {
        println("$name: ${reads.count { it.origin == name }}")
    }
}
```

#### Kurucudan Verilen Zincir

Kurucu enjeksiyonunda (constructor injection) sınıf bağımlılığını parametre olarak ister. Modül (module) hangi nesnenin nasıl kurulacağını tarif eder; kapsam (scope) o nesnenin ne zamana kadar paylaşılacağını belirler. Enjeksiyon kütüphaneleri bu tarifi derleme sırasında ya da çalışırken işler; biz yalnız kurucu parametresini ve tarifin sonucunu gösteriyoruz.

```kotlin
// Platform kodu: CartRepository ve modül
import dagger.Module
import dagger.Provides
import dagger.hilt.InstallIn
import dagger.hilt.components.SingletonComponent
import javax.inject.Inject
import javax.inject.Singleton

class CartRepository @Inject constructor(
    private val remote: CartApi,
    private val local: CartDao,
)

@Module
@InstallIn(SingletonComponent::class)
object DataModule {
    @Provides
    @Singleton
    fun provideCartApi(): CartApi = CartApi.create()
}
```

Tüketici `CartApi` ve `CartDao` nesnesini kendisi kurmaz; hangisinin geleceğini modül belirler. Bu parça platform kodudur ve burada çıktı, süre ya da dosya yolu iddia etmiyoruz. Model, platformun bu kuralını kurar; model platformun kendisi değildir.

Daha önceki kurslardaki iki soruyu burada ayırmak gerekir. M12/K02 bir değişikliğin nereye ulaştığını, M12/K03 bir sonucu kimin değiştirebildiğini sorar. Bu derste kimin hangi nesneyi kurduğunu anlatıyoruz, ama ölçtüğümüz o değildir: kategori nesneyi veren sahip değil, okumada yanıt veren basamaktır. Konteyner zinciri kurmuş olsa da bakılan şey kurucu değildir; yanıtın hangi basamaktan geldiğidir.

Modelde konteyner bir zincirdir. `Container.read` zinciri bir okumaya verir. `preload` ağ yanıtından istenen anahtarları belleğe önceden yükler. `productionContainer` üretim zincirini kurar: belleği `preload` dönüşüyle kurar, diski boş bırakır, ağı doldurur. `testContainer` bellek basamağına sahte veri koyar. `CartScreenModel` tüketicidir ve konteyneri kurucudan alır; hangi zincirin geldiğini bilmez.

```kotlin
// Container.kt — konteyner zinciri kurar, tuketici zinciri kurucudan alir
class Container(private val chain: List<Tier>) {
    fun read(label: String, key: String, fallback: String): Read =
        Read(label, key, chain, fallback)
}

fun preload(network: Map<String, String>, keys: List<String>): Map<String, String> =
    keys.filter { it in network }.associateWith { network.getValue(it) }

fun productionContainer(network: Map<String, String>): Container =
    Container(listOf(Tier("bellek", preload(network, listOf("kampanya.kod"))), Tier("disk", mapOf()), Tier("ag", network)))

fun testContainer(fake: Map<String, String>, network: Map<String, String>): Container =
    Container(listOf(Tier("bellek", fake), Tier("disk", mapOf()), Tier("ag", network)))

class CartScreenModel(private val container: Container) {
    fun readAll(): List<Read> = listOf(
        container.read("baslik", "ekran.baslik", "bos"),
        container.read("urun-fiyati", "urun.fiyat", "0"),
        container.read("stok", "urun.stok", "0"),
        container.read("kampanya", "kampanya.kod", "yok"),
    )
}
```

#### Üretim Zinciri

Üretimde konteyner belleğe yalnız kampanya kodunu önceden yüklemek ister ve diski boş bırakır. Ağ yanıtı başlığı, fiyatı ve stoğu taşır; kampanya kodu yok, bu yüzden `preload` boş bir eşlem döndürür ve bellek de boş kalır.

```kotlin
// Main.kt — uretim zincirinde yanit agdan gelir
fun main() {
    val network = mapOf(
        "ekran.baslik" to "Sepet",
        "urun.fiyat" to "120",
        "urun.stok" to "7",
    )
    val model = CartScreenModel(productionContainer(network))
    val reads = model.readAll()
    reads.forEach { it.show() }
    countOrigins(reads)
}
```

```text
baslik: anahtar=ekran.baslik | deger=Sepet -> ag
urun-fiyati: anahtar=urun.fiyat | deger=120 -> ag
stok: anahtar=urun.stok | deger=7 -> ag
kampanya: anahtar=kampanya.kod | deger=yok -> varsayilan
bellek: 0
disk: 0
ag: 3
varsayilan: 1
```

Üç okuma ağdan geldi; kampanya hiçbir basamakta bulunmadığı için varsayılana düştü. Bellek ve disk bu zincirde yanıt vermeyen basamaklardı: boş olduklarından okuma onları geçip ağa ulaştı.

**UM1.** Model konteyneri düz bir zincir olarak kurar ve kapsamı, zinciri her çağrıda yeniden kurmakla taklit eder. Platformda konteyner nesne grafiğini kurar ve tekil nesneleri yaşatır; zincir bu grafiğin yalnız bir kesitidir.

#### Kapsam Değişir, Köken Değişmez

Aynı tüketici iki kullanıcının kapsamında çalışsın. Her kapsam kendi ağ yanıtıyla kurulur; tüketici sınıfı ikisinde de aynıdır.

```kotlin
// Main.kt — iki kapsam: ayni tuketici, ayni koken, farkli deger
fun main() {
    val scopes = mapOf(
        "kullanici-a" to mapOf("urun.fiyat" to "120"),
        "kullanici-b" to mapOf("urun.fiyat" to "95"),
    )
    for ((name, network) in scopes) {
        val model = CartScreenModel(productionContainer(network))
        println("kapsam: $name")
        model.readAll()[1].show()
    }
}
```

```text
kapsam: kullanici-a
urun-fiyati: anahtar=urun.fiyat | deger=120 -> ag
kapsam: kullanici-b
urun-fiyati: anahtar=urun.fiyat | deger=95 -> ag
```

İki kapsamda fiyat 120 ve 95 çıktı; ikisinin de kökeni ağ. Tüketici aynı kaldı, değer değişti, köken değişmedi. Kapsam değeri belirler; kural yalnız yanıtı kimin verdiğine bakar.

**UM2.** Model kapsamı bir ağ yanıtı haritası olarak kurar. Platformda kapsam bir nesnenin yaşam sınırıdır ve aynı kapsamdaki iki tüketici aynı örneği paylaşır; modelde nesne paylaşımı yoktur.

#### Sınırlayıcı Ölçüm

Test, enjeksiyonun asıl kazancıdır: tüketici değişmeden zincir değişir. Testte bellek basamağına sahte fiyat ve stok konur. Ağ yanıtı üretimdekiyle aynı kalır.

```kotlin
// Main.kt — testte bellek basamagina sahte veri enjekte edilir
fun main() {
    val network = mapOf(
        "ekran.baslik" to "Sepet",
        "urun.fiyat" to "120",
        "urun.stok" to "7",
    )
    val fake = mapOf("urun.fiyat" to "1", "urun.stok" to "1")
    val model = CartScreenModel(testContainer(fake, network))
    val reads = model.readAll()
    reads.forEach { it.show() }
    countOrigins(reads)
}
```

```text
baslik: anahtar=ekran.baslik | deger=Sepet -> ag
urun-fiyati: anahtar=urun.fiyat | deger=1 -> bellek
stok: anahtar=urun.stok | deger=1 -> bellek
kampanya: anahtar=kampanya.kod | deger=yok -> varsayilan
bellek: 2
disk: 0
ag: 1
varsayilan: 1
```

Aynı tüketici, aynı ağ yanıtı; ama fiyat ve stok artık bellekten geldi ve değerleri 1 oldu. Başlık yine ağdan, kampanya yine varsayılandan. Sahte veri bellek basamağına girince okumanın yanıtı oradan verildi. Kural basamağı kimin kurduğuna değil, yanıtı kimin verdiğine göre sınıflar.

**UM3.** Model sahte veriyi bir bellek basamağı olarak kurar. Platformda sahte nesne bir arayüzün test gerçeklemesidir; sahte nesnenin davranışı bu derste ölçülmez.

#### Özet

- Bu dersin kökeni: ağ. Konteynerin kurduğu üretim zincirinde bellek ve disk boştu, ağ doluydu; başlık, fiyat ve stok ağdan geldi.
- Dersin ölçütü, yanıt vermeyen basamakların ardından ağın yanıt vermesi. Üretimde ağ 3, varsayılan 1, bellek 0, disk 0 çıktı.
- İki kapsamda aynı tüketici 120 ve 95 okudu; ikisi de ağ. Kapsam değeri belirler, kökeni değil.
- Testte bellek basamağına sahte veri girince fiyat ve stok bellekten geldi: bellek 2, ağ 1, varsayılan 1.
- Kategori nesneyi kuran sahip değil, yanıtı veren basamaktır.

#### Sonraki Adım

Zincirin basamaklarını ve onları kuran yapıyı gördük. Bir sonraki kurs konusu, `Depo Kalıbı`, bu zinciri tek bir arayüzün arkasına toplar: ekran hangi basamaktan okunduğunu bilmeden ister, depo sırayı yönetir ve veri katmanı da aynı ölçüyle okunur.

## Veri Katmanı

Depo kalıbı, yerel veritabanı, anahtar–değer saklama, dosya ve medya depolama, ağ istemcisi, önbellekleme ve çevrimdışı destek, sayfalı veri yükleme ve arka plan işleri.

### Depo Kalıbı

Source: https://academia.sh/tr/kurslar/android-mimari/depo-kalibi

Depo kalıbı, verinin nereden geldiğini çağıran koddan saklar: yerel ve uzak veri kaynaklarını tek bir arayüzün arkasında sıralar. Ders sıralı bir okuma zincirinde yanıtın kökenini ve sıra değişince kökenin nasıl değiştiğini ölçer.

Önceki derste bir sınıfın bağımlılığını kendisi kurmak yerine dışarıdan aldığını gördük. Alan katmanının dışarıdan aldığı bağımlılık çoğu zaman tek bir nesnedir ve veri katmanı onu verir. Bu nesneye depo (repository) denir. Katman Modeli dersinde bir okumanın yanıtının dört kökenden gelebildiğini ölçmüştük: bellek, disk, ağ ya da varsayılan. Bu derste o ölçü yeni bir yere bakıyor: yanıtı veren zinciri kim kuruyor ve çağıran bunu biliyor mu? Dersin sorusu önceki derslerle aynıdır: **bu okumanın yanıtı nereden geliyor?**

Aşağıdaki çekirdek, kursun tüm derslerinde aynı kalır. Bir okuma, basamakların sıralı bir zincirini gezer ve anahtarı taşıyan ilk basamağın kökenini alır. Hiçbir basamak anahtarı taşımıyorsa yanıt varsayılandır.

```kotlin
// Origin.kt — bir okumanin yanitinin hangi basamaktan geldigini siniflandirir
class Tier(val origin: String, private val entries: Map<String, String>) {
    init {
        require(origin in listOf("bellek", "disk", "ag")) { "gecersiz basamak: $origin" }
    }

    fun lookup(key: String): String? = entries[key]
}

class Read(val label: String, val key: String, chain: List<Tier>, fallback: String) {
    private val hit: Tier? = chain.firstOrNull { it.lookup(key) != null }
    val value: String = hit?.lookup(key) ?: fallback
    val origin: String = hit?.origin ?: "varsayilan"

    fun show() {
        println("$label: anahtar=$key | deger=$value -> $origin")
    }
}

fun countOrigins(reads: List<Read>) {
    for (name in listOf("bellek", "disk", "ag", "varsayilan")) {
        println("$name: ${reads.count { it.origin == name }}")
    }
}
```

#### Veri Kaynağı ve Depo

Uygulamanın verisi birden çok yerde durur. Disk üzerindeki kayıt yerel veri kaynağıdır (data source), sunucudaki kayıt uzak veri kaynağıdır. Her biri kendi dilini konuşur: biri satır ve sütun, öteki istek ve yanıt. Çağıran kod bu dilleri bilirse, kaynak her değiştiğinde çağıran da değişir. Depo kalıbı (repository pattern) bunu keser: depo, kaynakları kendi içinde bir sıraya dizer ve dışarıya tek bir okuma işlemi sunar.

Platformda bu, bir arayüz ve onu kullanan bir sınıf olarak yazılır. Sınıf yerel kaynağa bakar, bulamazsa uzak kaynağa gider ve gelen yanıtı yerele yazar.

```kotlin
// Platform kodu: ProductRepository
interface ProductLocalDataSource {
    suspend fun price(id: String): String?
    suspend fun save(id: String, price: String)
}

interface ProductRemoteDataSource {
    suspend fun fetchPrice(id: String): String
}

class ProductRepository(
    private val local: ProductLocalDataSource,
    private val remote: ProductRemoteDataSource,
) {
    suspend fun price(id: String): String =
        local.price(id) ?: remote.fetchPrice(id).also { local.save(id, it) }
}
```

`price` çağıran için tek bir askıya alınan fonksiyondur (suspend function). Fiyatın yerelden mi uzaktan mı geldiği çağıranın görmediği bir ayrıntıdır. Bu parça platform kodudur ve burada çıktı, süre ya da kaynak sayısı iddia etmiyoruz. Model, platformun bu kuralını kurar; model platformun kendisi değildir.

Modelde veri kaynağı bir `Tier`'dır ve depo, `Tier` listesini tutan küçük bir sınıftır. Çağıran kod yalnızca depoyu tanır.

```kotlin
// Repository.kt — depo, basamaklari bir sirayla dizer ve tek bir okuma sunar
class Repository(private val chain: List<Tier>) {
    fun read(label: String, key: String, fallback: String): Read = Read(label, key, chain, fallback)
}
```

Bellek basamağını da kendi başına yazmıyoruz. Belleğin içeriği, başka bir kaynaktan çekilmiş anahtarların eşlemidir ve bu çekmeyi bir işlev yapar.

```kotlin
// Sources.kt — bir veri kaynagindan verilen anahtarlarin eslemini cikarir
fun loadFrom(source: Tier, keys: List<String>): Map<String, String> =
    keys.mapNotNull { key -> source.lookup(key)?.let { key to it } }.toMap()
```

Çağıran kod da tek bir yerde durur. Bir ekranın okuduğu beş değeri depodan ister; hangi kaynaktan geldiğini bilmez.

```kotlin
// Caller.kt — cagiran kod depoyu tanir, veri kaynaklarini tanimaz
fun readProfileScreen(repository: Repository): List<Read> = listOf(
    repository.read("profil-adi", "profil.ad", "bilinmiyor"),
    repository.read("urun-fiyati", "urun.fiyat", "bilinmiyor"),
    repository.read("tema", "ayar.tema", "acik"),
    repository.read("stok", "urun.stok", "yok"),
    repository.read("kampanya", "kampanya.kod", "yok"),
)
```

#### Önce Belleğe Bakan Zincir

Önce zinciri kuralım. Disk, kayıtlı üç değeri tutar. Sunucu dört değer bilir; fiyatı diskten farklıdır, çünkü sunucuda güncellenmiştir. Bellek basamağı, diskten `profil.ad` ve `urun.fiyat` çekilerek dolduruldu. Depo sırayı bellek, disk, ağ olarak dizer.

```kotlin
// Main.kt — depo okumayi bellek, disk, ag sirasiyla dener
fun main() {
    val disk = Tier("disk", mapOf("profil.ad" to "Ayse", "urun.fiyat" to "120", "ayar.tema" to "koyu"))
    val remote = Tier("ag", mapOf("profil.ad" to "Ayse", "urun.fiyat" to "125", "urun.stok" to "8"))
    val memory = Tier("bellek", loadFrom(disk, listOf("profil.ad", "urun.fiyat")))
    val repository = Repository(listOf(memory, disk, remote))
    val reads = readProfileScreen(repository)
    reads.forEach { it.show() }
    countOrigins(reads)
}
```

```text
profil-adi: anahtar=profil.ad | deger=Ayse -> bellek
urun-fiyati: anahtar=urun.fiyat | deger=120 -> bellek
tema: anahtar=ayar.tema | deger=koyu -> disk
stok: anahtar=urun.stok | deger=8 -> ag
kampanya: anahtar=kampanya.kod | deger=yok -> varsayilan
bellek: 2
disk: 1
ag: 1
varsayilan: 1
```

Beş okumanın beş yanıtı, dört kökene dağıldı. `profil.ad` ve `urun.fiyat` belleğe alındığı için ilk basamakta bulundu. `ayar.tema` bellekte yoktur, diskte vardır. `urun.stok` yalnız sunucuda vardır. `kampanya.kod` hiçbir basamakta yoktur ve çağıranın verdiği yedek değer döner. Bu dersin kökeni bellektir: depoya ilk giren okuma, zincirin başında bulunan basamaktan yanıtlanır.

Dikkat edilecek nokta `urun.fiyat` değeridir: 120. Sunucuda 125 vardır, ama okuma sunucuya hiç ulaşmadı. Kural, sırayı görür, tazeliği görmez. Bellekteki değer bayat olsa da, anahtar orada olduğu sürece zincir ilk eşleşmede durur.

**VK1.** Model, belleğin diskten bir kez dolduğunu ve sonra değişmediğini varsayar. Platformda bellek önbelleğinin boyut sınırı, çıkarma kuralı ve bir değer yazıldığında önbelleğin güncellenmesi modelde yoktur; modelde bellek yalnız bir eşlemdir.

#### Çağıran Zincirden Habersizdir

Depo kalıbının değeri, çağıranın zincirden habersiz kalmasıdır. Aynı `readProfileScreen` işlevi iki farklı depoyla çağrılabilir. Birincisi üç basamaklı zincir, ikincisi yalnız ağdan oluşan bir zincirdir: çevrimdışı hazırlanmamış, yerel kaynağı olmayan bir ilk sürüm gibi.

```kotlin
// Main.kt — ayni cagiran kod, iki farkli depo
fun main() {
    val disk = Tier("disk", mapOf("profil.ad" to "Ayse", "urun.fiyat" to "120", "ayar.tema" to "koyu"))
    val remote = Tier("ag", mapOf("profil.ad" to "Ayse", "urun.fiyat" to "125", "urun.stok" to "8"))
    val memory = Tier("bellek", loadFrom(disk, listOf("profil.ad", "urun.fiyat")))
    val chains = listOf(
        "tam" to listOf(memory, disk, remote),
        "yalniz-ag" to listOf(remote),
    )
    for ((name, chain) in chains) {
        println("zincir: $name")
        countOrigins(readProfileScreen(Repository(chain)))
    }
}
```

```text
zincir: tam
bellek: 2
disk: 1
ag: 1
varsayilan: 1
zincir: yalniz-ag
bellek: 0
disk: 0
ag: 3
varsayilan: 2
```

Çağıran işlev tek karakter değişmeden iki depoyla çalıştı. İkinci zincirde bellek ve disk sıfırdır. `ayar.tema` sunucuda olmadığı için varsayılana düştü ve `kampanya.kod` yine yedeğe kaldı. Çağıran kod, kaynak eklemek ya da çıkarmak için değişmedi; değişen tek şey köken dağılımı oldu.

#### Sınırlayıcı Ölçüm

Depo sırayı değiştirirse ne olur? Önce ağa bakan bir zincirde aynı anahtarlar sunucudan gelir. Aynı üç anahtarı iki sırayla okuyalım: önce bellek, disk, ağ; sonra ağ, bellek, disk.

```kotlin
// Main.kt — sira degisince ayni anahtarin kokeni degisir
fun main() {
    val disk = Tier("disk", mapOf("profil.ad" to "Ayse", "urun.fiyat" to "120", "ayar.tema" to "koyu"))
    val remote = Tier("ag", mapOf("profil.ad" to "Ayse", "urun.fiyat" to "125", "urun.stok" to "8"))
    val memory = Tier("bellek", loadFrom(disk, listOf("profil.ad", "urun.fiyat")))
    val orders = listOf(
        "bellek-once" to listOf(memory, disk, remote),
        "ag-once" to listOf(remote, memory, disk),
    )
    for ((name, chain) in orders) {
        println("sira: $name")
        val repository = Repository(chain)
        listOf("profil.ad", "urun.fiyat", "ayar.tema")
            .map { repository.read(it.replace('.', '-'), it, "yok") }
            .forEach { it.show() }
    }
}
```

```text
sira: bellek-once
profil-ad: anahtar=profil.ad | deger=Ayse -> bellek
urun-fiyat: anahtar=urun.fiyat | deger=120 -> bellek
ayar-tema: anahtar=ayar.tema | deger=koyu -> disk
sira: ag-once
profil-ad: anahtar=profil.ad | deger=Ayse -> ag
urun-fiyat: anahtar=urun.fiyat | deger=125 -> ag
ayar-tema: anahtar=ayar.tema | deger=koyu -> disk
```

`profil.ad` iki sırada da `Ayse` değerini verdi, ama köken bellekten ağa geçti. Değer aynıydı; kural değeri değil, anahtarı ilk taşıyan basamağı görür. `urun.fiyat` ise hem kökenini hem değerini değiştirdi: 120 yerine 125. `ayar.tema` ağda bulunmadığı için her iki sırada da diskten geldi. Sıra, hangi basamağın sözü geçtiğini belirler; hangisinin doğru olduğunu belirlemez.

Ölçümün söylemediği şey, hangi sıranın iyi olduğudur. Önce belleğe bakan zincir ağa gitmeden yanıt verir, önce ağa bakan zincir güncel değeri alır. Kural bu iki yararı tartmaz; yalnız yanıtın hangi basamaktan geldiğini söyler.

**VK2.** Model, bir basamağın anahtarı ya taşıdığını ya taşımadığını varsayar. Platformda uzak kaynağın hata vermesi, zaman aşımı ya da yarım yanıt dönmesi modelde yoktur; modelde ağ basamağı her zaman cevap verir ya da hiç taşımaz.

#### Özet

- Bu dersin kökeni: bellek. Depoya ilk giren okuma, zincirin başındaki basamaktan yanıtlanır.
- Depo kalıbı, yerel ve uzak veri kaynaklarını bir sıraya dizer ve çağırana tek bir okuma sunar.
- Aynı çağıran kod iki farklı depoyla değişmeden çalışır; değişen tek şey köken dağılımıdır: 2, 1, 1, 1 ile 0, 0, 3, 2.
- Sıra değişince aynı anahtarın kökeni değişir. `profil.ad` değerini korudu, `urun.fiyat` değerini de değiştirdi: 120 yerine 125.
- Kural sırayı görür, tazeliği görmez; bellekteki bayat değer sunucuya bakılmasını engeller.

#### Sonraki Adım

Bu derste disk basamağı, hazır bir eşlemdi. Platformda disk, satırları ve sütunları olan bir veritabanıdır ve şeması zamanla değişir. Sıradaki ders, `Yerel Veritabanı`, şema değişince eski satırların hangi basamaktan okunduğunu ve yeni sütunun eski satırlarda neye düştüğünü ölçer.

### Yerel Veritabanı

Source: https://academia.sh/tr/kurslar/android-mimari/yerel-veritabani

Yerel veritabanı, uygulamanın verisini aygıt diskinde varlık ve veri erişim nesnesi ile tutar; şema değişince göç eski satırları yeni şemaya taşır. Ders göçten sonra bir okumanın yanıtının kökenini ve yeni sütunun eski satırlarda neye düştüğünü ölçer.

Önceki derste depo, kaynakları bir sıraya dizdi ve disk basamağı hazır bir eşlemdi. Platformda bu basamağın arkasında bir yerel veritabanı durur ve onun şeması uygulamayla birlikte büyür. Bu derste disk basamağının nasıl dolduğuna ve şema değişince eski satırların ne olduğuna bakıyoruz. Dersin sorusu önceki derslerle aynıdır: **bu okumanın yanıtı nereden geliyor?**

```kotlin
// Origin.kt — bir okumanin yanitinin hangi basamaktan geldigini siniflandirir
class Tier(val origin: String, private val entries: Map<String, String>) {
    init {
        require(origin in listOf("bellek", "disk", "ag")) { "gecersiz basamak: $origin" }
    }

    fun lookup(key: String): String? = entries[key]
}

class Read(val label: String, val key: String, chain: List<Tier>, fallback: String) {
    private val hit: Tier? = chain.firstOrNull { it.lookup(key) != null }
    val value: String = hit?.lookup(key) ?: fallback
    val origin: String = hit?.origin ?: "varsayilan"

    fun show() {
        println("$label: anahtar=$key | deger=$value -> $origin")
    }
}

fun countOrigins(reads: List<Read>) {
    for (name in listOf("bellek", "disk", "ag", "varsayilan")) {
        println("$name: ${reads.count { it.origin == name }}")
    }
}
```

#### Varlık, Veri Erişim Nesnesi ve Veritabanı

Yerel veritabanı kütüphanesi üç parçadan oluşur. Varlık (entity), bir tablonun satırını anlatan sınıftır; her alanı bir sütundur. Veri erişim nesnesi (data access object, DAO), o tabloya yapılan sorguların ve yazmaların arayüzüdür. Veritabanı sınıfı varlıkları ve şema sürümünü bir araya getirir ve veri erişim nesnelerini sunar. Bundan sonra yalnızca DAO diyeceğiz.

```kotlin
// Platform kodu: AppDatabase
@Entity(tableName = "cart_item")
data class CartItemEntity(
    @PrimaryKey val productId: String,
    val quantity: Int,
    val stock: Int = 0,
)

@Dao
interface CartItemDao {
    @Query("SELECT * FROM cart_item WHERE productId = :id")
    suspend fun find(id: String): CartItemEntity?

    @Insert(onConflict = OnConflictStrategy.REPLACE)
    suspend fun save(item: CartItemEntity)
}

@Database(entities = [CartItemEntity::class], version = 2)
abstract class AppDatabase : RoomDatabase() {
    abstract fun cartItemDao(): CartItemDao
}
```

Sürüm numarası şemanın kimliğidir. Uygulamanın yeni sürümü sütun eklediğinde numara 1'den 2'ye çıkar ve eski sürümle yazılmış satırlar diskte durmaya devam eder. Bu parça platform kodudur ve burada çıktı, süre ya da dosya yolu iddia etmiyoruz. Model, platformun bu kuralını kurar; model platformun kendisi değildir.

Modelde veritabanı, bir disk basamağıdır; satırlar ise anahtar ve değer çiftleridir. Şema değişikliği bu çiftlerin anahtarlarını etkiler.

#### Göç Satırları Taşır

Sürüm 1'de sepetin adedi `sepet.miktar` sütunundaydı. Sürüm 2'de bu sütun `sepet.adet` oldu ve yeni bir sütun, `urun.stok`, eklendi. Göç (migration), eski sürümün satırlarını yeni sürümün şemasına taşıyan kuraldır. Platformda bunu bir sınıf yazar ve içinde şema komutları bulunur.

```kotlin
// Platform kodu: Migration1To2
val migration1To2 = object : Migration(1, 2) {
    override fun migrate(db: SupportSQLiteDatabase) {
        db.execSQL("ALTER TABLE cart_item RENAME COLUMN quantity TO count")
        db.execSQL("ALTER TABLE cart_item ADD COLUMN stock INTEGER NOT NULL DEFAULT 0")
    }
}
```

```sql
ALTER TABLE cart_item RENAME COLUMN quantity TO count;
ALTER TABLE cart_item ADD COLUMN stock INTEGER NOT NULL DEFAULT 0;
```

Platformda `DEFAULT 0` ifadesi eski satırlara sıfır yazar. Modelde ise bu davranışı iki ayrı seçim olarak ayıracağız. Göç işlevi anahtarları yeniden adlandırır ve istenirse yeni sütunun değerini eski satırlara yazar.

```kotlin
// Migrate.kt — eski satirlari yeni semaya tasir, yeni sutunu istenirse doldurur
fun migrate(
    rows: Map<String, String>,
    renames: Map<String, String>,
    backfill: Map<String, String>,
): Map<String, String> =
    rows.map { (key, value) -> (renames[key] ?: key) to value }.toMap() + backfill
```

Ekran, sürüm 2'nin şemasına göre dört değer okur.

```kotlin
// Screen.kt — sema surumu 2'ye gore okunan dort deger
fun readCartScreen(disk: Tier): List<Read> {
    val chain = listOf(disk)
    return listOf(
        Read("profil-adi", "profil.ad", chain, "bilinmiyor"),
        Read("urun-fiyati", "urun.fiyat", chain, "bilinmiyor"),
        Read("sepet-adedi", "sepet.adet", chain, "0"),
        Read("urun-stogu", "urun.stok", chain, "0"),
    )
}
```

Önce göçten önceki hali ölçelim. Disk, sürüm 1 ile yazılmış üç satır taşır ve henüz kimse onları dönüştürmemiştir.

```kotlin
// Main.kt — goc yokken surum 2'nin sorgusu surum 1'in satirlarina bakar
fun main() {
    val v1Rows = mapOf("profil.ad" to "Ayse", "urun.fiyat" to "120", "sepet.miktar" to "2")
    val reads = readCartScreen(Tier("disk", v1Rows))
    reads.forEach { it.show() }
    countOrigins(reads)
}
```

```text
profil-adi: anahtar=profil.ad | deger=Ayse -> disk
urun-fiyati: anahtar=urun.fiyat | deger=120 -> disk
sepet-adedi: anahtar=sepet.adet | deger=0 -> varsayilan
urun-stogu: anahtar=urun.stok | deger=0 -> varsayilan
bellek: 0
disk: 2
ag: 0
varsayilan: 2
```

`profil.ad` ve `urun.fiyat` adı değişmeyen sütunlardır, diskten okundu. Adedin diskte durduğu anahtar `sepet.miktar`'dı; sorgu `sepet.adet`'i aradığı için satır var olduğu hâlde bulunmadı ve yanıt varsayılana düştü. Veri kaybolmadı; yeni şema onu eski adıyla görmüyor.

Şimdi göçü uygulayalım. Yalnız yeniden adlandırma var, yeni sütun doldurulmuyor.

```kotlin
// Main.kt — goc adi degisen sutunu tasir
fun main() {
    val v1Rows = mapOf("profil.ad" to "Ayse", "urun.fiyat" to "120", "sepet.miktar" to "2")
    val migrated = migrate(v1Rows, mapOf("sepet.miktar" to "sepet.adet"), emptyMap())
    val reads = readCartScreen(Tier("disk", migrated))
    reads.forEach { it.show() }
    countOrigins(reads)
}
```

```text
profil-adi: anahtar=profil.ad | deger=Ayse -> disk
urun-fiyati: anahtar=urun.fiyat | deger=120 -> disk
sepet-adedi: anahtar=sepet.adet | deger=2 -> disk
urun-stogu: anahtar=urun.stok | deger=0 -> varsayilan
bellek: 0
disk: 3
ag: 0
varsayilan: 1
```

Adet artık diskten okunuyor ve değeri 2'dir. Bu dersin kökeni disktir: göçten geçmiş satırın yanıtı diske aittir. `urun.stok` ise sürüm 1'de hiç yazılmadığı için diskte yoktur.

**VK1.** Model, göçü tek adımlık bir eşleme olarak kurar: anahtarı yeniden adlandırır, istenirse bir değer ekler. Platformda göçün ara sürümlerden geçmesi, tablonun yeniden oluşturulması ve göç sırasında verinin tutarlılığını koruyan işlem sınırları modelde yoktur.

#### Sınırlayıcı Ölçüm

Yeni sütun eski satırlarda ne olur? Göç onu doldurmazsa eski satırda yoktur ve yukarıdaki ölçümde gördüğümüz gibi o alan varsayılana düşer. Sütunun varsayılan değeri `Read`'in dördüncü parametresidir. Göç `DEFAULT 0` gibi bir kuralla sütunu doldurursa durum değişir.

```kotlin
// Main.kt — ayni deger, iki goc: doldurulmayan sutun varsayilan, doldurulan sutun disk
fun main() {
    val v1Rows = mapOf("profil.ad" to "Ayse", "urun.fiyat" to "120", "sepet.miktar" to "2")
    val renames = mapOf("sepet.miktar" to "sepet.adet")
    val plans = listOf(
        "doldurmadan" to emptyMap<String, String>(),
        "doldurarak" to mapOf("urun.stok" to "0"),
    )
    for ((name, backfill) in plans) {
        println("goc: $name")
        val migrated = migrate(v1Rows, renames, backfill)
        readCartScreen(Tier("disk", migrated)).filter { it.key == "urun.stok" }.forEach { it.show() }
    }
}
```

```text
goc: doldurmadan
urun-stogu: anahtar=urun.stok | deger=0 -> varsayilan
goc: doldurarak
urun-stogu: anahtar=urun.stok | deger=0 -> disk
```

İki göçte de `urun.stok` değeri 0'dır. Köken ise farklıdır: biri okuyucunun yedek değeri, öteki diskte yazılı bir satır. Kural değeri değil, anahtarı taşıyan ilk basamağı görür. Bunun yararı, bir sütunun hangi satırlarda yazıldığının, hangilerinde yalnızca öyle göründüğünün ayrılabilmesidir. Modeli kuran kişi doldurmanın değeri değiştirmediğini, ama yanıtın sahibini değiştirdiğini görür.

Bu ölçümün söylemediği şey, hangi göçün doğru olduğudur. Eski satırlara değer yazmak veritabanını büyütür ve göç süresini uzatır; yazmamak ise bu alanın okunduğu her yerde bir yedek değer gerektirir. Kural bu bedeli tartmaz.

**VK2.** Model, göçü ya uygulanmış ya uygulanmamış olarak ele alır. Platformda göçün yarıda kesilmesi, eksik bir göç tanımı olması ya da veritabanının eski sürüme geri alınması modelde yoktur; bu durumlarda platform hata verir ve model bu hatayı kurmaz.

#### Özet

- Bu dersin kökeni: disk. Göçten geçmiş bir satırın okuması, disk basamağından yanıtlanır.
- Varlık satırı, veri erişim nesnesi sorguyu, veritabanı sınıfı şema sürümünü taşır.
- Göç eski satırları yeni şemaya taşır; taşınmamış satır yeni adla aranınca bulunmaz ve varsayılana düşer: disk 2, varsayılan 2.
- Yeniden adlandırma göçüyle adet 2 değeriyle diskten okunur: disk 3, varsayılan 1.
- Doldurulmayan yeni sütun varsayılana düşer, doldurulan disk olur; iki durumda da değer 0'dır, köken farklıdır.

#### Sonraki Adım

Bu derste disk, satırları ve sütunları olan bir veritabanıydı. Her veri bir tabloya girecek kadar yapılı değildir: tema, dil ve bildirim gibi küçük tercihler tek anahtarla yazılır. Sıradaki ders, `Anahtar–Değer Saklama`, hiç yazılmamış bir tercihin hangi kökenden okunduğunu ve kullanıcı yazınca kökenin nasıl değiştiğini ölçer.

### Anahtar–Değer Saklama

Source: https://academia.sh/tr/kurslar/android-mimari/anahtar-deger-saklama

Anahtar–değer deposu küçük tercihleri tek bir anahtarla saklar; okuma ve yazma eşzamansızdır ve değer akış olarak gelir. Ders hiç yazılmamış bir tercihin okumasının kökenini ve kullanıcı yazınca bu kökenin nasıl değiştiğini ölçer.

Önceki derste bir göç, tablodaki satırları yeni şemaya taşıdı. Her veri bir tabloya girecek kadar yapılı değildir: tema, dil ve bildirim açık mı gibi tercihlerin her biri tek bir anahtarla yazılan tek bir değerdir. Bu derste o küçük veriye bakıyoruz ve en çok ilgilendiğimiz an, kullanıcının daha hiçbir şey yazmadığı andır. Dersin sorusu önceki derslerle aynıdır: **bu okumanın yanıtı nereden geliyor?**

```kotlin
// Origin.kt — bir okumanin yanitinin hangi basamaktan geldigini siniflandirir
class Tier(val origin: String, private val entries: Map<String, String>) {
    init {
        require(origin in listOf("bellek", "disk", "ag")) { "gecersiz basamak: $origin" }
    }

    fun lookup(key: String): String? = entries[key]
}

class Read(val label: String, val key: String, chain: List<Tier>, fallback: String) {
    private val hit: Tier? = chain.firstOrNull { it.lookup(key) != null }
    val value: String = hit?.lookup(key) ?: fallback
    val origin: String = hit?.origin ?: "varsayilan"

    fun show() {
        println("$label: anahtar=$key | deger=$value -> $origin")
    }
}

fun countOrigins(reads: List<Read>) {
    for (name in listOf("bellek", "disk", "ag", "varsayilan")) {
        println("$name: ${reads.count { it.origin == name }}")
    }
}
```

#### Tercih Verisi

Anahtar–değer deposu (key-value store), değerleri bir tabloya değil, dize anahtarlara bağlar. Şema yoktur; bir anahtar ya vardır ya yoktur. Tercih (preference) bu depoya yazılan küçük bir ayardır. Platformdaki tercih deposunun iki özelliği vardır. Birincisi, okuma ve yazmanın eşzamansız olmasıdır: ikisi de askıya alınan fonksiyondur ve ana iş parçacığını bloklamaz. İkincisi, okumanın bir kerelik değer değil, akış olarak gelmesidir: tercih her değiştiğinde yeni değer akıştan çıkar.

```kotlin
// Platform kodu: ThemePreferences
class ThemePreferences(private val dataStore: DataStore<Preferences>) {
    private val themeKey = stringPreferencesKey("ayar.tema")

    val theme: Flow<String> = dataStore.data.map { it[themeKey] ?: "acik" }

    suspend fun setTheme(value: String) {
        dataStore.edit { it[themeKey] = value }
    }
}
```

`theme` akışı, anahtar yazılmamışsa `"acik"` değerini üretir. Bu satır dersin konusunun kendisidir: tercih deposu yazılmamış anahtar için bir değer vermez, onu okuyan kod `?:` ile bir yedek seçer. Bu parça platform kodudur ve burada çıktı, süre ya da dosya yolu iddia etmiyoruz. Model, platformun bu kuralını kurar; model platformun kendisi değildir.

Modelde tercih deposu bir disk basamağıdır. Yazma, eski eşlemi bozmadan yeni bir eşlem veren bir işlevdir.

```kotlin
// Preferences.kt — yazma, eski eslemi bozmadan yeni bir esleme verir
fun write(store: Map<String, String>, key: String, value: String): Map<String, String> =
    store + (key to value)
```

Ayarlar ekranı üç tercih okur. Her birinin kendi yedek değeri vardır: tema için açık, dil için Türkçe, bildirim için evet.

```kotlin
// Settings.kt — ayarlar ekraninin okudugu uc tercih
fun readSettings(disk: Tier): List<Read> {
    val chain = listOf(disk)
    return listOf(
        Read("tema", "ayar.tema", chain, "acik"),
        Read("dil", "ayar.dil", chain, "tr"),
        Read("bildirim", "bildirim.acik", chain, "evet"),
    )
}
```

#### Hiç Yazılmamış Tercih

Kullanıcı uygulamayı ilk kez açtı. Tercih deposunda yalnız profilin adı var; ayar anahtarlarından hiçbiri yazılmamış.

```kotlin
// Main.kt — hic yazilmamis tercih yedek degerden okunur
fun main() {
    val store = write(emptyMap(), "profil.ad", "Ayse")
    val reads = readSettings(Tier("disk", store))
    reads.forEach { it.show() }
    countOrigins(reads)
}
```

```text
tema: anahtar=ayar.tema | deger=acik -> varsayilan
dil: anahtar=ayar.dil | deger=tr -> varsayilan
bildirim: anahtar=bildirim.acik | deger=evet -> varsayilan
bellek: 0
disk: 0
ag: 0
varsayilan: 3
```

Üç okumanın üçü de varsayılana düştü. Disk basamağı sıfır eşleşme verdi, çünkü ayar anahtarlarının hiçbiri yazılmamış. Bu dersin kökeni varsayılandır: tercih deposu, yazılmadıkça yanıt vermeyen bir basamaktır.

Burada bir ayrım yapmak gerekir. Android Platformu ve Araçlar kursunda (M31/K02) platformun istenen kaynağı bulamayıp başka bir kaynak vermesi ikame diye sınıflanıyordu. Bu dersteki varsayılan o ikame değildir: orada platform başka bir değer seçiyor ve seçilen değer istenenden farklı olabiliyordu; burada hiçbir basamak anahtarı taşımıyor ve yedek değeri okuyan kod kendisi getiriyor.

**VK1.** Model, tercih deposunun yalnız yazılmış anahtarları taşıdığını varsayar. Platformda bir anahtarın silinmesi, tercihin farklı bir türle yazılması ve depo dosyasının bozulması modelde yoktur.

#### Kullanıcı Yazınca

Kullanıcı temayı koyu yapar. Yazma, eski eşlemi bozmadan yeni bir eşlem verir ve tema anahtarı artık diskte vardır.

```kotlin
// Main.kt — kullanici temayi yazinca ayni okuma diskten gelir
fun main() {
    val store = write(write(emptyMap(), "profil.ad", "Ayse"), "ayar.tema", "koyu")
    val reads = readSettings(Tier("disk", store))
    reads.forEach { it.show() }
    countOrigins(reads)
}
```

```text
tema: anahtar=ayar.tema | deger=koyu -> disk
dil: anahtar=ayar.dil | deger=tr -> varsayilan
bildirim: anahtar=bildirim.acik | deger=evet -> varsayilan
bellek: 0
disk: 1
ag: 0
varsayilan: 2
```

Tema diskten okunuyor ve değeri koyudur. Dil ve bildirim değişmedi, hâlâ varsayılandır. Okuyan kod aynı kaldı; yalnız deponun içeriği değişti.

#### Sınırlayıcı Ölçüm

Yazılan değer, yedek değerle aynı olursa ne olur? Ya da boş bir dizgi yazılırsa? Üç durumu aynı anda deneyelim. Tema, yedeğiyle aynı değer olan `acik` olarak yazılıyor. Dil, boş dizgi olarak yazılıyor. Bildirim yazılmıyor.

```kotlin
// Main.kt — yedekle ayni deger ve bos dizgi de yazilmis tercihtir
fun main() {
    val written = write(write(emptyMap(), "ayar.tema", "acik"), "ayar.dil", "")
    val reads = readSettings(Tier("disk", written))
    reads.forEach { it.show() }
    countOrigins(reads)
}
```

```text
tema: anahtar=ayar.tema | deger=acik -> disk
dil: anahtar=ayar.dil | deger= -> disk
bildirim: anahtar=bildirim.acik | deger=evet -> varsayilan
bellek: 0
disk: 2
ag: 0
varsayilan: 1
```

Tema hâlâ `acik` ve bu kez disktendir. Yedek değerle yazılan değer aynıydı; köken değişti, çünkü anahtar artık yazılı. Dilin değeri boş dizgi ve onun da kökeni disktir. Boş dizgi bir yanıttır, varsayılan değildir: `Tier.lookup` boş dizgiyi `null` saymaz, anahtarı taşıdığını söyler. Okuma yedek değer olan `tr`'ye düşmedi.

Bu ölçümün yararı, "tercih boş" ile "tercih hiç yazılmamış" ayrımını görünür kılmasıdır. Kullanıcı dili bilerek boş bıraktıysa, yedek değer o kararı ezmemelidir. Kuralın söylemediği şey, boş dizginin uygulama için anlamlı olup olmadığıdır; bunu ders değil, o tercihi kullanan kod söyler.

**VK2.** Model, tercihi dize olarak saklar ve boş dizgiyi geçerli bir değer sayar. Platformda tercihin sayı, mantıksal değer ya da dize türünde tanımlanması ve türe göre farklı anahtar nesnesi gerekmesi modelde yoktur.

#### Özet

- Bu dersin kökeni: varsayılan. Hiç yazılmamış bir tercihi hiçbir basamak taşımaz ve yedek değeri okuyan kod verir.
- Anahtar–değer deposu küçük tercihleri tek bir anahtarla saklar; platformda okuma akış, yazma askıya alınan fonksiyondur.
- Boş depoda üç okumanın üçü varsayılandır: disk 0, varsayılan 3.
- Kullanıcı bir tercihi yazınca aynı okuma disk olur: disk 1, varsayılan 2. Yedekle aynı değer yazılırsa değer aynı kalır, köken disk olur.
- Boş dizgi yazılmış bir tercihtir; okuması diskten gelir ve yedek değere düşmez.

#### Sonraki Adım

Bu derste disk basamağı, tek bir anahtarlı küçük bir mağazaydı. Uygulama bazen bir resim ya da belge gibi bir dosya saklar ve dosya yalnız uygulamaya ait bir dizinde ya da başka uygulamaların da gördüğü ortak bir yerde durabilir. Sıradaki ders, `Dosya ve Medya Depolama`, uygulamanın kendi dizinindeki dosyanın hangi kökenden okunduğunu ve başka uygulamanın dosyasının neden zincire girmediğini ölçer.

### Dosya ve Medya Depolama

Source: https://academia.sh/tr/kurslar/android-mimari/dosya-ve-medya-depolama

Uygulama kendi dizinindeki dosyalara doğrudan, başkalarının medyasına ise medya deposu ve izin kuralları üzerinden erişir. Ders uygulamaya özel dizindeki bir dosyanın okumasının kökenini ve zincire girmeyen dosyanın neye düştüğünü ölçer.

Önceki derste disk basamağı küçük bir tercih mağazasıydı. Uygulama bazen bir fotoğraf, bir belge ya da bir fatura saklar ve bu dosyalar bir satıra ya da tek bir dizgiye sığmaz. Dosyanın nerede durduğu da önemlidir: uygulamanın kendi dizininde mi, yoksa başka uygulamaların da gördüğü ortak bir yerde mi? Dersin sorusu önceki derslerle aynıdır: **bu okumanın yanıtı nereden geliyor?**

```kotlin
// Origin.kt — bir okumanin yanitinin hangi basamaktan geldigini siniflandirir
class Tier(val origin: String, private val entries: Map<String, String>) {
    init {
        require(origin in listOf("bellek", "disk", "ag")) { "gecersiz basamak: $origin" }
    }

    fun lookup(key: String): String? = entries[key]
}

class Read(val label: String, val key: String, chain: List<Tier>, fallback: String) {
    private val hit: Tier? = chain.firstOrNull { it.lookup(key) != null }
    val value: String = hit?.lookup(key) ?: fallback
    val origin: String = hit?.origin ?: "varsayilan"

    fun show() {
        println("$label: anahtar=$key | deger=$value -> $origin")
    }
}

fun countOrigins(reads: List<Read>) {
    for (name in listOf("bellek", "disk", "ag", "varsayilan")) {
        println("$name: ${reads.count { it.origin == name }}")
    }
}
```

#### Uygulamaya Özel Dizin

Her uygulamanın platformun verdiği, yalnızca kendisine ait bir dizini vardır. Bu dizin uygulamaya özel dizin (app-specific directory) diye anılır. Uygulama oraya dosya yazmak ve oradan dosya okumak için özel bir izin istemez; başka uygulamalar o dizini göremez. Dizin uygulama silindiğinde de silinir.

```kotlin
// Platform kodu: PrivateFiles
class PrivateFiles(private val context: Context) {
    fun save(name: String, bytes: ByteArray) {
        File(context.filesDir, name).writeBytes(bytes)
    }

    fun read(name: String): ByteArray? {
        val file = File(context.filesDir, name)
        return if (file.exists()) file.readBytes() else null
    }
}
```

`read` dosya yoksa `null` döner; yani bu sınıf da önceki derslerdeki depolar gibi yazılmamış bir şey için yanıt vermez. Bu parça platform kodudur ve burada çıktı, dosya yolu ya da boyut iddia etmiyoruz. Model, platformun bu kuralını kurar; model platformun kendisi değildir.

Modelde dizin, dosya adlarından bir eşlem çıkaran bir işlevdir. Her dosya `dosya.` önekiyle bir anahtara bağlanır; değer, dosyanın dizin içindeki yoludur. Gerçek bir dosya okunmaz ya da yazılmaz.

```kotlin
// Files.kt — bir dizindeki dosya adlarindan anahtar ve yol eslemi cikarir
fun indexDirectory(directory: String, files: List<String>): Map<String, String> =
    files.associate { name -> "dosya.${name.substringBeforeLast('.')}" to "$directory/$name" }
```

Bir ekran üç dosya ister: profil fotoğrafı, fatura ve galeriden seçilmiş bir resim.

```kotlin
// Gallery.kt — dosya ekraninin istedigi uc dosya
fun readFileScreen(chain: List<Tier>): List<Read> = listOf(
    Read("profil", "dosya.profil", chain, "yok"),
    Read("fatura", "dosya.fatura", chain, "yok"),
    Read("galeri", "dosya.galeri", chain, "yok"),
)
```

#### Dizindeki Dosya Diskten Gelir

Uygulamanın dizininde iki dosya var: `profil.jpg` ve `fatura.pdf`. Galeri resmi uygulamaya kopyalanmadı.

```kotlin
// Main.kt — uygulamanin kendi dizinindeki dosya diskten okunur
fun main() {
    val privateDir = Tier("disk", indexDirectory("ozel", listOf("profil.jpg", "fatura.pdf")))
    val reads = readFileScreen(listOf(privateDir))
    reads.forEach { it.show() }
    countOrigins(reads)
}
```

```text
profil: anahtar=dosya.profil | deger=ozel/profil.jpg -> disk
fatura: anahtar=dosya.fatura | deger=ozel/fatura.pdf -> disk
galeri: anahtar=dosya.galeri | deger=yok -> varsayilan
bellek: 0
disk: 2
ag: 0
varsayilan: 1
```

İki dosya diskten, galeri resmi yedek değerden geldi. Bu dersin kökeni disktir: uygulamanın dizinindeki bir dosya, disk basamağı tarafından yanıtlanır.

**VK1.** Model, dizini bir ad listesinden türetir ve dosyanın içeriğini taşımaz; değer yalnız yoldur. Platformda dosyanın boyutu, yazılırken yarıda kesilmesi ve disk dolulukla başarısız olması modelde yoktur.

#### Medya Deposu ve Kapsamlı Depolama

Galeri resmi uygulamanın dizininde değil, kullanıcının ortak medyasındadır. Platform bu medyaya kapsamlı depolama (scoped storage) kuralıyla erişimi sınırlar: uygulama başka uygulamaların dosyalarını doğrudan yoluyla açamaz. Ortak medyaya, medya deposu (media store) sorgusu üzerinden, kullanıcının verdiği izinle ya da kullanıcının seçtiği dosyayla ulaşır.

```kotlin
// Platform kodu: GalleryQuery
fun queryImageNames(context: Context): List<String> {
    val names = mutableListOf<String>()
    val projection = arrayOf(MediaStore.Images.Media.DISPLAY_NAME)
    context.contentResolver.query(
        MediaStore.Images.Media.EXTERNAL_CONTENT_URI, projection, null, null, null,
    )?.use { cursor ->
        while (cursor.moveToNext()) names.add(cursor.getString(0))
    }
    return names
}
```

Sorgu, uygulamanın görmesine izin verilen resimleri döndürür. İzin yoksa liste boş gelir ya da sorgu hata verir; uygulamanın izin vermediği bir dosyayı yolu ile açma yolu yoktur. Bu parça da platform kodudur ve çıktı iddia etmiyoruz. Model, platformun bu kuralını kurar; model platformun kendisi değildir.

Modelde izin kuralı bir basamak değildir. Medya deposundaki dosya, zincire eklenmemiş bir `Tier` olarak durabilir ve zincirde olmayan basamak okumayı yanıtlamaz.

#### Sınırlayıcı Ölçüm

İki şeyi ölçeceğiz. İlki küçük resim: profil fotoğrafı küçültülüp belleğe alınmışsa okuma diske inmeden yanıtlanır. İkincisi galeri resmi: dosya medya deposunda var, ama okuma zincirinde yok.

Küçük resmi bellekte tutan işlev, bir dosyanın diskteki yolundan bellek değeri üretir.

```kotlin
// Thumbnails.kt — diskten okunan dosyanin kucuk resmini bellege alir
fun decodeThumbnails(source: Tier, keys: List<String>): Map<String, String> =
    keys.mapNotNull { key -> source.lookup(key)?.let { key to "kucuk:$it" } }.toMap()
```

```kotlin
// Main.kt — kucuk resim bellekte, galeri resmi zincirin disinda
fun main() {
    val privateDir = Tier("disk", indexDirectory("ozel", listOf("profil.jpg", "fatura.pdf")))
    val mediaStore = Tier("disk", indexDirectory("medya", listOf("galeri.png")))
    val memory = Tier("bellek", decodeThumbnails(privateDir, listOf("dosya.profil")))
    val chain = listOf(memory, privateDir)
    println("medya deposunda: ${mediaStore.lookup("dosya.galeri")}")
    val reads = readFileScreen(chain)
    reads.forEach { it.show() }
    countOrigins(reads)
}
```

```text
medya deposunda: medya/galeri.png
profil: anahtar=dosya.profil | deger=kucuk:ozel/profil.jpg -> bellek
fatura: anahtar=dosya.fatura | deger=ozel/fatura.pdf -> disk
galeri: anahtar=dosya.galeri | deger=yok -> varsayilan
bellek: 1
disk: 1
ag: 0
varsayilan: 1
```

Profil resmi belleğe alındığı için artık bellek kökenli ve değeri küçük resmin adını taşıyor. Fatura hâlâ diskten geliyor. Galeri resmi medya deposunda bulunuyor, yani dosya gerçekten var; ama okuma zincirinde olmadığı için yanıt varsayılan. Kural dosyanın var olup olmadığına değil, zincirin içeriğine bakar.

Galeri resmi uygulamanın kendi dizinine kopyalanırsa durum değişir. Dosya artık uygulamaya aittir ve zincirdeki disk basamağında durur.

```kotlin
// Main.kt — galeri resmi uygulamanin dizinine kopyalaninca zincire girer
fun main() {
    val files = listOf("profil.jpg", "fatura.pdf", "galeri.png")
    val privateDir = Tier("disk", indexDirectory("ozel", files))
    val reads = readFileScreen(listOf(privateDir))
    reads.forEach { it.show() }
    countOrigins(reads)
}
```

```text
profil: anahtar=dosya.profil | deger=ozel/profil.jpg -> disk
fatura: anahtar=dosya.fatura | deger=ozel/fatura.pdf -> disk
galeri: anahtar=dosya.galeri | deger=ozel/galeri.png -> disk
bellek: 0
disk: 3
ag: 0
varsayilan: 0
```

Üç okumanın üçü diskten geldi. Aynı dosya medya deposunda yanıtsızdı, uygulamanın dizininde yanıt veriyor; fark dosyanın içeriğinde değil, zincirdeki yerindedir.

Platformdaki karşılığı şudur: uygulama başka uygulamanın dosyasını yolunu bilse de doğrudan okuyamaz. Dosyaya erişimi izin kuralı belirler ve izin verilmemişse okuma, dosya diskte dursa da yanıtsız kalır. Modelde bu, dosyanın zincire hiç eklenmemesiyle gösterilir. Bu ölçümün söylemediği şey, iznin neden verilmediği ya da kullanıcının izni neden reddettiğidir.

**VK2.** Model, izni bir kural olarak değil, zincirin üyeliği olarak kurar. Platformda izin isteği, kullanıcının sonradan izni geri alması ve kullanıcının seçtiği tek dosya için geçici erişim verilmesi modelde yoktur.

#### Özet

- Bu dersin kökeni: disk. Uygulamanın kendi dizinindeki bir dosyanın okuması disk basamağından yanıtlanır.
- Uygulamaya özel dizin izin istemez ve yalnız uygulamaya aittir; ortak medyaya medya deposu sorgusu ve izinle ulaşılır.
- Dizinde iki dosya varken ve galeri resmi uygulamaya kopyalanmadığında okumalar disk 2, varsayılan 1 çıktı.
- Küçük resim belleğe alınınca profil okuması bellek olur: bellek 1, disk 1, varsayılan 1.
- Zincirde olmayan dosya, medya deposunda var olsa da varsayılana düşer; kural dosyanın varlığını değil, zincirin içeriğini görür.
- Galeri resmi uygulamanın dizinine kopyalanınca aynı okuma disk olur: disk 3.

#### Sonraki Adım

Bu derste dosya aynı aygıtın diskindeydi. Verinin çoğu başka bir makinededir ve ona bir istekle ulaşılır. Sıradaki ders, `Ağ İstemcisi`, bir isteğin yanıtının çözülüp ağ basamağını nasıl kurduğunu ve çözülemeyen bir alanın neye düştüğünü ölçer.

### Ağ İstemcisi

Source: https://academia.sh/tr/kurslar/android-mimari/ag-istemcisi

Ağ istemcisi bir isteği tanımlar, ara katmanlardan geçirir ve gelen yanıtı dizileştirmeyle çözer; çözülen alanlar okumanın ağ basamağını kurar. Ders çözülmüş yanıtın okumasının kökenini, önbelleğe alınmış yanıtın ve çözülemeyen alanın neye düştüğünü ölçer.

Önceki derste dosya aynı aygıtın diskindeydi. Verinin çoğu ise başka bir makinededir ve ona bir istekle ulaşılır. Bu derste o isteğin yanıtının zincire nasıl girdiğine bakıyoruz: yanıt bir metin olarak gelir, çözülür ve ancak çözüldüğü kadarıyla okuma için bir yanıt olur. Dersin sorusu önceki derslerle aynıdır: **bu okumanın yanıtı nereden geliyor?**

```kotlin
// Origin.kt — bir okumanin yanitinin hangi basamaktan geldigini siniflandirir
class Tier(val origin: String, private val entries: Map<String, String>) {
    init {
        require(origin in listOf("bellek", "disk", "ag")) { "gecersiz basamak: $origin" }
    }

    fun lookup(key: String): String? = entries[key]
}

class Read(val label: String, val key: String, chain: List<Tier>, fallback: String) {
    private val hit: Tier? = chain.firstOrNull { it.lookup(key) != null }
    val value: String = hit?.lookup(key) ?: fallback
    val origin: String = hit?.origin ?: "varsayilan"

    fun show() {
        println("$label: anahtar=$key | deger=$value -> $origin")
    }
}

fun countOrigins(reads: List<Read>) {
    for (name in listOf("bellek", "disk", "ag", "varsayilan")) {
        println("$name: ${reads.count { it.origin == name }}")
    }
}
```

#### İstek, Ara Katman, Dizileştirme

Ağ istemcisi (network client) üç iş yapar. İstek tanımı, hangi adrese hangi parametrelerle gidileceğini bir arayüz olarak yazar. Ara katman (interceptor), isteğin ve yanıtın arasına girip başlık eklemek, günlüğe yazmak ya da yanıtı saklamak gibi işleri yapar. Dizileştirme (serialization), yanıt gövdesindeki metni uygulamanın sınıflarına çevirir.

```kotlin
// Platform kodu: ProductApi
interface ProductApi {
    @GET("urun")
    suspend fun product(@Query("id") id: String, @Query("dil") lang: String): ProductDto
}

@Serializable
data class ProductDto(
    val fiyat: String,
    val stok: Int = 0,
    val kampanya: String? = null,
)
```

İstek tanımı arayüzde yazılıdır; `@GET` adresi, `@Query` parametreleri verir. Yanıt gövdesi `ProductDto`'ya çözülür ve gövdede olmayan alan, sınıftaki varsayılan değere düşer. Ara katman da ayrı bir sınıftır.

```kotlin
// Platform kodu: CacheHeaderInterceptor
class CacheHeaderInterceptor : Interceptor {
    override fun intercept(chain: Interceptor.Chain): Response {
        val response = chain.proceed(chain.request())
        return response.newBuilder()
            .header("Cache-Control", "max-age=60")
            .build()
    }
}
```

Bu iki parça platform kodudur ve burada çıktı, süre ya da yanıt kodu iddia etmiyoruz. Model, platformun bu kurallarını kurar; model platformun kendisi değildir.

Modelde istek, bir adres ve parametrelerden kurulan tek bir satırdır. Yanıt ise `anahtar=değer` çiftlerinin noktalı virgülle ayrıldığı bir gövdedir; çözücü bu gövdeden bir eşlem çıkarır. `=` işareti taşımayan parça çözülemez ve eşleme girmez.

```kotlin
// Wire.kt — istegi tek satira kurar, yanit govdesini esleme cozer
fun requestLine(path: String, query: Map<String, String>): String =
    "GET $path?" + query.toSortedMap().entries.joinToString("&") { "${it.key}=${it.value}" }

fun decodeResponse(body: String): Map<String, String> =
    body.split(";")
        .filter { "=" in it }
        .associate { part -> part.substringBefore("=") to part.substringAfter("=") }
```

Ekran üç değer okur. Her birinin yedek değeri vardır.

```kotlin
// Product.kt — urun ekraninin okudugu uc deger
fun readProduct(chain: List<Tier>): List<Read> = listOf(
    Read("fiyat", "urun.fiyat", chain, "bilinmiyor"),
    Read("stok", "urun.stok", chain, "0"),
    Read("kampanya", "kampanya.kod", chain, "yok"),
)
```

#### Çözülen Yanıt Ağdır

Sunucu üç alanlı bir gövde döndürür. Çözücü gövdeyi eşleme çevirir ve bu eşlem ağ basamağıdır.

```kotlin
// Main.kt — cozulen yanit agin eslemini kurar
fun main() {
    println(requestLine("/urun", mapOf("id" to "7", "dil" to "tr")))
    val body = "urun.fiyat=125;urun.stok=8;kampanya.kod=YAZ10"
    val network = Tier("ag", decodeResponse(body))
    val reads = readProduct(listOf(network))
    reads.forEach { it.show() }
    countOrigins(reads)
}
```

```text
GET /urun?dil=tr&id=7
fiyat: anahtar=urun.fiyat | deger=125 -> ag
stok: anahtar=urun.stok | deger=8 -> ag
kampanya: anahtar=kampanya.kod | deger=YAZ10 -> ag
bellek: 0
disk: 0
ag: 3
varsayilan: 0
```

Üç alanın üçü de ağdan geldi. Bu dersin kökeni ağdır: çözülen yanıtın alanları ağ basamağını kurar. Ağ basamağı, istek gidip yanıt çözüldükten sonra oluşan bir eşlemdir; çözülmemiş bir metin okumaya yanıt veremez.

**VK1.** Model, isteğin her zaman başarılı olduğunu ve yanıt gövdesinin tamamının geldiğini varsayar. Platformda bağlantının kopması, zaman aşımı ve sunucunun hata kodu dönmesi modelde yoktur; modelde ağ basamağı ya bir eşlemdir ya hiç kurulmaz.

#### Sınırlayıcı Ölçüm

İki şey ağ kökenini bozar. İlki ara katmanın yanıtı saklamasıdır: önceki isteğin yanıtı diskteki bir önbellekte kalmışsa, sonraki okuma o önbelleğe bakar. İkincisi çözülemeyen alandır: gövdede olan ama çözücünün eşleme alamadığı bir parça okumaya yanıt vermez.

Önce önbellek. Önbellek, bir önceki isteğin gövdesinden çözülmüş bir disk basamağıdır ve zincirde ağdan önce gelir. Önbellekteki gövde eskidir: fiyat 120, kampanya yok.

```kotlin
// Main.kt — onbellege alinmis yanit diskten, onbellekte olmayan alan agdan gelir
fun main() {
    val cachedBody = "urun.fiyat=120;urun.stok=8"
    val freshBody = "urun.fiyat=125;urun.stok=8;kampanya.kod=YAZ10"
    val cache = Tier("disk", decodeResponse(cachedBody))
    val network = Tier("ag", decodeResponse(freshBody))
    val reads = readProduct(listOf(cache, network))
    reads.forEach { it.show() }
    countOrigins(reads)
}
```

```text
fiyat: anahtar=urun.fiyat | deger=120 -> disk
stok: anahtar=urun.stok | deger=8 -> disk
kampanya: anahtar=kampanya.kod | deger=YAZ10 -> ag
bellek: 0
disk: 2
ag: 1
varsayilan: 0
```

Fiyat ve stok önbellekten, yani diskten okundu. Fiyat 120'dir; sunucudaki güncel değer 125 olsa da okuma ona ulaşmadı. Kampanya önbellekte yoktu ve ağdan geldi. Kural sırayı görür, tazeliği görmez.

Şimdi çözülemeyen alan. Gövdede `urun.stok` parçası var, ama `=` işareti ve değeri yok.

```kotlin
// Main.kt — cozulemeyen alan agin eslemine girmez ve varsayilana duser
fun main() {
    val body = "urun.fiyat=125;urun.stok;kampanya.kod=YAZ10"
    val network = Tier("ag", decodeResponse(body))
    val reads = readProduct(listOf(network))
    reads.forEach { it.show() }
    countOrigins(reads)
}
```

```text
fiyat: anahtar=urun.fiyat | deger=125 -> ag
stok: anahtar=urun.stok | deger=0 -> varsayilan
kampanya: anahtar=kampanya.kod | deger=YAZ10 -> ag
bellek: 0
disk: 0
ag: 2
varsayilan: 1
```

Sunucu stok alanını gönderdi, ama çözücü onu eşleme alamadı. Okuma için o alan yok sayılır ve yanıt yedek değer olan 0'dır. Platformda dizileştirmedeki varsayılan değer kuralı bunun yalnız bir yarısını karşılar: alan gövdede yoksa sınıftaki varsayılan değer kullanılır. Gövdede olup çözülemeyen değer ise varsayılan ayarda hata verir; varsayılana düşmesi için ayrıca açılan bir ayar gerekir.

Ölçümün söylemediği şey, gövdenin neden bozuk olduğudur ve 0'ın stok için doğru bir değer olup olmadığıdır. Kural yalnız yanıtın hangi basamaktan geldiğini söyler.

**VK2.** Model, çözücüyü yalnız `anahtar=değer` biçimi için kurar. Platformda iç içe nesneler, listeler, tür uyuşmazlığı ve bilinmeyen alanların yok sayılması modelde yoktur.

#### Özet

- Bu dersin kökeni: ağ. Çözülen yanıtın alanları ağ basamağını kurar ve okuma o basamaktan yanıtlanır.
- Ağ istemcisi isteği tanımlar, ara katmanlardan geçirir ve yanıtı dizileştirmeyle çözer.
- Üç alanlı bir yanıt çözülünce üç okumanın üçü ağdır: ağ 3.
- Önbelleğe alınmış yanıt zincirde ağdan önce gelirse okuma diskten yanıtlanır: disk 2, ağ 1. Fiyat 120 kalır, sunucudaki 125'i kural görmez.
- Çözülemeyen alan ağ eşlemine girmez; o okuma varsayılana düşer: ağ 2, varsayılan 1.

#### Sonraki Adım

Bu derste önbellek, eski bir yanıtın çözülmüş hâli olarak yalnız bir basamaktı. Yanıtı kalıcı olarak diske yazıp ekranın yalnız diski okumasını sağlamak ayrı bir tasarımdır: çevrimdışı çalışmanın temeli. Sıradaki ders, `Önbellekleme ve Çevrimdışı Destek`, ağdan diske yazan bir yenilemenin okumayı hangi kökene getirdiğini ve ağ düşünce kökenin neden aynı kalıp değerin eskidiğini ölçer.

### Önbellekleme ve Çevrimdışı Destek

Source: https://academia.sh/tr/kurslar/android-mimari/onbellekleme-ve-cevrimdisi-destek

Tek doğruluk kaynağı kalıbında arayüz yalnız yerel veritabanını okur, ağ yalnız veritabanını yeniler. Ders yenileme sonrası okumanın kökenini, ağ düşünce kökenin aynı kalıp değerin eskidiğini ve diskte hiç olmayan anahtarın neye düştüğünü ölçer.

Önceki derste ağdan gelen yanıt çözüldü ve zincirde bir basamak oldu; önbellek de eski bir yanıtın çözülmüş hâliydi. Bu derste yanıtın zincirde durmasını değil, kalıcı olarak diske yazılmasını inceliyoruz. Ağ yanıtı diske yazılırsa ekranın ağa hiç bakmasına gerek kalmaz ve ağ düştüğünde de ekran çalışmaya devam eder. Dersin sorusu önceki derslerle aynıdır: **bu okumanın yanıtı nereden geliyor?**

```kotlin
// Origin.kt — bir okumanin yanitinin hangi basamaktan geldigini siniflandirir
class Tier(val origin: String, private val entries: Map<String, String>) {
    init {
        require(origin in listOf("bellek", "disk", "ag")) { "gecersiz basamak: $origin" }
    }

    fun lookup(key: String): String? = entries[key]
}

class Read(val label: String, val key: String, chain: List<Tier>, fallback: String) {
    private val hit: Tier? = chain.firstOrNull { it.lookup(key) != null }
    val value: String = hit?.lookup(key) ?: fallback
    val origin: String = hit?.origin ?: "varsayilan"

    fun show() {
        println("$label: anahtar=$key | deger=$value -> $origin")
    }
}

fun countOrigins(reads: List<Read>) {
    for (name in listOf("bellek", "disk", "ag", "varsayilan")) {
        println("$name: ${reads.count { it.origin == name }}")
    }
}
```

#### Tek Doğruluk Kaynağı

Tek doğruluk kaynağı (single source of truth) kalıbı iki kural koyar. Arayüz yalnız yerel veritabanını okur; ağa hiç doğrudan bakmaz. Ağ yalnız veritabanını yeniler; arayüze doğrudan veri vermez. Veri bir yönde akar: ağdan veritabanına, veritabanından arayüze. Böylece ekran tek bir yerden beslenir ve ağ olmasa da o yer çalışır.

```kotlin
// Platform kodu: ProductRepository
class ProductRepository(private val dao: ProductDao, private val api: ProductApi) {
    fun products(): Flow<List<ProductEntity>> = dao.observeAll()

    suspend fun refresh() {
        try {
            dao.upsertAll(api.products())
        } catch (e: IOException) {
            // cevrimdisiysa eski kayitlar kalir
        }
    }
}
```

`products` yalnız veritabanını izler. `refresh` ağdan alıp veritabanına yazar ve ağ yoksa hiçbir şey yazmaz; veritabanı değişince akış yeni listeyi arayüze iletir. Bu parça platform kodudur ve burada çıktı, süre ya da kayıt sayısı iddia etmiyoruz. Model, platformun bu kuralını kurar; model platformun kendisi değildir.

Modelde yenileme, ağdan istenen anahtarları eski eşleme ekleyen bir işlevdir. Ağ yoksa eşlem değişmez. Okumanın zincirinde yalnız disk vardır; ağ hiçbir okumanın zincirine girmez.

```kotlin
// Refresh.kt — aga ulasilirsa istenen anahtarlari diske yazar, ulasilmazsa diski olduğu gibi birakir
fun refreshFrom(disk: Map<String, String>, network: Tier?, keys: List<String>): Map<String, String> {
    if (network == null) return disk
    return disk + keys.mapNotNull { key -> network.lookup(key)?.let { key to it } }
}
```

Ekran üç değer okur ve yalnız diske bakar.

```kotlin
// Screen.kt — arayuz yalniz diski okur
fun readProductScreen(disk: Map<String, String>): List<Read> {
    val chain = listOf(Tier("disk", disk))
    return listOf(
        Read("fiyat", "urun.fiyat", chain, "bilinmiyor"),
        Read("stok", "urun.stok", chain, "0"),
        Read("kampanya", "kampanya.kod", chain, "yok"),
    )
}
```

#### Yenileme Diske Yazar

Diskte önceki oturumdan kalan fiyat ve stok var. Sunucu fiyatı güncellemiş. Yenilemeden önce ve sonra ekranın okuması yan yana.

```kotlin
// Main.kt — yenileme ag degerini diske yazar, okuma yine diskten gelir
fun main() {
    val disk = mapOf("urun.fiyat" to "120", "urun.stok" to "9")
    val network = Tier("ag", mapOf("urun.fiyat" to "125", "urun.stok" to "8"))
    println("yenileme oncesi")
    readProductScreen(disk).forEach { it.show() }
    val refreshed = refreshFrom(disk, network, listOf("urun.fiyat", "urun.stok", "kampanya.kod"))
    println("yenileme sonrasi")
    val reads = readProductScreen(refreshed)
    reads.forEach { it.show() }
    countOrigins(reads)
}
```

```text
yenileme oncesi
fiyat: anahtar=urun.fiyat | deger=120 -> disk
stok: anahtar=urun.stok | deger=9 -> disk
kampanya: anahtar=kampanya.kod | deger=yok -> varsayilan
yenileme sonrasi
fiyat: anahtar=urun.fiyat | deger=125 -> disk
stok: anahtar=urun.stok | deger=8 -> disk
kampanya: anahtar=kampanya.kod | deger=yok -> varsayilan
bellek: 0
disk: 2
ag: 0
varsayilan: 1
```

Yenilemeden sonra fiyat 125, stok 8 oldu ve köken değişmedi: hâlâ disk. Ağ bir okumanın kökeni olmadı; ağ yalnız diskin içeriğini değiştirdi. Bu dersin kökeni disktir: ekran her durumda diskten okur ve ağın etkisi yalnızca diskin değerinde görünür. Sunucu kampanya kodunu bilmediği için o anahtar diske yazılmadı ve okuma varsayılanda kaldı.

**VK1.** Model, yenilemeyi tek adımda ve bütünüyle başarılı varsayar: istenen anahtarlar ya hep birlikte yazılır ya hiç yazılmaz. Platformda yenilemenin yarıda kesilmesi, iki yenilemenin üst üste binmesi ve yazma sırasında veritabanı işleminin başarısız olması modelde yoktur.

#### Ağ Düşünce

Ağ yoksa `refresh` hiçbir şey yazmaz. Eski kayıtlar kalır ve ekran onları göstermeye devam eder.

```kotlin
// Main.kt — ag dusunce okuma yine diskten gelir ama deger eskidir
fun main() {
    val disk = mapOf("urun.fiyat" to "120", "urun.stok" to "9")
    val refreshed = refreshFrom(disk, null, listOf("urun.fiyat", "urun.stok", "kampanya.kod"))
    val reads = readProductScreen(refreshed)
    reads.forEach { it.show() }
    countOrigins(reads)
}
```

```text
fiyat: anahtar=urun.fiyat | deger=120 -> disk
stok: anahtar=urun.stok | deger=9 -> disk
kampanya: anahtar=kampanya.kod | deger=yok -> varsayilan
bellek: 0
disk: 2
ag: 0
varsayilan: 1
```

Okumanın kökeni değişmedi: fiyat ve stok disk, sayılar da yenilemeden önceki ölçümle aynı. Ağ yoksa değişen şey değerdir: fiyat 120 kaldı, sunucuda ise 125. Ekran çevrimdışı çalışır ve gösterdiği değer eskidir.

Bu ölçüm M30/K04'teki bayatlık ölçüsünden farklıdır. O kursta bir ekranın gösterdiği değerin modelin değerinden sapması yargılanıyordu: ekran ya güncel ya bayattı. Bu derste tazelik yargılanmaz; kural yalnız okumanın hangi basamaktan geldiğini söyler. İki ölçümde de fiyat 120'dir ve köken disktir; değerin eski olduğunu kural değil, yan yana koyduğumuz sunucu değeri söyler.

#### Sınırlayıcı Ölçüm

Diskte hiç olmayan bir anahtar ne olur? Uygulama ilk kez çalışıyor, disk boş ve ağ yok. Bu, ağın düştüğü ölçümün en uç hâlidir: ekranın gösterebileceği eski bir değer bile yoktur.

```kotlin
// Main.kt — bos diskte ve agsiz her okuma varsayilana duser, ag gelince diske yazilir
fun main() {
    val network = Tier("ag", mapOf("urun.fiyat" to "125", "urun.stok" to "8", "kampanya.kod" to "YAZ10"))
    val keys = listOf("urun.fiyat", "urun.stok", "kampanya.kod")
    val states = listOf("agsiz" to null, "agli" to network)
    for ((name, source) in states) {
        println("ilk acilis: $name")
        val reads = readProductScreen(refreshFrom(emptyMap(), source, keys))
        reads.forEach { it.show() }
        countOrigins(reads)
    }
}
```

```text
ilk acilis: agsiz
fiyat: anahtar=urun.fiyat | deger=bilinmiyor -> varsayilan
stok: anahtar=urun.stok | deger=0 -> varsayilan
kampanya: anahtar=kampanya.kod | deger=yok -> varsayilan
bellek: 0
disk: 0
ag: 0
varsayilan: 3
ilk acilis: agli
fiyat: anahtar=urun.fiyat | deger=125 -> disk
stok: anahtar=urun.stok | deger=8 -> disk
kampanya: anahtar=kampanya.kod | deger=YAZ10 -> disk
bellek: 0
disk: 3
ag: 0
varsayilan: 0
```

Ağsız ilk açılışta üç okumanın üçü varsayılan. Diskte olmayan anahtar için ağın düşmesi bir değer ortaya çıkarmaz; ekran yedek değerlerle açılır. Ağ geldiğinde yenileme üç anahtarı da diske yazar ve aynı okumalar bu kez diskten gelir. Tek doğruluk kaynağında ağ, okumanın kökeni olmaz; yalnız diskin dolmasını sağlar.

Ölçümün söylemediği şey, hangi değerin doğru olduğudur ve ağsız ilk açılışta ekranın nasıl görünmesi gerektiğidir. İkincisi bir tasarım kararıdır ve kural onu vermez.

**VK2.** Model, ağın ya hiç erişilemediğini ya da tüm istenen anahtarları verdiğini varsayar. Platformda kısmi yanıt, yavaş bağlantı ve ağın geri gelmesine kadar bekleyen yenilemenin ne zaman yeniden denendiği modelde yoktur.

#### Özet

- Bu dersin kökeni: disk. Tek doğruluk kaynağında arayüz yalnız veritabanını okur ve ağ okumanın zincirine girmez.
- Yenileme ağdan diske yazar; yenileme sonrası fiyat 125 ve stok 8 olsa da köken disk kalır: disk 2, varsayılan 1.
- Ağ düşünce yenileme bir şey yazmaz; okuma yine disktir, ama fiyat 120 kalır ve eskidir.
- Tazelik burada yargılanmaz; kural değerin güncel olup olmadığını değil, hangi basamaktan geldiğini söyler.
- Diskte hiç olmayan anahtar ağsız ilk açılışta varsayılandır: varsayılan 3; ağ gelince disk 3 olur.

#### Sonraki Adım

Bu derste ekran tek bir ürünü okudu ve disk onu tek seferde tutabildi. Liste uzadığında hepsini birden getirmek ne ağa ne belleğe uyar; veri parça parça yüklenir. Sıradaki ders, `Sayfalı Veri Yükleme`, henüz yüklenmemiş bir sayfadaki ögenin hangi kökenden okunduğunu ve sayfa yüklenince kökenin nasıl değiştiğini ölçer.

### Sayfalı Veri Yükleme

Source: https://academia.sh/tr/kurslar/android-mimari/sayfali-veri-yukleme

Uzun bir liste tek seferde değil, sayfa sayfa yüklenir; her sayfa bir sayfa anahtarıyla istenir. Ders henüz yüklenmemiş sayfadaki ögenin okumasının kökenini, sayfa yüklenince kökenin değişimini ve aynı ögenin ağdan ya da diskten gelmesini ölçer.

Önceki derste ekran tek bir ürünü okudu ve disk onu tek seferde tutabildi. Bir ürün listesi binlerce satır olabilir ve hepsini birden istemek hem ağa hem belleğe fazla gelir. Liste bu yüzden sayfa sayfa yüklenir: kullanıcı aşağı indikçe sıradaki sayfa istenir. Bu derste o ana bakıyoruz, yani listenin henüz yüklenmemiş kısmına. Dersin sorusu önceki derslerle aynıdır: **bu okumanın yanıtı nereden geliyor?**

```kotlin
// Origin.kt — bir okumanin yanitinin hangi basamaktan geldigini siniflandirir
class Tier(val origin: String, private val entries: Map<String, String>) {
    init {
        require(origin in listOf("bellek", "disk", "ag")) { "gecersiz basamak: $origin" }
    }

    fun lookup(key: String): String? = entries[key]
}

class Read(val label: String, val key: String, chain: List<Tier>, fallback: String) {
    private val hit: Tier? = chain.firstOrNull { it.lookup(key) != null }
    val value: String = hit?.lookup(key) ?: fallback
    val origin: String = hit?.origin ?: "varsayilan"

    fun show() {
        println("$label: anahtar=$key | deger=$value -> $origin")
    }
}

fun countOrigins(reads: List<Read>) {
    for (name in listOf("bellek", "disk", "ag", "varsayilan")) {
        println("$name: ${reads.count { it.origin == name }}")
    }
}
```

#### Sayfa Sayfa Yükleme

Sayfalı yüklemede iki rol vardır. Sayfa kaynağı (paging source), bir sayfa anahtarı verildiğinde o sayfanın ögelerini getirir ve bir sonraki sayfanın anahtarını söyler. Sayfalayıcı (pager), kullanıcı listede ilerledikçe sayfa kaynağını çağırır ve gelen sayfaları arayüze akış olarak iletir. Sayfa anahtarı bir sayfa numarası, bir imleç ya da son ögenin kimliği olabilir.

```kotlin
// Platform kodu: ProductPagingSource
class ProductPagingSource(private val api: ProductApi) : PagingSource<Int, ProductDto>() {
    override suspend fun load(params: LoadParams<Int>): LoadResult<Int, ProductDto> {
        val page = params.key ?: 0
        val items = api.products(page = page, size = params.loadSize)
        return LoadResult.Page(
            data = items,
            prevKey = if (page == 0) null else page - 1,
            nextKey = if (items.isEmpty()) null else page + 1,
        )
    }
}
```

`load`, anahtarı verilen sayfayı ister; liste boş dönerse sonraki anahtar `null` olur ve yükleme biter. Bu parça platform kodudur ve burada çıktı, sayfa boyutu ya da süre iddia etmiyoruz. Model, platformun bu kuralını kurar; model platformun kendisi değildir.

```kotlin
// Platform kodu: ProductPager
val products: Flow<PagingData<ProductDto>> = Pager(
    config = PagingConfig(pageSize = 20),
    pagingSourceFactory = { ProductPagingSource(api) },
).flow
```

Sayfalayıcı, sayfa kaynağını bir yapılandırmayla birlikte kurar ve sonucu akış olarak verir; arayüz bu akışı toplar. Bu parça da platform kodudur ve çıktı iddia etmiyoruz. Model, platformun bu kuralını kurar; model platformun kendisi değildir.

Modelde bir sayfa, kaynaktan alınmış ardışık anahtarların eşlemidir. Sayfa `n` ve boyut `s` verildiğinde `n*s+1` ile `(n+1)*s` arasındaki ögeler yüklenir. Kaynakta olmayan öge eşleme girmez.

```kotlin
// Pages.kt — bir sayfanin ogelerini kaynaktan alip esleme cevirir
fun loadPage(source: Tier, page: Int, size: Int): Map<String, String> =
    ((page * size + 1)..((page + 1) * size)).mapNotNull { n ->
        source.lookup("urun.$n")?.let { "urun.$n" to it }
    }.toMap()
```

Ekran sıralı ögeleri okur. Her ögenin yedek değeri aynı yer tutucudur.

```kotlin
// List.kt — liste ekraninin okudugu ilk n oge
fun readList(chain: List<Tier>, count: Int): List<Read> =
    (1..count).map { n -> Read("oge$n", "urun.$n", chain, "yukleniyor") }
```

#### Yüklenmemiş Sayfa Varsayılandır

Sunucuda dokuz ürün var ve sayfa boyutu üç. Yalnız ilk sayfa ağdan yüklendi; ekran ise altı satırlık bir liste çiziyor.

```kotlin
// Main.kt — yuklenmemis sayfadaki oge yer tutucudan okunur
fun main() {
    val names = listOf("Kalem", "Defter", "Silgi", "Cetvel", "Makas", "Bant", "Zimba", "Mum", "Toka")
    val server = Tier("ag", names.withIndex().associate { (i, name) -> "urun.${i + 1}" to name })
    val loaded = Tier("ag", loadPage(server, 0, 3))
    val reads = readList(listOf(loaded), 6)
    reads.forEach { it.show() }
    countOrigins(reads)
}
```

```text
oge1: anahtar=urun.1 | deger=Kalem -> ag
oge2: anahtar=urun.2 | deger=Defter -> ag
oge3: anahtar=urun.3 | deger=Silgi -> ag
oge4: anahtar=urun.4 | deger=yukleniyor -> varsayilan
oge5: anahtar=urun.5 | deger=yukleniyor -> varsayilan
oge6: anahtar=urun.6 | deger=yukleniyor -> varsayilan
bellek: 0
disk: 0
ag: 3
varsayilan: 3
```

Yüklenen sayfadaki üç öge ağdan, yüklenmeyen sayfadaki üç öge yer tutucudan geldi. Bu dersin kökeni varsayılandır: sunucuda `urun.4` olsa bile sayfası yüklenmemiş bir ögeyi hiçbir basamak taşımaz. Kural ögenin sunucuda var olup olmadığına değil, zincirin içeriğine bakar.

**VK1.** Model, sayfayı tek adımda ve bütünüyle yüklenmiş sayar: bir sayfanın üç ögesi ya hep birlikte gelir ya hiç gelmez. Platformda sayfanın yüklenirken hata vermesi, yeniden denenmesi, listenin ortasına ekleme yapılması ve yer tutucuların arayüzde nasıl çizildiği modelde yoktur.

#### Kullanıcı İndikçe

Kullanıcı listede aşağı indi ve ikinci sayfa da yüklendi. Zincirdeki ağ basamağı artık iki sayfanın ögelerini taşıyor.

```kotlin
// Main.kt — ikinci sayfa yuklenince yer tutucular ag olur
fun main() {
    val names = listOf("Kalem", "Defter", "Silgi", "Cetvel", "Makas", "Bant", "Zimba", "Mum", "Toka")
    val server = Tier("ag", names.withIndex().associate { (i, name) -> "urun.${i + 1}" to name })
    val loaded = Tier("ag", loadPage(server, 0, 3) + loadPage(server, 1, 3))
    val reads = readList(listOf(loaded), 8)
    reads.forEach { it.show() }
    countOrigins(reads)
}
```

```text
oge1: anahtar=urun.1 | deger=Kalem -> ag
oge2: anahtar=urun.2 | deger=Defter -> ag
oge3: anahtar=urun.3 | deger=Silgi -> ag
oge4: anahtar=urun.4 | deger=Cetvel -> ag
oge5: anahtar=urun.5 | deger=Makas -> ag
oge6: anahtar=urun.6 | deger=Bant -> ag
oge7: anahtar=urun.7 | deger=yukleniyor -> varsayilan
oge8: anahtar=urun.8 | deger=yukleniyor -> varsayilan
bellek: 0
disk: 0
ag: 6
varsayilan: 2
```

Dördüncü, beşinci ve altıncı öge artık ağdan geliyor. Yedinci ve sekizinci ögenin sayfası hâlâ yüklenmedi ve yer tutucu olarak kalıyor. Sayfa yüklendikçe varsayılan sayısı düşer ve ağ sayısı artar.

#### Sınırlayıcı Ölçüm

Aynı ögeyi iki basamak taşıyabilir. İlk sayfa önceki oturumdan diske yazılmıştır ve kullanıcı indikçe iki sayfa da ağdan yüklenir. Hangi basamağın önce geldiği, aynı ögenin kökenini belirler.

```kotlin
// Main.kt — ayni oge diskte de agda da varsa zincirdeki sira kokeni belirler
fun main() {
    val names = listOf("Kalem", "Defter", "Silgi", "Cetvel", "Makas", "Bant", "Zimba", "Mum", "Toka")
    val server = Tier("ag", names.withIndex().associate { (i, name) -> "urun.${i + 1}" to name })
    val cached = Tier("disk", loadPage(server, 0, 3))
    val fetched = Tier("ag", loadPage(server, 0, 3) + loadPage(server, 1, 3))
    val chains = listOf("diskten once" to listOf(cached, fetched), "agdan once" to listOf(fetched, cached))
    for ((name, chain) in chains) {
        println("zincir: $name")
        val reads = readList(chain, 6)
        reads.forEach { it.show() }
        countOrigins(reads)
    }
}
```

```text
zincir: diskten once
oge1: anahtar=urun.1 | deger=Kalem -> disk
oge2: anahtar=urun.2 | deger=Defter -> disk
oge3: anahtar=urun.3 | deger=Silgi -> disk
oge4: anahtar=urun.4 | deger=Cetvel -> ag
oge5: anahtar=urun.5 | deger=Makas -> ag
oge6: anahtar=urun.6 | deger=Bant -> ag
bellek: 0
disk: 3
ag: 3
varsayilan: 0
zincir: agdan once
oge1: anahtar=urun.1 | deger=Kalem -> ag
oge2: anahtar=urun.2 | deger=Defter -> ag
oge3: anahtar=urun.3 | deger=Silgi -> ag
oge4: anahtar=urun.4 | deger=Cetvel -> ag
oge5: anahtar=urun.5 | deger=Makas -> ag
oge6: anahtar=urun.6 | deger=Bant -> ag
bellek: 0
disk: 0
ag: 6
varsayilan: 0
```

Değerler iki zincirde de aynı; değişen yalnız kökendir. Disk önce gelince ilk sayfanın üç ögesi diskten, ikinci sayfanın üç ögesi ağdan okunur. Ağ önce gelince altı ögenin altısı ağdandır. Sayfa yüklendikten sonra aynı ögenin kökeni, sayfanın hangi basamaktan yüklendiğine ve zincirin sırasına bağlıdır.

Ölçümün söylemediği şey, iki zincirden hangisinin doğru olduğudur ve disktekinin ağdakinden eski olup olmadığıdır. Kural sırayı görür, tazeliği görmez.

**VK2.** Model, iki basamağın aynı sayfa sınırlarını kullandığını varsayar. Platformda ağ ile diskteki sayfa boyutlarının farklı olması, sayfa kaynağının uzak ve yerel kaynağı birleştirmesi ve yenilemenin listeyi baştan yüklemesi modelde yoktur.

#### Özet

- Bu dersin kökeni: varsayılan. Henüz yüklenmemiş bir sayfadaki ögeyi hiçbir basamak taşımaz ve ekran yer tutucuyu gösterir.
- Sayfa kaynağı bir sayfa anahtarıyla sayfayı getirir; sayfalayıcı kullanıcı indikçe kaynağı çağırır ve sayfaları akış olarak iletir.
- Yalnız ilk sayfa yüklenmişken altı satırlık listede ağ 3, varsayılan 3 çıktı.
- İkinci sayfa yüklenince dördüncü ile altıncı öge ağ olur: sekiz satırda ağ 6, varsayılan 2.
- Aynı öge hem diskte hem ağda varsa kökeni zincirin sırası belirler: diskten önce gelirse disk 3, ağ 3; ağ önce gelirse ağ 6.

#### Sonraki Adım

Bu derste sayfalar kullanıcı indikçe, uygulama açıkken yüklendi. Bazı veri ise kullanıcı ekrana bakmazken de yenilenmelidir: ağın uygun olduğu bir anda, şarj sırasında ya da uygulama kapalıyken. Sıradaki ders, `Arka Plan İşleri`, bu işlerin hangi kısıt sağlandığında çalıştığını ve çalıştığında diske yazan bir işin okumayı hangi kökene getirdiğini ölçer.

### Arka Plan İşleri

Source: https://academia.sh/tr/kurslar/android-mimari/arka-plan-isleri

Ertelenebilir bir iş, kısıtları sağlandığında ağdan çekip diske yazar ve ekranın sonraki okuması diskten gelir. Ders kısıt sağlanınca okumanın kökenini, kısıt sağlanmayınca okumanın neye düştüğünü ve kursun on dört dersinin köken dağılımını ölçer.

Önceki derste sayfalar kullanıcı indikçe ve uygulama açıkken yüklendi. Bazı veri ise kullanıcı ekrana bakmazken de yenilenmelidir: ağın uygun olduğu bir anda, şarj sırasında ya da uygulama kapalıyken. Bu derste yenilemeyi ekranın değil, ertelenebilir bir işin yaptığını varsayıyoruz. Dersin sorusu önceki derslerle aynıdır: **bu okumanın yanıtı nereden geliyor?**

```kotlin
// Origin.kt — bir okumanin yanitinin hangi basamaktan geldigini siniflandirir
class Tier(val origin: String, private val entries: Map<String, String>) {
    init {
        require(origin in listOf("bellek", "disk", "ag")) { "gecersiz basamak: $origin" }
    }

    fun lookup(key: String): String? = entries[key]
}

class Read(val label: String, val key: String, chain: List<Tier>, fallback: String) {
    private val hit: Tier? = chain.firstOrNull { it.lookup(key) != null }
    val value: String = hit?.lookup(key) ?: fallback
    val origin: String = hit?.origin ?: "varsayilan"

    fun show() {
        println("$label: anahtar=$key | deger=$value -> $origin")
    }
}

fun countOrigins(reads: List<Read>) {
    for (name in listOf("bellek", "disk", "ag", "varsayilan")) {
        println("$name: ${reads.count { it.origin == name }}")
    }
}
```

#### Ertelenebilir İş ve Kısıt

Arka plan işi (background work), kullanıcının bir ekranda beklemediği ve hemen bitmesi gerekmeyen iştir. Böyle bir iş ertelenebilir (deferrable) olduğu için sistem onu uygun bir ana bırakabilir. İşin ne zaman çalışabileceğini kısıtlar (constraints) söyler: ağa bağlı olmak, şarjda olmak gibi. Kısıtların hepsi sağlanmadıkça iş çalışmaz; sağlandıktan sonra işi ne zaman çalıştıracağına sistem karar verir.

```kotlin
// Platform kodu: RefreshWorker ve istekler
class RefreshWorker(context: Context, params: WorkerParameters) : CoroutineWorker(context, params) {
    override suspend fun doWork(): Result = try {
        repository.refresh()
        Result.success()
    } catch (e: IOException) {
        Result.retry()
    }
}

val constraints = Constraints.Builder()
    .setRequiredNetworkType(NetworkType.CONNECTED)
    .setRequiresCharging(true)
    .build()
val once = OneTimeWorkRequestBuilder<RefreshWorker>().setConstraints(constraints).build()
val periodic = PeriodicWorkRequestBuilder<RefreshWorker>(1, TimeUnit.DAYS).setConstraints(constraints).build()
```

Bu parça platform kodudur ve burada çıktı, süre ya da çalışma sayısı iddia etmiyoruz. `doWork` önceki derslerdeki `refresh` işlevini çağırır; ağ yoksa işi daha sonra yeniden denemesini ister. `Constraints` ağ bağlantısını ve şarjı koşul koyar. İstek tek seferlik ya da dönemsel kurulabilir. Model, platformun bu kuralını kurar; model platformun kendisi değildir.

#### Modelde İş

Modelde iş, bir işlevdir. Gereken kısıtların hepsi sağlanmışsa ağdan istenen anahtarları alıp diske yazar, sağlanmamışsa diski olduğu gibi bırakır. Ekran önceki derste olduğu gibi yalnız diski okur.

```kotlin
// Work.kt — kisitlarin hepsi saglandiysa istenen anahtarlari diske yazar, saglanmadiysa diski birakir
fun runWork(
    required: Set<String>,
    satisfied: Set<String>,
    disk: Map<String, String>,
    network: Tier,
    keys: List<String>,
): Map<String, String> {
    if (!satisfied.containsAll(required)) return disk
    return disk + keys.mapNotNull { key -> network.lookup(key)?.let { key to it } }
}
```

```kotlin
// Screen.kt — ana ekran yalniz diski okur
fun readHomeScreen(disk: Map<String, String>): List<Read> {
    val chain = listOf(Tier("disk", disk))
    return listOf(
        Read("fiyat", "urun.fiyat", chain, "bilinmiyor"),
        Read("stok", "urun.stok", chain, "0"),
        Read("kampanya", "kampanya.kod", chain, "yok"),
    )
}
```

#### Kısıt Sağlanınca Diske Yazar

Disk boş. İş iki kısıt ister: `ag-bagli` ve `sarjda`. İkisi de sağlanıyor ve sunucu üç anahtarı da biliyor.

```kotlin
// Main.kt — kisitlar saglaninca is ag degerlerini diske yazar, sonraki okuma diskten gelir
fun main() {
    val required = setOf("ag-bagli", "sarjda")
    val network = Tier("ag", mapOf("urun.fiyat" to "125", "urun.stok" to "8", "kampanya.kod" to "YAZ10"))
    val keys = listOf("urun.fiyat", "urun.stok", "kampanya.kod")
    println("is calismadan once")
    readHomeScreen(emptyMap()).forEach { it.show() }
    val disk = runWork(required, required, emptyMap(), network, keys)
    println("is calistiktan sonra")
    val reads = readHomeScreen(disk)
    reads.forEach { it.show() }
    countOrigins(reads)
}
```

```text
is calismadan once
fiyat: anahtar=urun.fiyat | deger=bilinmiyor -> varsayilan
stok: anahtar=urun.stok | deger=0 -> varsayilan
kampanya: anahtar=kampanya.kod | deger=yok -> varsayilan
is calistiktan sonra
fiyat: anahtar=urun.fiyat | deger=125 -> disk
stok: anahtar=urun.stok | deger=8 -> disk
kampanya: anahtar=kampanya.kod | deger=YAZ10 -> disk
bellek: 0
disk: 3
ag: 0
varsayilan: 0
```

İş çalışmadan önce üç okumanın üçü varsayılandı. İş çalıştıktan sonra aynı üç okuma diskten geldi. Okumanın zincirine ağ girmedi: ağ yalnız diski doldurdu, ekran diskten okudu. Bu dersin kökeni disktir. Yenilemeyi ekran açılırken değil, ekran açılmadan önce bir iş yaptı.

**VK3.** Model, kısıtların sağlandığı anı verilmiş bir küme olarak alır ve işi o anda tek adımda çalıştırır. Platformda sistemin işi ne zaman başlattığı, işin yarıda kesilmesi ve aynı işin iki kez kuyruğa girmesi modelde yoktur.

#### Tek Seferlik ve Dönemsel İş

Tek seferlik iş bir kez çalışır. Dönemsel iş her dönemde yeniden çalışır. İkisinin ekranın okumasına etkisi, sunucu değeri değiştiğinde ayrılır. Disk 120 ve 9 ile başlıyor; sunucu önce 125 ve 8, sonra 130 ve 6 söylüyor.

```kotlin
// Main.kt — tek seferlik is bir kez, donemsel is her donemde diski yeniler
fun main() {
    val required = setOf("ag-bagli")
    val start = mapOf("urun.fiyat" to "120", "urun.stok" to "9")
    val first = Tier("ag", mapOf("urun.fiyat" to "125", "urun.stok" to "8"))
    val second = Tier("ag", mapOf("urun.fiyat" to "130", "urun.stok" to "6"))
    val keys = listOf("urun.fiyat", "urun.stok")
    val afterFirst = runWork(required, required, start, first, keys)
    println("tek seferlik: ilk calisma sonrasi")
    readHomeScreen(afterFirst).forEach { it.show() }
    println("tek seferlik: sunucu degisti, is bir daha calismadi")
    readHomeScreen(afterFirst).forEach { it.show() }
    val afterSecond = runWork(required, required, afterFirst, second, keys)
    println("donemsel: ikinci calisma sonrasi")
    readHomeScreen(afterSecond).forEach { it.show() }
}
```

```text
tek seferlik: ilk calisma sonrasi
fiyat: anahtar=urun.fiyat | deger=125 -> disk
stok: anahtar=urun.stok | deger=8 -> disk
kampanya: anahtar=kampanya.kod | deger=yok -> varsayilan
tek seferlik: sunucu degisti, is bir daha calismadi
fiyat: anahtar=urun.fiyat | deger=125 -> disk
stok: anahtar=urun.stok | deger=8 -> disk
kampanya: anahtar=kampanya.kod | deger=yok -> varsayilan
donemsel: ikinci calisma sonrasi
fiyat: anahtar=urun.fiyat | deger=130 -> disk
stok: anahtar=urun.stok | deger=6 -> disk
kampanya: anahtar=kampanya.kod | deger=yok -> varsayilan
```

Tek seferlik iş fiyatı 125 yaptı ve sunucu 130 söylese de ekran 125 gösterdi: iş bir daha çalışmadığı için disk değişmedi. Dönemsel iş ikinci çalışmada diski 130 ve 6 yaptı. Üç aşamada da kök aynı: disk. Değişen, diskin değeridir. Kural değerin güncel olup olmadığını söylemez; sunucu değerini yan yana koyduğumuz için eskiyi görürüz.

#### Sınırlayıcı Ölçüm

Kısıt sağlanmazsa ne olur? İki durum deneyelim. Birincide disk boş ve şarj yok. İkincisinde disk önceki bir çalışmadan kalma değerleri taşıyor ve ağ yok. İkisinde de iş gereken kısıtları bulamıyor.

```kotlin
// Main.kt — kisit saglanmadikca is calismaz, okuma varsayilan ya da eski disk kalir
fun main() {
    val required = setOf("ag-bagli", "sarjda")
    val network = Tier("ag", mapOf("urun.fiyat" to "125", "urun.stok" to "8", "kampanya.kod" to "YAZ10"))
    val keys = listOf("urun.fiyat", "urun.stok", "kampanya.kod")
    val old = mapOf("urun.fiyat" to "120", "urun.stok" to "9")
    val states = listOf(
        Triple("bos disk, sarj yok", emptyMap<String, String>(), setOf("ag-bagli")),
        Triple("eski disk, ag yok", old, setOf("sarjda")),
    )
    for ((name, disk, satisfied) in states) {
        println(name)
        val reads = readHomeScreen(runWork(required, satisfied, disk, network, keys))
        reads.forEach { it.show() }
        countOrigins(reads)
    }
}
```

```text
bos disk, sarj yok
fiyat: anahtar=urun.fiyat | deger=bilinmiyor -> varsayilan
stok: anahtar=urun.stok | deger=0 -> varsayilan
kampanya: anahtar=kampanya.kod | deger=yok -> varsayilan
bellek: 0
disk: 0
ag: 0
varsayilan: 3
eski disk, ag yok
fiyat: anahtar=urun.fiyat | deger=120 -> disk
stok: anahtar=urun.stok | deger=9 -> disk
kampanya: anahtar=kampanya.kod | deger=yok -> varsayilan
bellek: 0
disk: 2
ag: 0
varsayilan: 1
```

Boş diskte iş koşmadığı için üç okumanın üçü varsayılan kaldı. Eski diskte iki okuma eski değerle diskten geldi, kampanya varsayılan kaldı. Sunucu yeni değerleri biliyordu, ama iş onları diske taşımadı. Kural bunu görmez: sırada yalnız disk vardır ve yanıtı disk ya da yedek değer verir.

Ölçümün söylemediği şey, kısıtların ne kadar sürede sağlanacağı ve ekranın o zamana kadar ne göstermesi gerektiğidir. Birincisi platformun, ikincisi tasarımın kararıdır.

**VK4.** Model, bir kısıtı sağlanmış ya da sağlanmamış olarak alır. Platformda kısıtın kısmen sağlanması, sağlanıp yeniden bozulması ve işin kısıt sağlanana kadar bekleme biçimi modelde yoktur.

#### Kapanış Tablosu

Kursun on dört dersi aynı soruyu sordu. Her dersin ölçtüğü okuma, dersin kökeni ve sınırlayıcı ölçümü tabloda. Köken sütunu, her dersin `Özet` bölümünde yazılı olandır.

| Ders | Ölçülen okuma | Köken | Sınırlayıcı ölçüm |
|---|---|---|---|
| Katman Modeli | Alan katmanından geçen fiyat | ağ | Bellekteki eski değer, ağ yenisini söylese de yanıt verir |
| Görünüm Modeli | Döndürmede adet ve fiyat | bellek | Süreç sonlanmasında okumalar ağa ve varsayılana düşer |
| Durum Tutucular | Kaydedilen arama metni ve sekme | disk | Kaydedilmeyen alan varsayılana ya da ağa düşer |
| Gözlemlenebilir Durum | Yükleme bitmeden okunan fiyat | varsayılan | Yükleme bitince fiyat ve stok bellekten gelir |
| Tek Yönlü Veri Akışı | Olayın kurduğu adet | bellek | Olay gelmeden dört okuma varsayılan |
| Bağımlılık Enjeksiyonu | Konteynerin kurduğu zincirde başlık | ağ | Sahte veri bellek basamağından yanıt verir |
| Depo Kalıbı | Depoya ilk giren okuma | bellek | Zincir sırası değişince köken ağa geçer |
| Yerel Veritabanı | Göçten geçen satır | disk | Yazılmamış alan varsayılan, doldurulunca disk |
| Anahtar–Değer Saklama | Hiç yazılmamış tercih | varsayılan | Yazılan tercih ve boş dizgi diskten gelir |
| Dosya ve Medya Depolama | Uygulamanın dizinindeki dosya | disk | Medya deposundaki dosya, kopyalanana kadar yanıtsız |
| Ağ İstemcisi | Çözülen yanıtın alanları | ağ | Çözülemeyen alan varsayılana düşer |
| Önbellekleme ve Çevrimdışı Destek | Yenileme sonrası okuma | disk | Ağ düşünce disk kalır, değer eskir |
| Sayfalı Veri Yükleme | Yüklenmemiş sayfanın ögesi | varsayılan | Yüklenen sayfada öge ağdan ya da diskten gelir |
| Arka Plan İşleri | Kısıt sağlanınca diske yazan işten sonraki okuma | disk | Kısıt sağlanmazsa varsayılan ya da eski disk |

Tablodaki kökenleri sayan blok, yazılmış bir listeyi sayar; kural ölçümü değildir. Aynı bloğa çekirdeğin sekiz okumasının dağılımını da koyuyoruz.

```kotlin
// Main.kt — on dort dersin ve cekirdegin kokenlerini sayar
fun main() {
    val lessons = listOf(
        "katman-modeli" to "ag", "gorunum-modeli" to "bellek", "durum-tutucular" to "disk",
        "gozlemlenebilir-durum" to "varsayilan", "tek-yonlu-veri-akisi" to "bellek",
        "bagimlilik-enjeksiyonu" to "ag", "depo-kalibi" to "bellek", "yerel-veritabani" to "disk",
        "anahtar-deger-saklama" to "varsayilan", "dosya-ve-medya-depolama" to "disk",
        "ag-istemcisi" to "ag", "onbellekleme" to "disk", "sayfali-veri-yukleme" to "varsayilan",
        "arka-plan-isleri" to "disk",
    )
    val core = mapOf("bellek" to 2, "disk" to 3, "ag" to 1, "varsayilan" to 2)
    println("dersler: ${lessons.size}")
    for (name in listOf("bellek", "disk", "ag", "varsayilan")) {
        println("$name: ${lessons.count { it.second == name }}")
    }
    println("cekirdek okumalar: ${core.values.sum()}")
    for ((name, n) in core) {
        println("$name: $n")
    }
}
```

```text
dersler: 14
bellek: 3
disk: 5
ag: 3
varsayilan: 3
cekirdek okumalar: 8
bellek: 2
disk: 3
ag: 1
varsayilan: 2
```

On dört dersin üçü bellek, beşi disk, üçü ağ, üçü varsayılan. Çekirdeğin sekiz okumasının dağılımı ise 2, 3, 1, 2'dir. İkisi aynı şey değildir: çekirdek sekiz okumayı, tablo on dört dersi sayar. Sayıların benzer görünmesi bir kural değildir; birinin diğerini öngörmesi beklenmez.

#### Özet

- Bu dersin kökeni: disk. Kısıt sağlanınca iş ağdan çekip diske yazar ve ekranın sonraki okuması diskten gelir.
- İşin ekrandan farkı, yenilemeyi ekran açılmadan önce yapmasıdır; ağ okumanın zincirine girmez.
- Tek seferlik iş bir kez, dönemsel iş her dönemde diski yeniler; iş çalışmadıkça disk eski kalır.
- Kısıt sağlanmadıkça iş çalışmaz: okuma boş diskte varsayılan, eski diskte eski değerle disk olur.
- Kapanış tablosu on dört dersin kökenini yan yana koyar: bellek 3, disk 5, ağ 3, varsayılan 3.

#### Kurs Kapanışı

Bu kurs, Android uygulamasında bir okumanın yanıtının hangi basamaktan geldiğini on dört derste ölçtü: mimari katmanlardan görünüm modeline, durum tutuculardan bağımlılık enjeksiyonuna, depodan yerel veritabanına, tercih ve dosya depolamadan ağ istemcisine, önbellekten sayfalı yüklemeye ve arka plan işlerine. Ölçü hep aynıydı ve kural hep yalnız sırayı gördü; tazelik, doğruluk ve tasarım kararları ölçünün dışında kaldı. Sıradaki kurs, `Platform Servisleri ve Tümleştirmeler`, uygulamanın platform servisleriyle ve dış tümleştirmelerle kurduğu ilişkiyi konu alır.

