İçeriğe geç
academia.sh

Ders 05 / 17

Yan Etkiler

Bir bileşen işlevi ağacı kurarken ağacın dışına da dokunabilir: servise istek atar, kayıt tutar, bir dinleyiciye abone olur. Ders bu hareketleri ölçer ve hepsinin aynı yöne, dışa gittiğini gösterir.

Yayın Güncelleme

Bu derste

  • Yan etkiyi arayüz ağacının dışına çıkan bir hareket olarak tanımlar
  • Anahtar değişince etkinin yeniden başladığını modelde sayar
  • Abonelik ve temizlik hareketlerinin ikisinin de dış olduğunu ölçer
  • Servis yanıtının ağaca durum yazısıyla döndüğünü ve yönünün yukarı olduğunu gösterir
İçindekiler

Şimdiye kadar veri ağacın içinde dolaştı: aşağı indi, yukarı çıktı, araya girenleri atlayıp derine gitti. Oysa bir bileşen işlevinin yaptığı her şey ağaca düğüm eklemek değildir. Bir bileşen işlevi bir servise istek atabilir, bir kayıt satırı yazabilir, platformun bir dinleyicisine abone olabilir. Bunlar yan etkidir: işlevin ağaç dışında bıraktığı iz. Dersin sorusu yine aynıdır: bu veri arayüz ağacında hangi yöne gidiyor? Yan etkinin yanıtı hep aynıdır.

// Direction.kt — bir verinin arayuz agacinda hangi yone gittigini siniflandirir
class Tree(private val parentOf: Map<String, String>) {
    fun contains(node: String): Boolean = node in parentOf.keys || node in parentOf.values
    fun parent(node: String): String? = parentOf[node]
    fun ancestors(node: String): List<String> {
        val chain = mutableListOf<String>()
        var current = parentOf[node]
        while (current != null) {
            chain.add(current)
            current = parentOf[current]
        }
        return chain
    }
}

class Move(val label: String, val from: String, val to: String, tree: Tree) {
    val direction: String = when {
        !tree.contains(from) || !tree.contains(to) -> "dis"
        to in tree.ancestors(from) -> "yukari"
        tree.parent(to) == from -> "asagi"
        from in tree.ancestors(to) -> "derin"
        else -> error("akrabasiz: $from ve $to")
    }

    fun describe() {
        println("$label: kimden=$from | kime=$to -> $direction")
    }
}

fun countDirections(moves: List<Move>) {
    for (name in listOf("asagi", "yukari", "derin", "dis")) {
        println("$name: ${moves.count { it.direction == name }}")
    }
}

Ağacı Terk Eden Hareket

Platform belgesi yan etkiyi bileşen işlevinin kapsamı dışında uygulamanın durumunu değiştiren olay olarak anlatır ve bileşen işlevlerinin yeniden oluşturmayla her an yeniden çalışabildiğini hatırlatır. Bu yüzden etkinin işlevin gövdesine doğrudan yazılmaması, belgenin sunduğu etki işlevlerinden biriyle sarılması gerekir. Belge LaunchedEffect için şunu söyler: “When LaunchedEffect enters the Composition, it launches a coroutine with the block of code passed as a parameter.” Anahtar değişirse mevcut eş yordam iptal edilir ve yenisi başlatılır.

// Platform kodu: LaunchedEffect
import androidx.compose.runtime.*

@Composable
fun CartRow(productId: String) {
    LaunchedEffect(productId) {
        service.loadPrice(productId)
    }
    Text("Fiyat yükleniyor")
}

productId aynı kaldıkça yeniden oluşturma etkiyi yeniden başlatmaz. Değişirse önceki iş iptal edilir, yeni anahtarla yeni iş başlar. Bu parça platform kodudur ve burada çıktı iddia etmiyoruz. Model, platformun bu kuralını kurar; model platformun kendisi değildir.

Modelde etkiyi sahibi düğüm, hedefi ve anahtarı tanımlar. Hedef ağaçta olmayan bir ad olduğu için etkiden çıkan her hareket dıştır.

// Effects.kt — anahtar degisince yeniden baslayan etki
class Effect(val owner: String, val target: String, private var key: String) {
    fun keyChanged(newKey: String): Boolean {
        val changed = newKey != key
        key = newKey
        return changed
    }
}

fun runIfKeyChanged(effect: Effect, newKey: String, tree: Tree): List<Move> =
    if (effect.keyChanged(newKey)) {
        listOf(Move("istek-$newKey", effect.owner, effect.target, tree))
    } else {
        emptyList()
    }

fun contactMove(label: String, effect: Effect, tree: Tree): Move =
    Move(label, effect.owner, effect.target, tree)

fun replyMove(label: String, effect: Effect, tree: Tree): Move =
    Move(label, effect.target, effect.owner, tree)

