Part of Github Copilot with Kotlin

Jetpack compose: View Model and mutablestatelistof

Sandy LaneSandy Lane

Video: Jetpack compose: View Model and mutablestatelistof by Taught by Celeste AI - AI Coding Coach

Take the quiz on the full lesson page
Test what you've read · interactive walkthrough

Jetpack Compose: ViewModel with mutableStateListOf

ViewModel to hold business logic and survive config changes. mutableStateListOf<T> for a reactive list — Compose recomposes whenever you add/remove/mutate it. Expose as read-only List<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.

Ready? Take the quiz on the full lesson page →
Test what you've learned. Watch the lesson and try the interactive quiz on the same page.