Skip to content
academia.sh

Lesson 02 / 14

The View Model

A view model holds screen data so that it survives a configuration change, but not process death. The lesson measures how the origin of a read changes when the screen rotates and when the process dies.

Published Updated

In this lesson

  • Distinguishes that a view model is kept across a configuration change and lost on process death
  • Measures that after the screen rotates, the answer to a read comes from the memory tier
  • Shows that when the UI owns the memory, the same read goes to the network
  • Classifies that after process death the same reads fall to disk, the network or the default
Contents

In the previous lesson we built the chain that classifies the origin of a read and filled memory by hand. On the platform there is an owner that fills memory and keeps it when the screen rotates: the view model (ViewModel). In this lesson we look at the first tier of the chain, and the question stays the same: where does the answer to this read come from?

The measuring tool is the previous lesson’s core; we repeat it unchanged.

// Origin.kt — classifies which tier a read's answer came from
class Tier(val origin: String, private val entries: Map<String, String>) {
    init {
        require(origin in listOf("memory", "disk", "network")) { "invalid tier: $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 ?: "default"

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

fun countOrigins(reads: List<Read>) {
    for (name in listOf("memory", "disk", "network", "default")) {
        println("$name: ${reads.count { it.origin == name }}")
    }
}

Memory That Survives Rotation

When the screen rotates, or the language or dark theme changes, the system rebuilds the screen’s UI from scratch. This is called a configuration change. The UI objects disappear; the view model does not. The system gives the same view model to the new UI. Because the view model holds the value read from the network in a field, the new UI reads the same value again without going to the network.

// Platform code: 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 is a field of the view model. Work inside viewModelScope continues until the view model is cleared; rotating the screen does not cancel it. This piece is platform code, and here we make no claim about output, timing or file paths. The model builds this rule of the platform; the model is not the platform itself.

In earlier courses, the lifetime axes (M10/K02, M11/K02, M13/K03) measured how long something lives. This lesson also talks about lifetime, but that is not the measure. Memory is not a lifetime but a place in the chain: the measure is whether the memory tier gave the answer to the read.

The model needs three functions. fillMemory puts the values from the network into an owner’s memory. surviveRotation returns whose memory remains after rotation. afterTermination keeps only the saved keys in memory once the process has died. All three transform their input.

// Lifecycle.kt — which memory remains after a screen rotation and after process death
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 merges the memories of the owners in the set of keepers; the memory of an owner that is not kept does not reach the new UI.

Suppose the cart screen has loaded the count and the price into the view model’s memory. The screen rotated. The new UI reads five values.

// Main.kt — the screen rotated, the view model's memory was kept
fun main() {
    val network = mapOf("cart.count" to "3", "product.price" to "120", "product.stock" to "7")
    val disk = mapOf("settings.theme" to "dark")
    val stores = mapOf("view-model" to fillMemory(mutableMapOf(), network, listOf("cart.count", "product.price")))
    val memory = surviveRotation(stores, setOf("view-model"))
    println("remaining memory keys: ${memory.keys.sorted()}")

    val chain = listOf(Tier("memory", memory), Tier("disk", disk), Tier("network", network))
    val reads = listOf(
        Read("cart-count", "cart.count", chain, "0"),
        Read("product-price", "product.price", chain, "0"),
        Read("theme", "settings.theme", chain, "light"),
        Read("stock", "product.stock", chain, "0"),
        Read("notification", "notification.enabled", chain, "off"),
    )
    reads.forEach { it.show() }
    countOrigins(reads)
}
remaining memory keys: [cart.count, product.price]
cart-count: key=cart.count | value=3 -> memory
product-price: key=product.price | value=120 -> memory
theme: key=settings.theme | value=dark -> disk
stock: key=product.stock | value=7 -> network
notification: key=notification.enabled | value=off -> default
memory: 2
disk: 1
network: 1
default: 1

Two reads came from memory: the count and the price. The network was not consulted. stock came from the network because it was never loaded, theme from disk, and notification from the default.

AA1. The model counts rotation as a single event and loses no field of the view model. On the platform, if written wrongly, the view model’s own fields can also be leaked by holding on to a UI object; the model has no such case.

Whose Memory

What would happen if a UI object held the memory? The same two values would have been loaded on the same screen from the same network, but the owner would disappear on rotation. Now we put the two owners side by side. The same load is done for each owner, the same rotation is applied, and the same three reads run again.

// Main.kt — the same load, two different owners
fun main() {
    val network = mapOf("cart.count" to "3", "product.price" to "120")
    val disk = mapOf("settings.theme" to "dark")
    for (owner in listOf("view-model", "ui")) {
        val stores = mapOf(owner to fillMemory(mutableMapOf(), network, listOf("cart.count", "product.price")))
        val memory = surviveRotation(stores, setOf("view-model"))
        val chain = listOf(Tier("memory", memory), Tier("disk", disk), Tier("network", network))
        println("owner: $owner")
        countOrigins(listOf(
            Read("cart-count", "cart.count", chain, "0"),
            Read("product-price", "product.price", chain, "0"),
            Read("theme", "settings.theme", chain, "light"),
        ))
    }
}
owner: view-model
memory: 2
disk: 1
network: 0
default: 0
owner: ui
memory: 0
disk: 1
network: 2
default: 0

When the owner is the view model, two reads come from memory. When the owner is the UI, memory stays empty and the same two reads go to the network; theme comes from disk in both cases. The values are the same and the load is the same; the only difference is whether the memory survived the rotation.

AA2. The model takes the kept owners as a set. On the platform the system decides which owner is kept; choosing the set by hand is possible in the model but not on the platform.

The Bounding Measurement

A view model survives a configuration change but not process death. Under memory pressure the system can end the app’s background process; every object in memory, the view model included, goes away. Only saved state returns. In this lesson we saved no keys; saved state is the subject of the next lesson. We pass the same memory through the two events.

// Main.kt — rotation and process death carry the same reads to different origins
fun main() {
    val network = mapOf("cart.count" to "3", "product.price" to "120")
    val disk = mapOf("settings.theme" to "dark")
    val viewModelMemory = fillMemory(mutableMapOf("screen.tab" to "summary"), network, listOf("cart.count", "product.price"))
    val events = listOf(
        "rotation" to surviveRotation(mapOf("view-model" to viewModelMemory), setOf("view-model")),
        "process-death" to afterTermination(viewModelMemory, emptySet()),
    )
    for ((event, memory) in events) {
        println("event: $event")
        val chain = listOf(Tier("memory", memory), Tier("disk", disk), Tier("network", network))
        val reads = listOf(
            Read("cart-count", "cart.count", chain, "0"),
            Read("product-price", "product.price", chain, "0"),
            Read("theme", "settings.theme", chain, "light"),
            Read("screen-tab", "screen.tab", chain, "first"),
        )
        reads.forEach { it.show() }
        countOrigins(reads)
    }
}
event: rotation
cart-count: key=cart.count | value=3 -> memory
product-price: key=product.price | value=120 -> memory
theme: key=settings.theme | value=dark -> disk
screen-tab: key=screen.tab | value=summary -> memory
memory: 3
disk: 1
network: 0
default: 0
event: process-death
cart-count: key=cart.count | value=3 -> network
product-price: key=product.price | value=120 -> network
theme: key=settings.theme | value=dark -> disk
screen-tab: key=screen.tab | value=first -> default
memory: 0
disk: 1
network: 2
default: 1

On rotation, three of the four reads came from memory. On process death, the result of the same four reads changed: the count and the price were read from the network, theme stayed on disk, and screen-tab fell to the default because it was found in no tier. The tab selection in memory counts as lost, because it was not saved.

AA3. The model assumes the network stays reachable on process death. On the platform, with no connection, the count and the price would fall to the default as well; the model does not set up this case.

Summary

  • This lesson’s origin: memory. When the screen rotates, the view model’s memory is the first tier of the chain, and the count and the price are read from it.
  • The lesson’s measure is whether the memory tier gave the answer to a read. After rotation, five reads came out as memory 2, disk 1, network 1, default 1.
  • For the same load, when the memory owner is the view model the result is memory 2, network 0; when the owner is the UI, memory 0, network 2.
  • On process death memory empties: over four reads, memory dropped from 3 to 0, the network rose from 0 to 2, and the default rose from 0 to 1.
  • The unsaved tab selection fell to the default; saved state is the subject of the next lesson.

Next Step

In this lesson the set of saved keys was empty and the tab selection was lost. The next lesson, State Holders, measures how the screen’s state is gathered in one place and how the saved fields are restored from disk.

To keep your progress and take notes, Log in

My notes

Log in to take notes.

Start typing to search.

↑↓ ↵ Esc navigate · open · close