fun writeToOwner(label: String, effect: Effect, tree: Tree): Move =
    Move(label, effect.owner, tree.ancestors(effect.owner).dropLast(1).last(), tree)

fun writeToParent(label: String, effect: Effect, tree: Tree): Move =
    Move(label, effect.owner, tree.ancestors(effect.owner).first(), tree)

Bu yardımcılar hareketlerin uçlarını Effect alanlarından ve ağaçtan türetir. contactMove sahipten hedefe, replyMove hedeften sahibe gider. writeToOwner durumu tutan ekranı ağaçtan seçer, writeToParent en yakın ebeveyni seçer. Bu işlevler hareketin uçlarını modelin Effect kaydından okur; dış uç, ağaçta olmayan ve modelin kaydında tanımlanan bir addır. Kayıtlardaki sahip ve hedef adları Effect(...) satırında yazılır, ama hareket bu adlarla elle kurulmaz.

// Main.kt — dort yeniden olusturma, iki anahtar, iki istek
fun main() {
    val tree = Tree(mapOf(
        "Screen" to "App",
        "CartList" to "Screen",
        "CartRow" to "CartList",
    ))
    val effect = Effect("CartRow", "Service", "")
    val keys = listOf("k1", "k1", "k2", "k2")
    val moves = keys.flatMap { runIfKeyChanged(effect, it, tree) }
    println("yeniden olusturma: ${keys.size} | calisan etki: ${moves.size}")
    moves.forEach { it.describe() }
    countDirections(moves)
}
yeniden olusturma: 4 | calisan etki: 2
istek-k1: kimden=CartRow | kime=Service -> dis
istek-k2: kimden=CartRow | kime=Service -> dis
asagi: 0
yukari: 0
derin: 0
dis: 2

Dört yeniden oluşturmada anahtar yalnız iki kez değişti: ilk anahtar ve k2. Etki iki kez çalıştı, iki hareket çıktı ve ikisi de dış. Service ağaçta olmadığı için kural başka bir şeye bakmaz.

BI9. Model anahtarın farklılaşmasını metin eşitliğiyle kurar. Platformda anahtarın equals ile karşılaştırılması, eş yordamın iptal edilme anı ve ilk çalışmanın zamanlaması ölçülmedi.

Abonelik ve Temizlik

Bazı etkiler bir şeye bağlanır ve bağlandıkları şeyden ayrılmaları gerekir. Belge DisposableEffect için şunu der: bir DisposableEffect bloğunun son ifadesi onDispose olmak zorundadır; aksi hâlde derleme sırasında hata verilir. Anahtar değişirse ya da bileşen işlevi ağaçtan çıkarsa platform önce onDispose’u çalıştırır.

// Platform kodu: DisposableEffect
import androidx.compose.runtime.*

@Composable
fun CartScreen(logger: Logger) {
    DisposableEffect(logger) {
        logger.subscribe()
        onDispose { logger.unsubscribe() }
    }
}

Abone olmak da, abonelikten çıkmak da ağacın dışına gider. Aynı hedefe giden iki hareket de dışa gider; biri gidip biri dönmez.

// Main.kt — abone ol ve temizle: iki hareket, ikisi de dis
fun main() {
    val tree = Tree(mapOf(
        "Screen" to "App",
        "CartList" to "Screen",
    ))
    val effect = Effect("Screen", "Log", "")
    val moves = listOf(
        contactMove("abone-ol", effect, tree),
        contactMove("abonelikten-cik", effect, tree),
    )
    moves.forEach { it.describe() }
    countDirections(moves)
}
abone-ol: kimden=Screen | kime=Log -> dis
abonelikten-cik: kimden=Screen | kime=Log -> dis
asagi: 0
yukari: 0
derin: 0
dis: 2

Temizlik bir geri dönüş gibi görünse de yön geri dönmek değildir. Kayıt ağaçta olmadığı için “kimden Screen, kime Log” dışa gider; kuralın çözebileceği başka bir yol yoktur.

Üçüncü ve dördüncü etki işlevleri farklı bir şey yapar. SideEffect, belgeye göre her başarılı yeniden oluşturmadan sonra çalışır ve platform durumunu platform dışı koda yayınlamak içindir. rememberUpdatedState ise etkinin yeniden başlamasını istemediğiniz bir değeri güncel tutar.

// Platform kodu: SideEffect ve rememberUpdatedState
import androidx.compose.runtime.*

@Composable
fun CartRow(onTimeout: () -> Unit, user: User) {
    val currentOnTimeout by rememberUpdatedState(onTimeout)
    LaunchedEffect(true) {
        delay(3000)
        currentOnTimeout()
    }
    SideEffect {
        analytics.setUser(user.id)
    }
}

