Ders 04 / 17
Yeniden Oluşturma
Durum değişince platform, değişikliği okuyan bileşen işlevlerini yeniden çalıştırır ve girdisi değişmeyenleri atlar. Ders değişikliğin sahibinden okuyucusuna hangi yolla gittiğini ve okumanın ertelenmesinin yönü nasıl değiştirdiğini ölçer.
Yayın Güncelleme
Bu derste
- Yeniden oluşturmayı ve girdisi değişmeyen bileşen işlevinin atlanmasını açıklar
- Değişikliğin yeniden oluşturulan ve atlanan düğümlerini modelde ayırır
- Okuma sahipte kalırsa değişikliğin her düzeyden aşağı indiğini ölçer
- Okuma en alttaki düğüme ertelenirse değişikliğin tek derin hareketle ulaştığını gösterir
İçindekiler
Önceki derste olay sahibine döndü ve sahip yeni değeri yazdı. Yazıdan sonra ekranın güncellenmesi gerekir, ama ağacın tamamının yeniden çalışması gerekmez. Platform durum değişince yalnız değişiklikten etkilenebilecek bileşen işlevlerini yeniden çalıştırır. Buna yeniden oluşturma denir. Dersin sorusu yine aynıdır: bu veri arayüz ağacında hangi yöne gidiyor? Burada veri, durumun yeni değeridir ya da değiştiğinin haberidir; ve yönü, okumanın nerede yapıldığına bağlı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 }}")
}
}
Yeniden Oluşturma ve Atlama
Platform belgesine göre durum değiştiğinde platform yeniden oluşturma planlar: değişmiş olabilecek bileşen işlevlerini yeniden çalıştırır ve oluşturulan ağacı günceller. Yeniden oluşturma genellikle bir State<T> nesnesinin değişmesiyle başlar; platform bu nesneyi okuyan bileşen işlevlerini ve onların çağırdığı, atlanamayan bileşen işlevlerini çalıştırır. Girdisi önceki oluşturmadan değişmemiş uygun bir bileşen işlevi ise tümüyle atlanabilir. Belgeye göre kararlı girdiler equals ile, kararsız girdiler güçlü atlama kipinde referans eşitliğiyle karşılaştırılır; girdiler önceki değerlerle eşitse bileşen işlevi atlanır.
Bir yeniden oluşturmanın hangi bileşen işlevlerini kapsadığına yeniden oluşturma kapsamı denir. Değişen durumu okuyan bileşen işlevi kapsamın içindedir. Kapsamın içindeki bir bileşen işlevi çocuğuna değişen bir değer geçirirse çocuk da kapsama girer; değişmeyen girdiyle çağrılan çocuk atlanır.
Sepet ekranında toplamı CartScreen hatırlasın ve en alttaki CartRow göstersin. Önce değeri sahibinde okuyup parametreyle indiren biçime bakalım.
// Platform kodu: ValueChannel
import androidx.compose.runtime.*
@Composable
fun CartScreen() {
var total by remember { mutableStateOf(0) }
Header()
CartList(total = total)
}
@Composable
fun CartList(total: Int) {
CartRow(total = total)
}
@Composable
fun CartRow(total: Int) {
Text("Toplam: $total")
}
CartScreen değeri kendisi okur ve CartList’e geçirir; CartList kullanmadan CartRow’a iletir. Header değişmeyen girdiyle çağrılır. Bu parça platform kodudur ve burada çıktı, düzen, yeniden oluşturma sayısı ya da süre iddia etmiyoruz. Model, platformun bu kuralını kurar; model platformun kendisi değildir.
Model yeniden oluşturmayı üç işleviyle kurar. scopeChain, iki düğüm arasındaki yolu verir. recomposedScopes, okumanın yapıldığı düğümden değeri kullanan düğüme kadar olan yolu kapsam sayar. skippedScopes, kapsamın çocuğu olup kapsamda olmayan düğümleri atlanmış sayar. Ayrıca iki kanalın hareketlerini valueMoves ve deferredMoves kurar; ikincisini bu dersin ilerisinde kullanacağız.
// Channels.kt — degisikligin sahibinden okuyucuya gittigi iki yol
fun scopeChain(from: String, to: String, tree: Tree): List<String> =
((listOf(to) + tree.ancestors(to)).takeWhile { it != from } + from).reversed()
fun recomposedScopes(readAt: String, consumer: String, tree: Tree): List<String> =
scopeChain(readAt, consumer, tree)
fun skippedScopes(recomposed: List<String>, nodes: List<String>, tree: Tree): List<String> =
nodes.filter { it !in recomposed && tree.parent(it) in recomposed }
fun valueMoves(owner: String, consumer: String, tree: Tree): List<Move> =
scopeChain(owner, consumer, tree).zipWithNext()
.map { (from, to) -> Move("degisim-${to.lowercase()}", from, to, tree) }
fun deferredMoves(owner: String, consumer: String, tree: Tree): List<Move> =
listOf(Move("degisim-${consumer.lowercase()}", owner, consumer, tree))
// Main.kt — okuma sahipte: kapsam yoldaki uc dugumdur, Header atlanir
fun main() {
val tree = Tree(mapOf(
"Screen" to "App",
"Header" to "Screen",
"CartList" to "Screen",
"CartRow" to "CartList",
))
val nodes = listOf("App", "Screen", "Header", "CartList", "CartRow")
val recomposed = recomposedScopes("Screen", "CartRow", tree)
println("yeniden olusturulan: ${recomposed.sorted()}")
println("atlanan: ${skippedScopes(recomposed, nodes, tree).sorted()}")
}
yeniden olusturulan: [CartList, CartRow, Screen] atlanan: [Header]
Okuma Screen’de yapıldığı için kapsam Screen’den başlar ve değerin indiği yol boyunca CartList ve CartRow’u içerir. Header kapsamın çocuğudur ama girdisi değişmemiştir: atlanır. App hiç etkilenmez. Bu modelin atlama için girdilerin kararlı ve eşit olduğunu varsaydığını not edelim; kararlılık sonraki dersin konusudur.
BI7. Model kapsamı, okuma düğümünden değeri kullanan düğüme kadar olan yol olarak kurar. Platformda satır içi işlevlerin kapsam sayılmaması, kapsamın bileşen işlevi sınırlarına göre belirlenmesi ve yeniden oluşturmanın kaç kez çalıştığı ölçülmedi.
Parametre Kanalı: İki Aşağı
Aynı değişikliği hareket olarak ölçelim. Okuma Screen’de kaldığında yeni değer CartList’e parametreyle geçer, CartList onu CartRow’a geçirir. Her geçiş ebeveynden doğrudan çocuğadır.
// Main.kt — parametre kanali: her duzeyde bir asagi hareket
fun main() {
val tree = Tree(mapOf(
"Screen" to "App",
"Header" to "Screen",
"CartList" to "Screen",
"CartRow" to "CartList",
))
val moves = valueMoves("Screen", "CartRow", tree)
moves.forEach { it.describe() }
countDirections(moves)
}
degisim-cartlist: kimden=Screen | kime=CartList -> asagi degisim-cartrow: kimden=CartList | kime=CartRow -> asagi asagi: 2 yukari: 0 derin: 0 dis: 0
İki aşağı hareket var. Aradaki CartList değeri kullanmıyor, ama yeni değer ona geldiği için yeniden oluşturuluyor. Hareket sayısını ve kapsamı okumanın yeri belirler.
Okumayı Ertelemek: Tek Derin
Platform belgesi okumanın ertelenmesini bir performans yöntemi olarak anlatır. Durum yukarıda tutuluyorsa ve bir çocukta okunuyorsa, okuma bir geri çağrıya sarılabilir; böylece okuma yalnız gerçekten gerektiğinde ve gerektiği yerde yapılır. Belgeye göre değer yalnız çocuk içinde okunduğunda, değişince en yakın kapsam çocuğun kendisi olur.
// Platform kodu: DeferredChannel
import androidx.compose.runtime.*
@Composable
fun CartScreen() {
var total by remember { mutableStateOf(0) }
Header()
CartList(total = { total })
}
@Composable
fun CartList(total: () -> Int) {
CartRow(total = total)
}
@Composable
fun CartRow(total: () -> Int) {
Text("Toplam: ${total()}")
}
CartScreen artık değeri okumaz; değeri okuyan bir geri çağrıyı aşağı verir. Okuma CartRow içinde, total() çağrıldığında yapılır. Geri çağrının kendisi ilk oluşturmada her düzeyden parametreyle geçer; ama değişen şey geri çağrı değil, durumun değeridir ve bu değişikliğin haberi doğrudan okuyanın kapsamına ulaşır. Modelde değişiklik hareketi sahibinden okuyucuya tek adımda gider.
// Main.kt — okuma ertelendi: kapsam tek dugum, degisiklik tek derin hareket
fun main() {
val tree = Tree(mapOf(
"Screen" to "App",
"Header" to "Screen",
"CartList" to "Screen",
"CartRow" to "CartList",
))
val nodes = listOf("App", "Screen", "Header", "CartList", "CartRow")
val recomposed = recomposedScopes("CartRow", "CartRow", tree)
println("yeniden olusturulan: ${recomposed.sorted()}")
println("atlanan: ${skippedScopes(recomposed, nodes, tree).sorted()}")
val moves = deferredMoves("Screen", "CartRow", tree)
moves.forEach { it.describe() }
countDirections(moves)
}
yeniden olusturulan: [CartRow] atlanan: [] degisim-cartrow: kimden=Screen | kime=CartRow -> derin asagi: 0 yukari: 0 derin: 1 dis: 0
Kapsam tek düğümdür. Screen ile CartList yeniden oluşturulmaz, çünkü ikisi de değeri okumaz. Değişiklik Screen’den CartRow’a tek hareketle, aradaki CartList’e uğramadan ulaşır: derin. Dersin yönü budur.
Bu ders M30/K03’ten farklıdır. O kursun “bu yeniden hesaplamayı ne tetikledi” sorusu bir görünümün neden yeniden hesaplandığını okuyanın gözünden sınıflar. Burada neden değil, değişikliğin sahibinden okuyucusuna giden yol ölçülür.
BI8. Ertelenmiş kanalda değişikliğin doğrudan okuyanın kapsamına gitmesi platformun gözlem kuralıdır ve modelde böyle kurulmuştur. Gözlemin nasıl çalıştığı, yani okumanın kapsamla nasıl ilişkilendirildiği modelde yoktur.
Sınırlayıcı Ölçüm
Aynı veri, aynı sahip, aynı okuyucu, iki kanal. Kural kanalı görmez; yalnız iki ucun konumuna bakar. Ama yönün farkı kanaldan gelir ve kapsamın boyutu da.
// Main.kt — ayni veri, iki kanal, iki yon
fun main() {
val tree = Tree(mapOf(
"Screen" to "App",
"Header" to "Screen",
"CartList" to "Screen",
"CartRow" to "CartList",
))
val byValue = valueMoves("Screen", "CartRow", tree)
val deferred = deferredMoves("Screen", "CartRow", tree)
println("parametre: kapsam=${recomposedScopes("Screen", "CartRow", tree).size} yonler=${byValue.map { it.direction }}")
println("ertelenmis: kapsam=${recomposedScopes("CartRow", "CartRow", tree).size} yonler=${deferred.map { it.direction }}")
}
parametre: kapsam=3 yonler=[asagi, asagi] ertelenmis: kapsam=1 yonler=[derin]
Parametre kanalı iki aşağı hareket ve üç düğümlük kapsam kurar. Ertelenmiş kanal tek derin hareket ve tek düğümlük kapsam kurar. Derin, “daha iyi” demek değildir: ertelenmiş okumada CartRow’un içeriği geri çağrıyla okunur ve bunun bedeli kodun okunurluğudur. Ders bu bedeli ölçmez. Ölçtüğü şey, aynı değişikliğin iki kanalda iki farklı yönle ve iki farklı kapsamla CartRow’a ulaşmasıdır.
Özet
- Bu dersin yönü: derin. Okuma en alttaki düğüme ertelenince değişiklik sahibinden okuyucuya tek hareketle ulaşır.
- Okuma sahipte kalınca yeniden oluşturulan düğümler
CartList,CartRowveScreenolur;Headeratlanır. - Parametre kanalında değişiklik her düzeyden iner: aşağı 2, derin 0.
- Ertelenmiş kanalda aynı değişiklik tek hareketle gider: aşağı 0, derin 1.
- İki kanal aynı iki ucu bağlar; kapsam 3 düğümden 1 düğüme iner. Kural kanalı görmez, yön farkı kanaldan gelir.
Sonraki Adım
Bu derste girdisi değişmeyen bileşen işlevlerinin atlandığını varsaydık. Hangi girdinin değişmemiş sayılacağı ayrı bir sorudur ve 06. dersin konusudur. Ondan önce Yan Etkiler dersi, yeniden oluşturmanın dışına, ağaç dışında iz bırakan etkilere bakar: hareketin ucu ağaçta olmadığında yön ne olur?
İlerlemeni kaydetmek ve not almak için Giriş yap
Notlarım
Not almak için giriş yapmalısın.