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 Flow
s 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 theMutableStateFlow
as a non-mutableFlow
by callingasStateFlow()
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 Flow
s.
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 Flow
s, 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 Flow
s 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 = valueclearSelectedIds()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 setfun 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 } } } }