Movies UI - Lists

Selection Tracking

Now let's set up item selection. We'll use the common approach of

  • Tapping the icon or long-pressing anywhere in the row toggles the selection
  • Tapping in the row anywhere except the icon:
    • if any items are selected, toggle this item
    • if no items are selected, trigger navigation

First, we need to track the selections somewhere. Selections are data that are typically not kept between runs of an application, so there's no need to store them in the database.

But we usually want selections to persist across configuation changes (such as device rotation), so the View Model is a good choice to hold them.

We're going to use a Kotlin Flow to track the set of selected ids. MutableStateFlow holds onto a single emitted item (in this case, a Set of selected ids) that can be collected by the UI and its value accessed/set via its value property.

We're defining two Flows here:

  • _selectedIdsFlow - this is private and can only be accessed by the view model. We don't want the UI to be able to directly modify things in the view model. The UI should call functions that give us more control over how updates are made.

  • selectedIdsFlow - we expose the MutableStateFlow as a non-mutable Flow by calling asStateFlow() on _selectedIdsFlow. This property is public and can be collected from the UI.

You may ask "why not use a MutableState? You could do that here instead of creating a Flow. It would look similar to our currentScreen property, which we added before we started talking about Flows.

currentScreen is exposed to the UI but only read-only. Using private set makes the setter function private, only accessible to the view model.

The currentScreen approach code ends up being simpler as well - there's no need to collect the Flow in the UI; you just reference selectedIds.

In fact, I;d recommend converting currentScreen to use a Flow (but will leave it here for comparison).

So why use a Flow instead of a MutableState?

Part of this is "purity" - using State in a view model leaks some of the way we're implementing our UI into the view model.

Note

Technically, State is part of compose-runtime, which is responsible for creating/managing trees and state. It isn't a "UI thing" and could be used for data management at any level.

Unfortunately, there are some close ties with compose-ui, as well as the perception that State is a "UI thing". Thus, I recommend avoiding its use in view models (or further down, such as the repository or data modules).

The other part of this is potential reuse. If you use State inside your view model, your view model will only work where Compose can run. If you use Flows, the view model will work wherever Kotlin can run.

This opens up the ability to use the same view model logic on Android, iOS, desktop app, web app, or even command-line ui. (My hope is that over time, Kotlin logic will be used across many platforms. Kotlin Multiplatform is getting there...)

To keep the purity, we'll use Flows to expose data from the view model from now on. (I also hope to create a Kotlin Multiplatform course soon so we can explore this more!)

The last thing we need to do in the view model is add some selection management functions. This will allow the UI to ask us to clear the selections and toggle a single selection.

Note

It's also a good idea to clear the selections whenever the user changes screens. We can do this by adding a call to clearSelectedIds() in the setter for our screenStack. Be sure to do this before the actual screen changes.

Code Changes

CHANGED: /app/src/main/java/com/androidbyexample/movie/MovieViewModel.kt
package com.androidbyexample.movie

import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.setValue
import androidx.lifecycle.ViewModel
import androidx.lifecycle.ViewModelProvider
import androidx.lifecycle.viewmodel.CreationExtras
import com.androidbyexample.movie.repository.MovieDatabaseRepository
import com.androidbyexample.movie.repository.MovieRepository
import com.androidbyexample.movie.screens.MovieList
import com.androidbyexample.movie.screens.Screen
import kotlinx.coroutines.flow.Flowimport kotlinx.coroutines.flow.MutableStateFlowimport kotlinx.coroutines.flow.asStateFlow
class MovieViewModel(
    private val repository: MovieRepository,
): ViewModel(), MovieRepository by repository {
private val _selectedIdsFlow = MutableStateFlow<Set<String>>(emptySet()) val selectedIdsFlow: Flow<Set<String>> = _selectedIdsFlow.asStateFlow()
fun clearSelectedIds() { _selectedIdsFlow.value = emptySet() } fun toggleSelection(id: String) { if (id in _selectedIdsFlow.value) { _selectedIdsFlow.value -= id } else { _selectedIdsFlow.value += id } }
private var screenStack = listOf<Screen>(MovieList) set(value) { field = value
clearSelectedIds()
currentScreen = value.lastOrNull() }
// NOTE: We're keep this as a Compose State for comparison. // You can use Compose state to expose anything from the view model, // but our example will be using Flow from now on to demonstrate how // the view model can be used without Compose, perhaps for other // platforms such as iOS, desktop, web or command line var currentScreen by mutableStateOf<Screen?>(MovieList) private set
fun pushScreen(screen: Screen) { screenStack = screenStack + screen } fun popScreen() { screenStack = screenStack.dropLast(1) } companion object { val Factory: ViewModelProvider.Factory = object: ViewModelProvider.Factory { @Suppress("UNCHECKED_CAST") override fun <T : ViewModel> create( modelClass: Class<T>, extras: CreationExtras ): T { // Get the Application object from extras val application = checkNotNull(extras[ViewModelProvider.AndroidViewModelFactory.APPLICATION_KEY]) return MovieViewModel( MovieDatabaseRepository.create(application) ) as T } } } }