Movies UI - Updates
Display-screen list cleanup
In our previous example, you could not delete items from display screens. For example, the Rating display screen shows a list of movies with that rating, but you cannot delete the movies from that screen.
We'd like to reuse as much as possible, but our ListScaffold
isn't as flexible as we need.
It manages the entire body content as a List
. For our display screens, we have other content
above the list of children.
But let's not ignore a big issue: When we rotate the screen, there's very little room for the list. If there were more fields at the top, or the user increases the font size, we may lose some field and the list completely!
So we need to take advantage of adding individual items to the LazyColumn
, as follows:
LazyColumn(...) {
item {
// fixed part of UI - could be multiple separate item {...} blocks
}
items(list) { // dynamic part
// card for each item in the list
}
}
We do this as follows:
- Add a
topContent
parameter inList
- Add a
topContent
parameter inListScaffold
- Pass
topContent
fromListScaffold
toList
- Add
topContent
(if it exists) toLazyColumn
when setting up theList
. We'll only passtopContent
in the display screens. The list screens only display the dynamic lists - Because the display screens now have selectable lists, we need to add selection parameters to them
- Pass the selection and other top-bar parameters to the display screen
Note
I'm only linking buttons in this text to the rating display changes. The Actor and Movie display screens all require similar changes.
After making these changes, the entire display screen scrolls. We have reasonable space to look at the list items.
But those list-selection tabs at the bottom are taking up a huge amount of space. It would be nice to hide it when we scroll or when the top-bar is contextual (and we're focusing on selecting items.)
We can do this by explicitly passing in a
LazyListState
to List
.
This state
tracks
the scrolling position of our LazyColumn
, which we can use to determine whether the bottom bar
should be displayed.
We create an instance of LazyListState
and
pass it to the List
. We use remember
to keep the same
instance of LazyListState
as long as ListScaffold
remains in the UI tree.
Warning
Things get a little "interesting" here, so hold on tight...
We want to compute whether we should display the bottom bar based on the position in the
LazyListState
. This computation only matters when position == 0 (show the bottom bar) or
position != 0 (hide the bottom bar). There are many other possible values, but we only
care about zero or not zero.
If we write code like
val showBottomBar = remember(lazyListState) {
lazyListState.firstVisibleItemScrollOffset == 0
}
whenever lazyListState
changes at all, we'll completely recompose ListScaffold
to
trigger that remember
to recompute its value. That will cause a lot of unneeded recompositions
of the top bar as the user scrolls through the list; we really only need the LazyColumn
(inside List
which is inside ListScaffold
) to recompose for the scrolling),
and the bottom bar to recompose when we change from zero to non-zero position.
That key on the remember
is a great thing to reduce computations, but here it's triggering
way too many. What we really need is some way to only recompose the bottom bar on that
zero to non-zero transition.
We can ask the snapshot manager to inform us of such a change by using derivedStateOf
:
val showBottomBar by remember {
derivedStateOf {
lazyListState.firstVisibleItemScrollOffset == 0
}
}
This creates a new state (the result of derivedStateOf
) for the snapshot manager to manage,
and a lambda that the snapshot manager uses to recompute the value of that state
whenever any state in that lambda changes.
Whenever lazyListState
changes, the snapshot manager evaluates
{ lazyListState.firstVisibleItemScrollOffset == 0 }
, setting that as the value for
showBottomBar
, which will only trigger recomposition if its value changes. It's important to note
that the computation is done by the snapshot manager here. Normally we recompute things like this
inside a remember
block triggered by its key changing during a recomposition.
We don't need to evaluate the remember
to do this evaluation,
so no recomposition is required just to compute.
Info
There's a great article at Jetpack Compose — When should I use derivedStateOf? that describes this in much more detail.
Using showBottomBar
in an if
that surrounds the NavigationBar
we can conrtol its visibility.
The result is, when scrolling, is:
But it's a bit abrupt. We can clean this up by adding an
AnimatedVisibility
composable, which results
in a nicer experience:
However - we also want to hide the bottom bar when there are selections. This is another
zero or non-zero type situation, so derivedStateOf
is also a great place to do this.
So we modify showBottomBar
to also check selectedIds
:
val showBottomBar by remember {
derivedStateOf {
lazyListState.firstVisibleItemScrollOffset == 0 && selectedIds.isEmpty()
}
}
But this doesn't work! Selecting items doesn't cause the bottom bar to hide.
Let's think about how things work here...
Whenever compose state is read, the snapshot manager makes a note of where that read occurred, so it will know where to trigger recomposition (or in this case, recalculation of the derived value).
The scroll position is tracked using compose State
values inside LazyListState
.
Because the scroll position is read inside the derivedStateOf
lambda, the snapshot manager
knows it needs to recompute the value when the scroll position changes.
But where is the compose state for selectedIds
read? Inside the derivedStateOf
, we're reading
selectedIds
, which is just a Set<String>
. No State
is read, so the snapshot manager has
nothing on which to base an update.
We could change the way we pass around selectedIds
- when we collect it, we could change
val selectedIds by viewModel.selectedIdsFlow.collectAsStateWithLifecycle(initialValue = emptySet())
to
val selectedIds = viewModel.selectedIdsFlow.collectAsStateWithLifecycle(initialValue = emptySet())
Recall that using by
causes all gets/sets of selectedIds
to delegate to the object after the
by
. When we pass selectedIds
into a composable function, the read happens immediately
before the function call to get the parameter value. The value is then passed into the composable
function. The state is only read in the caller to that function.
Changing the by
to =
changes the type of selectedIds
from Set<String>
to State<Set<String>>
.
If we pass that into a composable function, the state hasn't yet been read. We could then use it
inside the derivedStateOf
and we'd get the expected behavior.
In general, I don't like passing State
to composable functions; I prefer passing the value.
This makes the composable functions easier to test, as you don't need to set up State
to pass in
and then change the values of that State
. You simply pass values to the function.
(We're stuck with state holders like LazyListState
...) If we observe a performance issue because
of the value passing, we can switch to the State
and use it inside
derivedStateOf
and so forth.
In this instance, think about what is happening. When we scroll, we'll get a huge number of changes very quickly, and the extra recompositions could cause some jank. But item selection is a rather slow process with the user clicking to select. They could rapidly click items, but that's not typical behavior.
So, instead of using the State
read to trigger recomputation of the derivedStateOf
based on
selections, we'll fall back to the remember
key to do so.
Our showBottomBar
now looks like:
val showBottomBar by remember(selectedIds) {
derivedStateOf {
lazyListState.firstVisibleItemScrollOffset == 0 && selectedIds.isEmpty()
}
}
and now behaves as expected:
Code Changes
CHANGED: /app/src/main/java/com/androidbyexample/movie/screens/ActorDisplayUi.kt
package com.androidbyexample.movie.screens//import androidx.compose.foundation.layout.Column//import androidx.compose.foundation.layout.padding//import androidx.compose.material3.ExperimentalMaterial3Api//import androidx.compose.material3.Scaffold//import androidx.compose.material3.Text//import androidx.compose.material3.TopAppBarimport androidx.compose.runtime.Composable import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.getValue import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.remember import androidx.compose.runtime.setValue//import androidx.compose.ui.Modifierimport androidx.compose.ui.res.stringResource import com.androidbyexample.movie.R import com.androidbyexample.movie.components.Display import com.androidbyexample.movie.components.Label import com.androidbyexample.movie.repository.ActorWithFilmographyDto import com.androidbyexample.movie.repository.MovieDto import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.withContext//@OptIn(ExperimentalMaterial3Api::class)@Composable fun ActorDisplayUi( id: String, fetchActor: suspend (String) -> ActorWithFilmographyDto, onMovieClicked: (MovieDto) -> Unit, selectedIds: Set<String>, onSelectionToggle: (id: String) -> Unit, onClearSelections: () -> Unit, onDeleteSelectedMovies: () -> Unit, currentScreen: Screen, onSelectListScreen: (Screen) -> Unit, onResetDatabase: () -> Unit,) { var actorWithFilmography by remember { mutableStateOf<ActorWithFilmographyDto?>(null) } LaunchedEffect(key1 = id) { withContext(Dispatchers.IO) { actorWithFilmography = fetchActor(id) } }// Scaffold(// topBar = {// TopAppBar(// title = {// Text(text = actorWithFilmography?.actor?.name ?: stringResource(R.string.loading))// }// )// }// ) { paddingValues ->// actorWithFilmography?.let { actorWithFilmography ->// Column(// modifier = Modifier// .padding(paddingValues)// ) {ListScaffold( titleId = R.string.rating, items = actorWithFilmography?.filmography?.sortedBy { it.movie.title } ?: emptyList(), onItemClicked = { onMovieClicked(it.movie) }, selectedIds = selectedIds, onSelectionToggle = onSelectionToggle, onClearSelections = onClearSelections, onDeleteSelectedItems = onDeleteSelectedMovies, currentScreen = currentScreen, onSelectListScreen = onSelectListScreen, onResetDatabase = onResetDatabase, itemContent = { role -> Display(text = role.movie.title) }, topContent = { Label(textId = R.string.name)// Display(text = actorWithFilmography.actor.name)Display(text = actorWithFilmography?.actor?.name ?: "") Label( text = stringResource( id = R.string.movies_starring,// actorWithFilmography.actor.nameactorWithFilmography?.actor?.name ?: "" ) )// List(// items = actorWithFilmography.filmography.sortedBy { it.movie.title },// onItemClicked = { onMovieClicked(it.movie) },// selectedIds = emptySet(),// onSelectionToggle = {},// onClearSelections = {},// modifier = Modifier.weight(1f)// ) { role ->// Display(text = role.movie.title)// }// }// }} )}
CHANGED: /app/src/main/java/com/androidbyexample/movie/screens/ActorListUi.kt
package com.androidbyexample.movie.screens import androidx.compose.foundation.clickable import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.padding import androidx.compose.material.icons.Icons import androidx.compose.material.icons.filled.Person//import androidx.compose.material.icons.filled.Starimport androidx.compose.material3.Icon 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.movie.R import com.androidbyexample.movie.components.Display import com.androidbyexample.movie.repository.ActorDto @Composable fun ActorListUi( actors: List<ActorDto>, onActorClicked: (ActorDto) -> Unit, selectedIds: Set<String>, onSelectionToggle: (id: String) -> Unit, onClearSelections: () -> Unit, onDeleteSelectedActors: () -> Unit, currentScreen: Screen, onSelectListScreen: (Screen) -> Unit, onResetDatabase: () -> Unit, ) { ListScaffold( titleId = R.string.actors, items = actors, onItemClicked = onActorClicked, selectedIds = selectedIds, onSelectionToggle = onSelectionToggle, onClearSelections = onClearSelections, onDeleteSelectedItems = onDeleteSelectedActors, currentScreen = currentScreen, onSelectListScreen = onSelectListScreen, onResetDatabase = onResetDatabase ) { actor -> Row( verticalAlignment = Alignment.CenterVertically, modifier = Modifier.padding(8.dp) ) { Icon( imageVector = Icons.Default.Person, contentDescription = stringResource(id = R.string.actor), modifier = Modifier.clickable { onSelectionToggle(actor.id) } ) Display(text = actor.name) } } }
CHANGED: /app/src/main/java/com/androidbyexample/movie/screens/List.kt
package com.androidbyexample.movie.screens import androidx.activity.compose.BackHandler import androidx.compose.foundation.ExperimentalFoundationApi import androidx.compose.foundation.combinedClickable import androidx.compose.foundation.layout.ColumnScope import androidx.compose.foundation.layout.padding import androidx.compose.foundation.lazy.LazyColumn import androidx.compose.foundation.lazy.LazyListStateimport androidx.compose.foundation.lazy.items import androidx.compose.material3.Card import androidx.compose.material3.CardDefaults import androidx.compose.material3.MaterialTheme import androidx.compose.material3.contentColorFor import androidx.compose.runtime.Composable import androidx.compose.ui.Modifier import androidx.compose.ui.unit.dp import com.androidbyexample.movie.repository.HasId @OptIn(ExperimentalFoundationApi::class) @Composable fun <T: HasId> List(state: LazyListState,items: List<T>, onItemClicked: (T) -> Unit, selectedIds: Set<String>, onSelectionToggle: (id: String) -> Unit, onClearSelections: () -> Unit, modifier: Modifier = Modifier,topContent: (@Composable () -> Unit)? = null,itemContent: @Composable ColumnScope.(T) -> Unit, ) { LazyColumn(state = state,modifier = modifier ) {topContent?.let { item { topContent() } }items( items = items, key = { it.id }, ) { item -> val containerColor = if (item.id in selectedIds) { MaterialTheme.colorScheme.secondary } else { MaterialTheme.colorScheme.surface } val contentColor = MaterialTheme.colorScheme.contentColorFor(containerColor) if (selectedIds.isNotEmpty()) { BackHandler { onClearSelections() } } Card( elevation = CardDefaults.cardElevation( defaultElevation = 8.dp, ), colors = CardDefaults.cardColors( containerColor = containerColor, contentColor = contentColor, ), modifier = Modifier .padding(8.dp) .combinedClickable( onClick = { if (selectedIds.isEmpty()) { onItemClicked(item) } else { onSelectionToggle(item.id) } }, onLongClick = { onSelectionToggle(item.id) }, ) ) { itemContent(item) } } } }
CHANGED: /app/src/main/java/com/androidbyexample/movie/screens/ListScaffold.kt
package com.androidbyexample.movie.screens import androidx.annotation.StringRes import androidx.compose.animation.AnimatedVisibilityimport androidx.compose.animation.expandVerticallyimport androidx.compose.animation.fadeInimport androidx.compose.animation.fadeOutimport androidx.compose.animation.shrinkVerticallyimport androidx.compose.foundation.clickable import androidx.compose.foundation.layout.ColumnScope import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.padding import androidx.compose.foundation.lazy.LazyListStateimport androidx.compose.material.icons.Icons import androidx.compose.material.icons.filled.ArrowBack import androidx.compose.material.icons.filled.Delete import androidx.compose.material.icons.filled.Movie import androidx.compose.material.icons.filled.Person import androidx.compose.material.icons.filled.Refresh import androidx.compose.material.icons.filled.Star import androidx.compose.material3.ExperimentalMaterial3Api import androidx.compose.material3.Icon import androidx.compose.material3.IconButton import androidx.compose.material3.NavigationBar import androidx.compose.material3.Scaffold import androidx.compose.material3.Text import androidx.compose.material3.TopAppBar import androidx.compose.runtime.Composable import androidx.compose.runtime.derivedStateOfimport androidx.compose.runtime.getValueimport androidx.compose.runtime.rememberimport androidx.compose.ui.Modifier import androidx.compose.ui.res.stringResource import androidx.compose.ui.unit.dp import com.androidbyexample.movie.R import com.androidbyexample.movie.components.ScreenSelectButton import com.androidbyexample.movie.repository.HasId @OptIn(ExperimentalMaterial3Api::class) @Composable fun <T: HasId> ListScaffold( @StringRes titleId: Int, items: List<T>, onItemClicked: (T) -> Unit, selectedIds: Set<String>, onSelectionToggle: (id: String) -> Unit, onClearSelections: () -> Unit, onDeleteSelectedItems: () -> Unit, currentScreen: Screen, onSelectListScreen: (Screen) -> Unit, onResetDatabase: () -> Unit,topContent: (@Composable () -> Unit)? = null,itemContent: @Composable ColumnScope.(T) -> Unit, ) {val lazyListState = remember { LazyListState() }val showBottomBar by remember(selectedIds) { derivedStateOf { lazyListState.firstVisibleItemScrollOffset == 0 && selectedIds.isEmpty() } }Scaffold( topBar = { if (selectedIds.isEmpty()) { TopAppBar( title = { Text(text = stringResource(titleId)) }, actions = { IconButton(onClick = onResetDatabase) { Icon( imageVector = Icons.Default.Refresh, contentDescription = stringResource(R.string.reset_database) ) } } ) } else { TopAppBar( navigationIcon = { Icon( imageVector = Icons.Default.ArrowBack, contentDescription = stringResource(R.string.clear_selections), modifier = Modifier.clickable(onClick = onClearSelections), ) }, title = { Text(text = selectedIds.size.toString(), modifier = Modifier.padding(8.dp)) }, actions = { IconButton(onClick = onDeleteSelectedItems) { Icon( imageVector = Icons.Default.Delete, contentDescription = stringResource(R.string.delete_selected_items) ) } }, ) } }, bottomBar = {AnimatedVisibility( visible = showBottomBar, enter = expandVertically() + fadeIn(), exit = shrinkVertically() + fadeOut(), ) {NavigationBar { ScreenSelectButton( targetScreen = RatingList, imageVector = Icons.Default.Star, labelId = R.string.ratings, currentScreen = currentScreen, onSelectListScreen = onSelectListScreen ) ScreenSelectButton( targetScreen = MovieList, imageVector = Icons.Default.Movie, labelId = R.string.movies, currentScreen = currentScreen, onSelectListScreen = onSelectListScreen ) ScreenSelectButton( targetScreen = ActorList, imageVector = Icons.Default.Person, labelId = R.string.actors, currentScreen = currentScreen, onSelectListScreen = onSelectListScreen ) } } }, modifier = Modifier.fillMaxSize() ) { paddingValues -> List(state = lazyListState,items = items, onItemClicked = onItemClicked, selectedIds = selectedIds, onSelectionToggle = onSelectionToggle, onClearSelections = onClearSelections,topContent = topContent,modifier = Modifier .padding(paddingValues) .fillMaxSize(), itemContent = itemContent, ) } }
CHANGED: /app/src/main/java/com/androidbyexample/movie/screens/MovieDisplayUi.kt
package com.androidbyexample.movie.screens//import androidx.compose.foundation.layout.Column//import androidx.compose.foundation.layout.fillMaxSize//import androidx.compose.foundation.layout.padding//import androidx.compose.foundation.rememberScrollState//import androidx.compose.foundation.verticalScroll//import androidx.compose.material3.ExperimentalMaterial3Api//import androidx.compose.material3.Scaffold//import androidx.compose.material3.Text//import androidx.compose.material3.TopAppBarimport androidx.compose.runtime.Composable import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.getValue import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.remember import androidx.compose.runtime.setValue//import androidx.compose.ui.Modifierimport androidx.compose.ui.res.stringResource import com.androidbyexample.movie.R import com.androidbyexample.movie.components.Display import com.androidbyexample.movie.components.Label import com.androidbyexample.movie.repository.MovieWithCastDto import com.androidbyexample.movie.repository.RoleWithActorDto import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.withContext//@OptIn(ExperimentalMaterial3Api::class)@Composable fun MovieDisplayUi( id: String, fetchMovie: suspend (String) -> MovieWithCastDto, onActorClicked: (RoleWithActorDto) -> Unit, selectedIds: Set<String>, onSelectionToggle: (id: String) -> Unit, onClearSelections: () -> Unit, onDeleteSelectedMovies: () -> Unit, currentScreen: Screen, onSelectListScreen: (Screen) -> Unit, onResetDatabase: () -> Unit,) { var movieWithCast by remember { mutableStateOf<MovieWithCastDto?>(null) } LaunchedEffect(key1 = id) { withContext(Dispatchers.IO) { movieWithCast = fetchMovie(id) } }// Scaffold(// topBar = {// TopAppBar(// title = {// Text(text = movieWithCast?.movie?.title ?: stringResource(R.string.loading))// }// )// }// ) { paddingValues ->// movieWithCast?.let { movieWithCast ->// Column(// modifier = Modifier// .padding(paddingValues)// ) {// Label(textId = R.string.title)// Display(text = movieWithCast.movie.title)// Label(textId = R.string.description)// Display(text = movieWithCast.movie.description)// Label(textId = R.string.cast)// List(// items = movieWithCast.cast.sortedBy { it.orderInCredits },ListScaffold( titleId = R.string.rating, items = movieWithCast?.cast?.sortedBy { it.orderInCredits } ?: emptyList(), onItemClicked = onActorClicked,// selectedIds = emptySet(),// onSelectionToggle = {},// onClearSelections = {},// modifier = Modifier.weight(1f)// ) { role ->selectedIds = selectedIds, onSelectionToggle = onSelectionToggle, onClearSelections = onClearSelections, onDeleteSelectedItems = onDeleteSelectedMovies, currentScreen = currentScreen, onSelectListScreen = onSelectListScreen, onResetDatabase = onResetDatabase, itemContent = { role -> Display( text = stringResource( R.string.cast_entry, role.character, role.actor.name, ) ) }, topContent = { Label(textId = R.string.title) Display(text = movieWithCast?.movie?.title ?: "") Label(textId = R.string.description) Display(text = movieWithCast?.movie?.description ?: "") Label(textId = R.string.cast) }// }// }// })}
CHANGED: /app/src/main/java/com/androidbyexample/movie/screens/RatingDisplayUi.kt
package com.androidbyexample.movie.screens//import androidx.compose.foundation.layout.Column//import androidx.compose.foundation.layout.padding//import androidx.compose.material3.ExperimentalMaterial3Api//import androidx.compose.material3.Scaffold//import androidx.compose.material3.Text//import androidx.compose.material3.TopAppBarimport androidx.compose.runtime.Composable import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.getValue import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.remember import androidx.compose.runtime.setValue//import androidx.compose.ui.Modifierimport androidx.compose.ui.res.stringResource import com.androidbyexample.movie.R import com.androidbyexample.movie.components.Display import com.androidbyexample.movie.components.Label import com.androidbyexample.movie.repository.MovieDto import com.androidbyexample.movie.repository.RatingWithMoviesDto import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.withContext//@OptIn(ExperimentalMaterial3Api::class)@Composable fun RatingDisplayUi( id: String, fetchRating: suspend (String) -> RatingWithMoviesDto, onMovieClicked: (MovieDto) -> Unit,selectedIds: Set<String>, onSelectionToggle: (id: String) -> Unit, onClearSelections: () -> Unit, onDeleteSelectedMovies: () -> Unit,currentScreen: Screen, onSelectListScreen: (Screen) -> Unit, onResetDatabase: () -> Unit,) { var ratingWithMovies by remember { mutableStateOf<RatingWithMoviesDto?>(null) } LaunchedEffect(key1 = id) { withContext(Dispatchers.IO) { ratingWithMovies = fetchRating(id) } }// Scaffold(// topBar = {// TopAppBar(// title = {// Text(text = ratingWithMovies?.rating?.name ?: stringResource(R.string.loading))// }ListScaffold( titleId = R.string.rating, items = ratingWithMovies?.movies?.sortedBy { it.title } ?: emptyList(), onItemClicked = onMovieClicked, selectedIds = selectedIds, onSelectionToggle = onSelectionToggle, onClearSelections = onClearSelections, onDeleteSelectedItems = onDeleteSelectedMovies, currentScreen = currentScreen, onSelectListScreen = onSelectListScreen, onResetDatabase = onResetDatabase, itemContent = { movie -> Display( text = movie.title )// }// ) { paddingValues ->}, topContent = { ratingWithMovies?.let { ratingWithMovies ->// Column(// modifier = Modifier// .padding(paddingValues)// ) {Label(textId = R.string.name) Display(text = ratingWithMovies.rating.name) Label(textId = R.string.description) Display(text = ratingWithMovies.rating.description) Label( text = stringResource( id = R.string.movies_rated, ratingWithMovies.rating.name ) )// List(// items = ratingWithMovies.movies.sortedBy { it.title },// onItemClicked = onMovieClicked,// selectedIds = emptySet(),// onSelectionToggle = {},// onClearSelections = {},// modifier = Modifier.weight(1f)// ) { movie ->// Display(// text = movie.title} } )// }// }// }// }}
CHANGED: /app/src/main/java/com/androidbyexample/movie/screens/Ui.kt
package com.androidbyexample.movie.screens import androidx.activity.compose.BackHandler import androidx.compose.runtime.Composable import androidx.compose.runtime.getValue import androidx.compose.runtime.rememberCoroutineScope import androidx.lifecycle.compose.collectAsStateWithLifecycle import com.androidbyexample.movie.MovieViewModel import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.launch @Composable fun Ui( viewModel: MovieViewModel, onExit: () -> Unit, ) { BackHandler { viewModel.popScreen() } val scope = rememberCoroutineScope() when (val screen = viewModel.currentScreen) { null -> onExit() is MovieDisplay -> { val selectedIds by viewModel.selectedIdsFlow.collectAsStateWithLifecycle(initialValue = emptySet()) MovieDisplayUi( id = screen.id, fetchMovie = viewModel::getMovieWithCast,// onActorClicked = { viewModel.pushScreen(ActorDisplay(it.actor.id)) }onActorClicked = { viewModel.pushScreen(ActorDisplay(it.actor.id)) }, selectedIds = selectedIds, onClearSelections = viewModel::clearSelectedIds, onSelectionToggle = viewModel::toggleSelection, onDeleteSelectedMovies = viewModel::deleteSelectedMovies, currentScreen = screen, onSelectListScreen = viewModel::setScreen, onResetDatabase = { scope.launch(Dispatchers.IO) { viewModel.resetDatabase() } } ) } is ActorDisplay -> { val selectedIds by viewModel.selectedIdsFlow.collectAsStateWithLifecycle(initialValue = emptySet()) ActorDisplayUi( id = screen.id, fetchActor = viewModel::getActorWithFilmography,// onMovieClicked = { viewModel.pushScreen(MovieDisplay(it.id)) }onMovieClicked = { viewModel.pushScreen(MovieDisplay(it.id)) }, selectedIds = selectedIds, onClearSelections = viewModel::clearSelectedIds, onSelectionToggle = viewModel::toggleSelection, onDeleteSelectedMovies = viewModel::deleteSelectedMovies, currentScreen = screen, onSelectListScreen = viewModel::setScreen, onResetDatabase = { scope.launch(Dispatchers.IO) { viewModel.resetDatabase() } } ) } is RatingDisplay -> { val selectedIds by viewModel.selectedIdsFlow.collectAsStateWithLifecycle(initialValue = emptySet()) RatingDisplayUi( id = screen.id, fetchRating = viewModel::getRatingWithMovies,// onMovieClicked = { viewModel.pushScreen(MovieDisplay(it.id)) }onMovieClicked = { viewModel.pushScreen(MovieDisplay(it.id)) },selectedIds = selectedIds, onClearSelections = viewModel::clearSelectedIds, onSelectionToggle = viewModel::toggleSelection, onDeleteSelectedMovies = viewModel::deleteSelectedMovies, currentScreen = screen, onSelectListScreen = viewModel::setScreen, onResetDatabase = { scope.launch(Dispatchers.IO) { viewModel.resetDatabase() } }) } MovieList -> { val movies by viewModel.moviesFlow.collectAsStateWithLifecycle(initialValue = emptyList()) val selectedIds by viewModel.selectedIdsFlow.collectAsStateWithLifecycle(initialValue = emptySet()) MovieListUi( movies = movies, onMovieClicked = { movie -> viewModel.pushScreen(MovieDisplay(movie.id)) }, selectedIds = selectedIds, onClearSelections = viewModel::clearSelectedIds, onSelectionToggle = viewModel::toggleSelection, onDeleteSelectedMovies = viewModel::deleteSelectedMovies, currentScreen = screen, onSelectListScreen = viewModel::setScreen, onResetDatabase = { scope.launch(Dispatchers.IO) { viewModel.resetDatabase() } } ) } ActorList -> { val actors by viewModel.actorsFlow.collectAsStateWithLifecycle(initialValue = emptyList()) val selectedIds by viewModel.selectedIdsFlow.collectAsStateWithLifecycle(initialValue = emptySet()) ActorListUi( actors = actors, onActorClicked = { actor -> viewModel.pushScreen(ActorDisplay(actor.id)) }, selectedIds = selectedIds, onClearSelections = viewModel::clearSelectedIds, onSelectionToggle = viewModel::toggleSelection, onDeleteSelectedActors = viewModel::deleteSelectedActors, currentScreen = screen, onSelectListScreen = viewModel::setScreen, onResetDatabase = { scope.launch(Dispatchers.IO) { viewModel.resetDatabase() } } ) } RatingList -> { val ratings by viewModel.ratingsFlow.collectAsStateWithLifecycle(initialValue = emptyList()) val selectedIds by viewModel.selectedIdsFlow.collectAsStateWithLifecycle(initialValue = emptySet()) RatingListUi( ratings = ratings, onRatingClicked = { rating -> viewModel.pushScreen(RatingDisplay(rating.id)) }, selectedIds = selectedIds, onClearSelections = viewModel::clearSelectedIds, onSelectionToggle = viewModel::toggleSelection, onDeleteSelectedRatings = viewModel::deleteSelectedRatings, currentScreen = screen, onSelectListScreen = viewModel::setScreen, onResetDatabase = { scope.launch(Dispatchers.IO) { viewModel.resetDatabase() } } ) } } }