Movies UI - Lists
Selections in the UI
Let's integrate selections into the UI.
First, we add parameters to our MovieListUi
composable function. These parameters give the
function
selectedIds
- the set of selections so we know what to highlightonSelectionToggle
- allows the function to request to toggle a selectiononClearSelections
- allows the function to request all selections be cleared
We choose the color of each card based on its selection status. (Note that contentColorFor
will
only work if the color passed in is defined in the theme. In this case, we're using secondary
and surface
colors from the theme so it'll work.)
We tell the Card
which colors to use for its background, containerColor
and foreground,
contentColor
. The contentColor will be used for any nested text or icons.
show in full file app/src/main/java/com/androidbyexample/compose/movies/screens/MovieList.kt
// ...
//@OptIn(ExperimentalMaterial3Api::class) // for TopAppBar
@OptIn(ExperimentalMaterial3Api::class, ExperimentalFoundationApi::class) // for TopAppBar
@Composable
fun MovieListUi(
// ...
modifier: Modifier = Modifier,
onMovieClicked: (MovieDto) -> Unit,
selectedIds: Set<String>,
onSelectionToggle: (id: String) -> Unit,
onClearSelections: () -> Unit,
onResetDatabase: () -> Unit,
) {
Scaffold(
// ...
) { innerPadding ->
LazyColumn (
// ...
) {
items(
// ...
key = { it.id }
) { movie ->
val containerColor =
if (movie.id in selectedIds) {
MaterialTheme.colorScheme.secondary
} else {
MaterialTheme.colorScheme.surface
}
val contentColor = MaterialTheme.colorScheme.contentColorFor(containerColor)
Card (
// ...
defaultElevation = 8.dp,
),
// onClick = {
// onMovieClicked(movie)
// },
colors = CardDefaults.cardColors(
containerColor = containerColor,
contentColor = contentColor,
),
modifier = Modifier.padding(8.dp)
.combinedClickable(
// ...
) {
// ...
}
}
}
}
}
We need to change the way clicks are handled to match our strategy. We'll do this for the
Icon
- any clicks toggle the selectionCard
- any long-clicks toggle the selection
- any normal clicks
- toggle the selection (if anything was selected), or
- navigate to the movie (if nothing was selected)
We currently have an onClick
defined on the Card
, but because we want to handle both
long and normal clicks, we need to switch to a combinedClickable
modifier.
We add a clickable
modifier to the Icon
to finish our click handling.
show in full file app/src/main/java/com/androidbyexample/compose/movies/screens/MovieList.kt
// ...
//@OptIn(ExperimentalMaterial3Api::class) // for TopAppBar
@OptIn(ExperimentalMaterial3Api::class, ExperimentalFoundationApi::class) // for TopAppBar
@Composable
fun MovieListUi(
// ...
modifier: Modifier = Modifier,
onMovieClicked: (MovieDto) -> Unit,
selectedIds: Set<String>,
onSelectionToggle: (id: String) -> Unit,
onClearSelections: () -> Unit,
onResetDatabase: () -> Unit,
) {
Scaffold(
// ...
) { innerPadding ->
LazyColumn (
// ...
) {
items(
// ...
) { movie ->
// ...
Card (
// ...
),
modifier = Modifier.padding(8.dp)
.combinedClickable(
onClick = {
if (selectedIds.isEmpty()) {
onMovieClicked(movie)
} else {
onSelectionToggle(movie.id)
}
},
onLongClick = {
onSelectionToggle(movie.id)
},
)
) {
Row (
// ...
) {
Icon(
// ...
// contentDescription = stringResource(id = R.string.movie)
contentDescription = stringResource(id = R.string.movie),
modifier = Modifier.clickable {
onSelectionToggle(movie.id)
}
)
Display(text = movie.title)
}
}
}
}
}
}
Finally, we collect the selection from the view model and wire up the new
parameters in the Ui
function.
show in full file app/src/main/java/com/androidbyexample/compose/movies/screens/Ui.kt
// ...
@Composable
fun Ui(
// ...
) {
// ...
when (val screen = viewModel.currentScreen) {
// ...
MovieList -> {
// ...
)
val selectedIds by viewModel
.selectedIdsFlow
.collectAsStateWithLifecycle(initialValue = emptySet())
MovieListUi(
movies = movies,
modifier = modifier,
selectedIds = selectedIds,
onClearSelections = viewModel::clearSelectedIds,
onSelectionToggle = viewModel::toggleSelection,
onResetDatabase = {
scope.launch (Dispatchers.IO) {
// ...
)
}
}
}
This gives us a movie list that allows us to select movies!
All code changes
CHANGED: app/src/main/java/com/androidbyexample/compose/movies/screens/MovieList.kt
package com.androidbyexample.compose.movies.screens
import androidx.compose.foundation.ExperimentalFoundationApi
import androidx.compose.foundation.clickable
import androidx.compose.foundation.combinedClickable
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.lazy.LazyColumn
import androidx.compose.foundation.lazy.items
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.Refresh
import androidx.compose.material.icons.filled.Star
import androidx.compose.material3.Card
import androidx.compose.material3.CardDefaults
import androidx.compose.material3.ExperimentalMaterial3Api
import androidx.compose.material3.Icon
import androidx.compose.material3.IconButton
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Scaffold
import androidx.compose.material3.Text
import androidx.compose.material3.TopAppBar
import androidx.compose.material3.contentColorFor
import androidx.compose.runtime.Composable
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.unit.dp
import com.androidbyexample.compose.movies.R
import com.androidbyexample.compose.movies.components.Display
import com.androidbyexample.compose.movies.repository.MovieDto
//@OptIn(ExperimentalMaterial3Api::class) // for TopAppBar
@OptIn(ExperimentalMaterial3Api::class, ExperimentalFoundationApi::class) // for TopAppBar
@Composable
fun MovieListUi(
movies: List<MovieDto>,
modifier: Modifier = Modifier,
onMovieClicked: (MovieDto) -> Unit,
selectedIds: Set<String>,
onSelectionToggle: (id: String) -> Unit,
onClearSelections: () -> Unit,
onResetDatabase: () -> Unit,
) {
Scaffold(
topBar = {
TopAppBar(
title = {
Text(text = stringResource(R.string.movies))
},
actions = {
IconButton (onClick = onResetDatabase) {
Icon(
imageVector = Icons.Default.Refresh,
contentDescription = stringResource(R.string.reset_database)
)
}
}
)
},
modifier = modifier,
) { innerPadding ->
LazyColumn (
modifier = Modifier
.padding(innerPadding)
.fillMaxSize()
) {
items(
items = movies,
key = { it.id }
) { movie ->
val containerColor =
if (movie.id in selectedIds) {
MaterialTheme.colorScheme.secondary
} else {
MaterialTheme.colorScheme.surface
}
val contentColor = MaterialTheme.colorScheme.contentColorFor(containerColor)
Card (
elevation = CardDefaults.cardElevation(
defaultElevation = 8.dp,
),
// onClick = {
// onMovieClicked(movie)
// },
colors = CardDefaults.cardColors(
containerColor = containerColor,
contentColor = contentColor,
),
modifier = Modifier.padding(8.dp)
.combinedClickable(
onClick = {
if (selectedIds.isEmpty()) {
onMovieClicked(movie)
} else {
onSelectionToggle(movie.id)
}
},
onLongClick = {
onSelectionToggle(movie.id)
},
)
) {
Row (
verticalAlignment = Alignment.CenterVertically,
modifier = Modifier.padding(8.dp)
) {
Icon(
imageVector = Icons.Default.Star,
// contentDescription = stringResource(id = R.string.movie)
contentDescription = stringResource(id = R.string.movie),
modifier = Modifier.clickable {
onSelectionToggle(movie.id)
}
)
Display(text = movie.title)
}
}
}
}
}
}
CHANGED: app/src/main/java/com/androidbyexample/compose/movies/screens/Ui.kt
package com.androidbyexample.compose.movies.screens
import androidx.activity.compose.BackHandler
import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue
import androidx.compose.runtime.rememberCoroutineScope
import androidx.compose.ui.Modifier
import androidx.lifecycle.compose.collectAsStateWithLifecycle
import com.androidbyexample.compose.movies.MovieViewModel
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch
@Composable
fun Ui(
viewModel: MovieViewModel,
modifier: Modifier = Modifier,
onExit: () -> Unit,
) {
BackHandler {
viewModel.popScreen()
}
val scope = rememberCoroutineScope()
when (val screen = viewModel.currentScreen) {
null -> onExit()
is MovieDisplay -> {
MovieDisplayUi(
id = screen.id,
fetchMovie = viewModel::getMovieWithCast,
modifier = modifier,
)
}
MovieList -> {
val movies by viewModel.moviesFlow.collectAsStateWithLifecycle(
initialValue = emptyList()
)
val selectedIds by viewModel
.selectedIdsFlow
.collectAsStateWithLifecycle(initialValue = emptySet())
MovieListUi(
movies = movies,
modifier = modifier,
selectedIds = selectedIds,
onClearSelections = viewModel::clearSelectedIds,
onSelectionToggle = viewModel::toggleSelection,
onResetDatabase = {
scope.launch (Dispatchers.IO) {
viewModel.resetDatabase()
}
},
onMovieClicked = { movie ->
viewModel.pushScreen(MovieDisplay(movie.id))
}
)
}
}
}