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!

Not enough room for the list!

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:

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.

Space is a little better

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:

Bottom bar hiding on scroll

But it's a bit abrupt. We can clean this up by adding an AnimatedVisibility composable, which results in a nicer experience:

Bottom bar hiding when scrolling with animation

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:

Bottom bar hiding on selection

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.name                    actorWithFilmography?.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() } } ) } } }