LaunchedEffect(true) anahtarı hiç değişmediği için etki yeniden başlamaz; rememberUpdatedState sayesinde geri çağrı yine en güncel değeri taşır. SideEffect ise ağacın dışındaki analytics nesnesine her başarılı yeniden oluşturmada yazar. İkisi de dışa giden hareketlerdir.

Yanıt Ağaca Nasıl Döner

Yan etkinin istediği iş bitince bir yanıt gelir ve yanıt ağaca dönmek zorundadır, yoksa ekranda hiçbir şey değişmez. Bu dönüş doğrudan arayüz düğümüne yazılmaz. Etki yanıtı durum yazısıyla durumun sahibine verir. Üç hareket var: istek, yanıt, durum yazısı.

// Main.kt — istek ve yanit dista, durum yazisi agacin icinde
fun main() {
    val tree = Tree(mapOf(
        "Screen" to "App",
        "CartList" to "Screen",
        "CartRow" to "CartList",
    ))
    val effect = Effect("CartRow", "Service", "")
    val moves = listOf(
        contactMove("istek", effect, tree),
        replyMove("yanit", effect, tree),
        writeToOwner("durum-yazisi", effect, tree),
    )
    moves.forEach { it.describe() }
    countDirections(moves)
}
istek: kimden=CartRow | kime=Service -> dis
yanit: kimden=Service | kime=CartRow -> dis
durum-yazisi: kimden=CartRow | kime=Screen -> yukari
asagi: 0
yukari: 1
derin: 0
dis: 2

İstek ve yanıt aynı yöne, dışa gider. Hangisinin hangisi olduğunu yönden değil kimden ve kime ayırır: istekte kaynak ağaçtadır, yanıtta ağaçta değildir. Dersin yönü budur: dış. Üçüncü hareket ise ağaç içindedir. Etki yanıtı CartRow‘dan sahibe yazar ve bu bir yukarıdır; Ders 03’ün olay yolu buraya bağlanır.

Bu ders M12/K02’den farklıdır. O kurs bir istekle ağın hangi uçlara ulaştığını ve yayılımı sınıflar. Burada sorulan, yayılım değil, hareketin ağacın içinde mi dışında mı bittiğidir.

BI10. Servis yanıtının durum yazısıyla ağaca döndüğü, modelin kurduğu bir kuraldır. Eş yordamın hangi iş parçacığında çalıştığı, iptalin zamanlaması ve yanıtın birden çok kez gelmesi modelde yoktur.

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

İki sahip, aynı ağ ve aynı hareket sayısı. Birinde etki, ağaçtaki bir düğümden dışarıdaki bir hedefe iki hareket çıkarır. Diğerinde etki ağaç içinde kalan bir yazı yapar.

// Main.kt — etki disa gider, yazi icerde kalir
fun main() {
    val tree = Tree(mapOf(
        "Screen" to "App",
        "CartList" to "Screen",
        "CartRow" to "CartList",
    ))
    val service = Effect("CartRow", "Service", "")
    val log = Effect("CartRow", "Log", "")
    val outside = listOf(
        contactMove("istek", service, tree),
        contactMove("kayit", log, tree),
    )
    val inside = listOf(
        writeToOwner("yazi-1", service, tree),
        writeToParent("yazi-2", service, tree),
    )
    println("etki: ${outside.map { it.direction }}")
    println("yazi: ${inside.map { it.direction }}")
}
etki: [dis, dis]
yazi: [yukari, yukari]

Aynı kaynak, iki hedef ailesi, iki yön. Fark, hedefin ağaçta olup olmamasıdır; etkinin hangi etki işleviyle sarıldığı yönü değiştirmez.

Özet

  • Bu dersin yönü: dış. Etkiden çıkan her hareket ağacın dışındaki bir hedefe gider.
  • Dört yeniden oluşturmada anahtar iki kez değiştiği için etki 2 kez çalıştı ve ikisi de dış çıktı.
  • Abone olmak ve temizlemek iki ayrı hareket olarak ölçüldü: dış 2.
  • Servis yanıtı ağaca durum yazısıyla döner: dış 2, yukarı 1.
  • Ağaçta kalan iki yazı yukarı, ağaçtan çıkan iki etki dış ölçüldü; etki işlevinin türü yönü değiştirmez.

Sonraki Adım

Etkiyi yeniden başlatan şey anahtarın değişmesiydi. Bir değerin değişip değişmediğini platformun nasıl anladığı, yeniden oluşturmada atlamanın temelidir. Sıradaki ders, Değişmezlik ve Kararlılık, bu soruyu ve onun sahibe yönünü ölçer.

İlerlemeni kaydetmek ve not almak için Giriş yap

Notlarım

Not almak için giriş yapmalısın.

Aramak için yazmaya başlayın.

↑↓ ↵ Esc gezin · aç · kapat