State hoisting moves state to the caller and replaces it with a value and an event callback, React 7,897 's controlled component: QuantityStepper(quantity: Int, onQuantityChange: (Int) -> Unit) owns nothing. State flows down, events flow up. The state goes to the lowest common parent of its readers; the cart total needs every quantity, so CartScreen owns them:
@Composable
fun CartScreen(books: List<Book>) {
LogCompositions("CartScreen")
val quantities = remember { mutableStateMapOf<Int, Int>() } // book id to quantity
val total = books.sumOf { it.price * (quantities[it.id] ?: 0) }
Column(Modifier.padding(20.dp), verticalArrangement = Arrangement.spacedBy(8.dp)) {
books.forEach { book ->
CartLine(book, quantities[book.id] ?: 0, onQuantityChange = { quantities[book.id] = it })
}
Text("Total: $%.2f".format(total), style = MaterialTheme.typography.titleLarge)
}
}Hoist no higher than needed, because whoever reads a state recomposes; when logic grows, the owner becomes a ViewModel (ViewModel and UDF).