Jetpack compose: View Model and mutablestatelistof
Video: Jetpack compose: View Model and mutablestatelistof by Taught by Celeste AI - AI Coding Coach
Jetpack Compose: ViewModel with mutableStateListOf
ViewModelto hold business logic and survive config changes.mutableStateListOf<T>for a reactive list — Compose recomposes whenever youadd/remove/mutate it. Expose as read-onlyList<T>to the UI.
A small but architecturally meaningful task: build a ViewModel that owns a list of items, lets callers add new items, and notifies the UI to rerender when the list changes.
The Copilot prompt
import androidx.compose.runtime.mutableStateListOf
import androidx.lifecycle.ViewModel
// ViewModel holding a mutable state list of strings, with a function to add new items
class MyViewModel : ViewModel() {
Copilot completes:
class MyViewModel : ViewModel() {
private val _items = mutableStateListOf("Apple", "Banana", "Cherry")
val items: List<String> get() = _items
fun addItem(item: String) {
_items.add(item)
}
fun removeItem(item: String) {
_items.remove(item)
}
}
Walkthrough
Three pieces.
1. private val _items. The mutable backing state. mutableStateListOf<T> is Compose's reactive-list type — adding, removing, or modifying it triggers recomposition of any Composable that reads it.
2. val items: List<String> get() = _items. The public, read-only view. Consumers see List<String> (no add/remove); only the ViewModel can mutate. Standard "private mutable, public read-only" pattern.
3. Mutation methods. addItem and removeItem are the public surface for changes. The ViewModel's external API is "read the list, request changes via methods" — never direct list manipulation.
Why mutableStateListOf, not regular MutableList
// BAD: not reactive
private val _items = mutableListOf("Apple", "Banana")
// GOOD: reactive
private val _items = mutableStateListOf("Apple", "Banana")
mutableListOf returns a plain MutableList. Mutating it is invisible to Compose — the UI doesn't rerender. mutableStateListOf wraps the list in Compose's snapshot system, so changes propagate.
For single mutable values, the equivalent is mutableStateOf:
private val _count = mutableStateOf(0)
val count: Int get() = _count.value
For lists, use mutableStateListOf. For maps, mutableStateMapOf. All three play nicely with the snapshot system.
Using the ViewModel from Compose
@Composable
fun ItemList(viewModel: MyViewModel = viewModel()) {
Column {
Button(onClick = { viewModel.addItem("New ${viewModel.items.size + 1}") }) {
Text("Add")
}
LazyColumn {
items(viewModel.items) { item ->
Text(item)
}
}
}
}
viewModel() (from androidx.lifecycle.viewmodel.compose) provides the ViewModel scoped to the nearest LocalLifecycleOwner — typically your Activity or NavBackStackEntry.
When the user taps "Add," addItem mutates _items. Compose's snapshot system notices the change, marks the LazyColumn for recomposition, and the new row appears.
ViewModel survives config changes
The reason to use ViewModel instead of a plain remember { ... }:
// Lost on screen rotation:
@Composable
fun MyScreen() {
val items = remember { mutableStateListOf("a", "b") }
// ...
}
// Survives screen rotation:
@Composable
fun MyScreen(viewModel: MyViewModel = viewModel()) {
// viewModel.items survives because the VM is retained
}
Compose's remember is tied to composition lifetime. Rotation reconfigures the activity, recomposition restarts, remember re-creates the list.
ViewModel is tied to the Activity/Fragment/NavBackStackEntry lifetime, which survives configuration changes. For state that should outlive composition, use a ViewModel.
StateFlow as an alternative
mutableStateListOf is Compose-specific. For a multiplatform or cross-framework codebase:
class MyViewModel : ViewModel() {
private val _items = MutableStateFlow(listOf("Apple", "Banana"))
val items: StateFlow<List<String>> = _items.asStateFlow()
fun addItem(item: String) {
_items.update { it + item }
}
}
StateFlow is from kotlinx.coroutines, works anywhere. Compose reads it via viewModel.items.collectAsState():
val items by viewModel.items.collectAsState()
Trade-offs:
mutableStateListOf— simpler, Compose-native, mutate in place.StateFlow<List<T>>— multiplatform, immutable updates, better for shared business logic.
For Android-only Compose apps, mutableStateListOf is fine. For shared codebases, StateFlow.
Add/remove/update
class TodoViewModel : ViewModel() {
private val _todos = mutableStateListOf<Todo>()
val todos: List<Todo> get() = _todos
fun add(text: String) {
_todos.add(Todo(id = _todos.size, text = text, done = false))
}
fun toggle(id: Int) {
val index = _todos.indexOfFirst { it.id == id }
if (index >= 0) {
_todos[index] = _todos[index].copy(done = !_todos[index].done)
}
}
fun remove(id: Int) {
_todos.removeAll { it.id == id }
}
}
data class Todo(val id: Int, val text: String, val done: Boolean)
Three operations, all reactive. Notice toggle uses _todos[index] = .copy(...) rather than _todos[index].done = ... — the items are immutable data classes, so we replace the slot.
Common mistakes
Using mutableListOf instead of mutableStateListOf. Mutations are invisible. UI doesn't update.
Exposing _items directly. val items: SnapshotStateList<String> = _items lets callers mutate, defeating the encapsulation. Always expose as List<T>.
Mutating items inside the list. _items[0].field = "new" — if items are mutable, this won't trigger recomposition. Use immutable data class items and .copy() to replace.
Forgetting viewModel() import. The free function viewModel() lives in androidx.lifecycle.viewmodel.compose. Add the gradle dep androidx.lifecycle:lifecycle-viewmodel-compose.
Mixing mutableStateOf<List<T>> with mutableStateListOf<T>. mutableStateOf<List<T>> is "a mutable cell holding an immutable list" — replace the whole list to update. mutableStateListOf<T> is "a list itself observable" — mutate in place. Both work, but pick one consistently.
What's next
Episode 7: Get the device ID in Jetpack Compose. Read Settings.Secure.ANDROID_ID from a Composable using LocalContext.
Recap
mutableStateListOf<T> for a reactive list in a ViewModel. Pattern: private val _items = mutableStateListOf(...) + val items: List<T> get() = _items. Mutations (add, remove, _items[i] = ...) trigger recomposition. ViewModel survives config changes; remember doesn't. For multiplatform shared logic, prefer StateFlow<List<T>> and collectAsState() in the UI.
Next episode: getting the device ID.