Skip to content

Movies UI - Lists

Refactor time!

Our app bakes the list support in with the Movie UI. We can make it much more reusable!

We're going to have two types of lists in the application:

  • Top-level lists of all movies, actors and ratings
  • Nested lists, such as actors starring in a movie (on the movie's display screen)

Generic List Data

To create consistent list support, we need to separate the LazyColumn and Scaffold from the MovieListUi. We want to keep all selection management, but make it more generic.

But there's a problem. If we make a generic List composable, something like

fun <T> ListScaffold(
  items: List<T>,
  ...
) {
  ...
}

we have several spots that access the item's id. When the item type was explicitly MovieDto, we knew it had an id, but if the item type is a generic parameter T, we can no longer make that assumption.

To fix this, we create a HasId interface in the repository module. (You could create it in the data module, but we don't have a need for it that low.)

interface HasId {
    val id: String
}

The ids defined by our entities are the actual unique IDs in the database. These are simple to define:

show in full file repository/src/main/java/com/androidbyexample/movies/repository/MovieDto.kt
// ...

data class MovieDto(
//  val id: String,
    override val id: String,
    val title: String,
    val description: String,
    val ratingId: String,
//)
): HasId

internal fun MovieEntity.toDto() =
    // ...
show in full file repository/src/main/java/com/androidbyexample/movies/repository/ActorDto.kt
// ...

data class ActorDto(
//  val id: String,
    override val id: String,
    val name: String,
//)
): HasId

internal fun ActorEntity.toDto() =
    // ...
show in full file repository/src/main/java/com/androidbyexample/movies/repository/RatingDto.kt
// ...

data class RatingDto(
//  val id: String,
    override val id: String,
    val name: String,
    val description: String,
//)
): HasId

internal fun RatingEntity.toDto() =
    // ...

But what happens when (later) we display a cast list using RoleWithActorDto? We need unique ids for each of these as well.

If we have

data class RoleWithActorDto(
    val actor: ActorDto,
    val character: String,
    val orderInCredits: Int,
)

We can generate a unique id by appending the orderInCredits to the actor's id. We do this by defining a derived property for the id. A derived property is one whose value is generated from other properties rather than using a backing field to store data.

data class RoleWithActorDto(
    val actor: ActorDto,
    val character: String,
    val orderInCredits: Int,
): HasId {
    override val id: String
        get() = "${actor.id}:$character"
}

But this will raise another issue further down the line... When we display the cast list, what happens when the user clicks on one of those RoleWithActorDtos? The way we've written our list will use the id of the item as the "target" we want to visit when the user clicks. This won't work with this derived id property.

So we really need a separate property to track the targetId. Often this will have the same value as the id, so we can define a default implementation in the HasId interface.

show in full file repository/src/main/java/com/androidbyexample/movies/repository/HasId.kt
package com.androidbyexample.movies.repository

interface HasId {
    val id: String
    val targetId: String
        get() = id
}

Any implementations that need to can override it with their specific implementation

show in full file repository/src/main/java/com/androidbyexample/movies/repository/MovieDto.kt
// ...
data class RoleWithActorDto(
    val actor: ActorDto,
    val character: String,
    val orderInCredits: Int,
//)
): HasId {
    override val id: String
        get() = "${actor.id}:$character"
    override val targetId: String
        get() = actor.id
}

internal fun RoleWithActor.toDto() =
    // ...
show in full file repository/src/main/java/com/androidbyexample/movies/repository/ActorDto.kt
// ...
data class RoleWithMovieDto(
    val movie: MovieDto,
    val character: String,
    val orderInCredits: Int,
//)
): HasId {
    override val id: String
        get() = "${movie.id}:$orderInCredits"
    override val targetId: String
        get() = movie.id
}

internal fun RoleWithMovie.toDto() =
    // ...

Factoring out the list

We can now create a generic ListScaffold composable.

show in full file app/src/main/java/com/androidbyexample/movies/helper/ListScaffold.kt
// ...
import com.androidbyexample.movies.repository.HasId

@OptIn(ExperimentalFoundationApi::class, ExperimentalMaterial3Api::class)
@Composable
fun <T: HasId> ListScaffold(
    title: String,
    items: List<T>,
    onItemClicked: (String) -> Unit,
    onDeleteSelectedItems: (Set<String>) -> Unit,
    onResetDatabase: () -> Unit,
    @DrawableRes itemIconId: Int,
    @StringRes itemContentDescriptionId: Int,
    modifier: Modifier = Modifier,
    cardContent: @Composable (T) -> Unit,
) {
    val selectedIds = rememberSaveable { mutableStateSetOf<String>()}

    fun onSelectionToggle(id: String) {
        if (id in selectedIds) {
            selectedIds -= id
        } else {
            selectedIds += id
        }
    }

    fun clearSelectedIds() {
        selectedIds.clear()
    }

    if (selectedIds.isNotEmpty()) {
        BackHandler {
            clearSelectedIds()
        }
    }

    Scaffold(
        topBar = {
            val colors = TopAppBarDefaults.topAppBarColors(
                containerColor = MaterialTheme.colorScheme.primary,
                scrolledContainerColor = MaterialTheme.colorScheme.surfaceContainer,
                titleContentColor = MaterialTheme.colorScheme.onPrimary,
                navigationIconContentColor = MaterialTheme.colorScheme.onPrimary,
                actionIconContentColor = MaterialTheme.colorScheme.onSecondary
            )
            if (selectedIds.isEmpty()) {
                TopAppBar(
                    colors = colors,
                    title = {
                        Text(text = title)
                    },
                    actions = {
                        IconButton(onClick = onResetDatabase) {
                            Icon(
                                painter = painterResource(R.drawable.refresh_24),
                                contentDescription = stringResource(R.string.reset_database),
                            )
                        }
                    }
                )
            } else {
                TopAppBar(
                    colors = colors,
                    navigationIcon = {
                        Icon(
                            painter = painterResource(R.drawable.arrow_back_24),
                            contentDescription = stringResource(R.string.clear_selections),
                            modifier = Modifier.clickable(onClick = ::clearSelectedIds),
                        )
                    },
                    title = {
                        Text(
                            text = selectedIds.size.toString(),
                            modifier = Modifier.padding(8.dp)
                        )
                    },
                    actions = {
                        IconButton(
                            onClick = {
                                onDeleteSelectedItems(selectedIds.toSet())
                                // NOTE - this is a mutable set, and we need to be sure we just
                                //        pass a read-only copy out of here. If we don't, the
                                //        caller could modify the contents AND we set up a race
                                //        condition between the clear() call below and the read
                                //        of the data in the caller.
                                selectedIds.clear()
                            }
                        ) {
                            Icon(
                                painter = painterResource(R.drawable.delete_24),
                                contentDescription = stringResource(R.string.delete_selected_items)
                            )
                        }
                    },
                )
            }
        },
        modifier = modifier,
    ) { innerPadding ->
        LazyColumn(
            modifier = Modifier.padding(innerPadding)
        ) {
            items(
                items = items,
                key = { it.id },
            ) { item ->
                val containerColor =
                    if (item.id in selectedIds) {
                        MaterialTheme.colorScheme.primaryContainer
                    } else {
                        MaterialTheme.colorScheme.surface
                    }
                val contentColor = MaterialTheme.colorScheme.contentColorFor(containerColor)
                Card(
                    elevation = CardDefaults.cardElevation(
                        defaultElevation = 8.dp,
                    ),
                    colors = CardDefaults.cardColors(
                        containerColor = containerColor,
                        contentColor = contentColor,
                    ),
                    modifier = Modifier
                        .padding(8.dp)
                        .combinedClickable(
                            // NOTE - use targetId for navigation, id for selections
                            onClick = {
                                if (selectedIds.isEmpty()) {
                                    onItemClicked(item.targetId)
                                } else {
                                    onSelectionToggle(item.id)
                                }
                            },
                            onLongClick = {
                                onSelectionToggle(item.id)
                            },
                        )
                ) {
                    Row(
                        verticalAlignment = Alignment.CenterVertically,
                        modifier = Modifier.padding(8.dp),
                    ) {
                        Icon(
                            painter = painterResource(itemIconId),
                            contentDescription = stringResource(itemContentDescriptionId),
                            modifier = Modifier.clickable {
                                onSelectionToggle(item.id)
                            }
                        )
                        cardContent(item)
                    }
                }
            }
        }
    }
}

There are several spots to call out here to make this generic across all screens that might use it.

First let's take a look at the parameters passed to ListScaffold. This function takes a generic parameter T that implements HasId:

show in full file app/src/main/java/com/androidbyexample/movies/helper/ListScaffold.kt
// ...

@OptIn(ExperimentalFoundationApi::class, ExperimentalMaterial3Api::class)
@Composable
fun <T: HasId> ListScaffold(
    title: String,
    items: List<T>,
    onItemClicked: (String) -> Unit,
    onDeleteSelectedItems: (Set<String>) -> Unit,
    onResetDatabase: () -> Unit,
    @DrawableRes itemIconId: Int,
    @StringRes itemContentDescriptionId: Int,
    modifier: Modifier = Modifier,
    cardContent: @Composable (T) -> Unit,
) {
    val selectedIds = rememberSaveable { mutableStateSetOf<String>()}
    // ...
}
Parameter Description
title The text to display at the top of the screen
items The items to display in the list. Items are of generic type T
onItemClicked We change onMovieClicked to be more generic
onDeleteSelectedItems Similarly renamed to be generic
onResetDatabase Same function as before
itemIconId The id of the icon to display in each card
itemContentDescriptionId The content description of that icon
Modifier The normal modifier being passed in
cardContent A function that is called to emit the contents of each card. This function is passed each item in the list.

When we're dealing with user clicks, we use the HasId functions in each item to obtain the id and targetId. We use targetId for navigation, and id for unique ids in the LazyColumn and selection tracking.

show in full file app/src/main/java/com/androidbyexample/movies/helper/ListScaffold.kt
// ...

@OptIn(ExperimentalFoundationApi::class, ExperimentalMaterial3Api::class)
@Composable
fun <T: HasId> ListScaffold(
    // ...
) {
    // ...
    Scaffold(
        // ...
    ) { innerPadding ->
        LazyColumn(
            // ...
        ) {
            items(
                // ...
            ) { item ->
                // ...
                Card(
                    // ...
                    modifier = Modifier
                        .padding(8.dp)
                        .combinedClickable(
                            // NOTE - use targetId for navigation, id for selections
                            onClick = {
                                if (selectedIds.isEmpty()) {
                                    onItemClicked(item.targetId)
                                } else {
                                    onSelectionToggle(item.id)
                                }
                            },
                            onLongClick = {
                                onSelectionToggle(item.id)
                            },
                        )
                ) {
                    // ...
                }
            }
        }
    }
}

(We use the id when clicking the card icon as well)

show in full file app/src/main/java/com/androidbyexample/movies/helper/ListScaffold.kt
// ...

@OptIn(ExperimentalFoundationApi::class, ExperimentalMaterial3Api::class)
@Composable
fun <T: HasId> ListScaffold(
    // ...
) {
    // ...
    Scaffold(
        // ...
    ) { innerPadding ->
        LazyColumn(
            // ...
        ) {
            items(
                // ...
            ) { item ->
                // ...
                Card(
                    // ...
                ) {
                    Row(
                        // ...
                        modifier = Modifier.padding(8.dp),
                    ) {
                        Icon(
                            painter = painterResource(itemIconId),
                            contentDescription = stringResource(itemContentDescriptionId),
                            modifier = Modifier.clickable {
                                onSelectionToggle(item.id)
                            }
                        )
                        cardContent(item)
                    }
                }
            }
        }
    }
}

The card content function is called to fill in the type-specific details.

show in full file app/src/main/java/com/androidbyexample/movies/helper/ListScaffold.kt
// ...

@OptIn(ExperimentalFoundationApi::class, ExperimentalMaterial3Api::class)
@Composable
fun <T: HasId> ListScaffold(
    // ...
) {
    // ...
    Scaffold(
        // ...
    ) { innerPadding ->
        LazyColumn(
            // ...
        ) {
            items(
                // ...
            ) { item ->
                // ...
                Card(
                    // ...
                ) {
                    Row(
                        // ...
                    ) {
                        // ...
                            }
                        )
                        cardContent(item)
                    }
                }
            }
        }
    }
}

Using the ListScaffold in MovieListUi

We can now replace the common list function in MovieListUi

show in full file app/src/main/java/com/androidbyexample/movies/screens/MovieListUi.kt
// ...

@OptIn(ExperimentalMaterial3Api::class)
@Composable
fun MovieListUi(
    // ...
    onResetDatabase: () -> Unit,
) {
//  val selectedIds = rememberSaveable { mutableStateSetOf<String>()}
//
//  fun onSelectionToggle(id: String) {
//      if (id in selectedIds) {
//          selectedIds -= id
//      } else {
//          selectedIds += id
//      }
//  }
//
//  fun clearSelectedIds() {
//      selectedIds.clear()
//  }
//
//  if (selectedIds.isNotEmpty()) {
//      BackHandler {
//          clearSelectedIds()
//      }
//  }
//
//  Scaffold(
//      topBar = {
//          val colors = TopAppBarDefaults.topAppBarColors(
//              containerColor = MaterialTheme.colorScheme.primary,
//              scrolledContainerColor = MaterialTheme.colorScheme.surfaceContainer,
//              titleContentColor = MaterialTheme.colorScheme.onPrimary,
//              navigationIconContentColor = MaterialTheme.colorScheme.onPrimary,
//              actionIconContentColor = MaterialTheme.colorScheme.onSecondary
//          )
//          if (selectedIds.isEmpty()) {
//              TopAppBar(
//                  colors = colors,
//                  title = {
//                      Text(text = stringResource(R.string.movies))
//                  },
//                  actions = {
//                      IconButton(onClick = onResetDatabase) {
//                          Icon(
//                              painter = painterResource(R.drawable.refresh_24),
//                              contentDescription = stringResource(R.string.reset_database),
//                          )
//                      }
//                  }
//              )
//          } else {
//              TopAppBar(
//                  colors = colors,
//                  navigationIcon = {
//                      Icon(
//                          painter = painterResource(R.drawable.arrow_back_24),
//                          contentDescription = stringResource(R.string.clear_selections),
//                          modifier = Modifier.clickable(onClick = ::clearSelectedIds),
//                      )
//                  },
//                  title = {
//                      Text(
//                          text = selectedIds.size.toString(),
//                          modifier = Modifier.padding(8.dp)
//                      )
//                  },
//                  actions = {
//                      IconButton(
//                          onClick = {
//                              onDeleteSelectedMovies(selectedIds.toSet())
//                              // NOTE - this is a mutable set, and we need to be sure we just
//                              //        pass a read-only copy out of here. If we don't, the
//                              //        caller could modify the contents AND we set up a race
//                              //        condition between the clear() call below and the read
//                              //        of the data in the caller.
//                              selectedIds.clear()
//                          }
//                      ) {
//                          Icon(
//                              painter = painterResource(R.drawable.delete_24),
//                              contentDescription = stringResource(R.string.delete_selected_items)
//                          )
//                      }
//                  },
//              )
//          }
//      },
//      modifier = modifier,
//  ) { innerPadding ->
//      LazyColumn(
//          modifier = modifier
//              .padding(innerPadding)
//      ) {
//          items(
    ListScaffold(
        title = stringResource(R.string.movies),
        items = movies,
//              key = { it.id }
        onItemClicked = onMovieClicked,
        onDeleteSelectedItems = onDeleteSelectedMovies,
        onResetDatabase = onResetDatabase,
        itemIconId = R.drawable.movie_24,
        itemContentDescriptionId = R.string.movie,
        modifier = modifier
    ) { movie ->
//              val containerColor =
//                  if (movie.id in selectedIds) {
//                      MaterialTheme.colorScheme.primaryContainer
//                  } else {
//                      MaterialTheme.colorScheme.surface
//                  }
//              val contentColor = MaterialTheme
//                  .colorScheme
//                  .contentColorFor(containerColor)
//
//              Card(
//                  elevation = CardDefaults.cardElevation(
//                      defaultElevation = 8.dp,
//                  ),
//                  colors = CardDefaults.cardColors(
//                      containerColor = containerColor,
//                      contentColor = contentColor,
//                  ),
//                  border = BorderStroke(1.dp, MaterialTheme.colorScheme.outlineVariant),
//                  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(
//                          painter = painterResource(R.drawable.movie_24),
//                          contentDescription = stringResource(R.string.movie),
//                          modifier = Modifier.clickable {
//                              onSelectionToggle(movie.id)
//                          }
//                      )
        Display(text = movie.title)
    }
//              }
//          }
//      }
//  }
}

Note that we change the parameter passed to onMovieClicked to be just the id rather than the entire object.

show in full file app/src/main/java/com/androidbyexample/movies/screens/MovieListUi.kt
// ...

@OptIn(ExperimentalMaterial3Api::class)
@Composable
fun MovieListUi(
    movies: List<MovieDto>,
    modifier: Modifier = Modifier,
//  onMovieClicked: (MovieDto) -> Unit,
//  onDeleteSelectedMovies: (Set<String>) -> Unit,
    onMovieClicked: (String) -> Unit,
    onDeleteSelectedMovies: (Set<String>) -> Unit,
    onResetDatabase: () -> Unit,
) {
    // ...
}

and we tweak this in Ui

show in full file app/src/main/java/com/androidbyexample/movies/screens/Ui.kt
// ...

@OptIn(ExperimentalMaterial3AdaptiveApi::class)
@Composable
fun Ui(
    // ...
) {
    // ...
    NavDisplay(
        // ...
        entryProvider = entryProvider {
            entry<MovieList>(
                // ...
            ) {
                // ...
                MovieListUi(
                    movies = movies,
//                  onMovieClicked = { movie ->
//                      viewModel.pushScreen(MovieDisplay(movie.id))
                    onMovieClicked = { movieId ->
                        viewModel.pushScreen(MovieDisplay(movieId))
                    },
                    onDeleteSelectedMovies = { ids ->
                        // ...
                )
            }
            // ...
        }
    )
}

All code changes

ADDED: app/src/main/java/com/androidbyexample/movies/helper/ListScaffold.kt
package com.androidbyexample.movies.helper

import androidx.activity.compose.BackHandler
import androidx.annotation.DrawableRes
import androidx.annotation.StringRes
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.padding
import androidx.compose.foundation.lazy.LazyColumn
import androidx.compose.foundation.lazy.items
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.TopAppBarDefaults
import androidx.compose.material3.contentColorFor
import androidx.compose.runtime.Composable
import androidx.compose.runtime.mutableStateSetOf
import androidx.compose.runtime.saveable.rememberSaveable
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.res.painterResource
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.unit.dp
import com.androidbyexample.movies.R
import com.androidbyexample.movies.repository.HasId

@OptIn(ExperimentalFoundationApi::class, ExperimentalMaterial3Api::class) @Composable
fun <T: HasId> ListScaffold( title: String, items: List<T>, onItemClicked: (String) -> Unit, onDeleteSelectedItems: (Set<String>) -> Unit, onResetDatabase: () -> Unit, @DrawableRes itemIconId: Int, @StringRes itemContentDescriptionId: Int, modifier: Modifier = Modifier, cardContent: @Composable (T) -> Unit,
) { val selectedIds = rememberSaveable { mutableStateSetOf<String>()} fun onSelectionToggle(id: String) { if (id in selectedIds) { selectedIds -= id } else { selectedIds += id } } fun clearSelectedIds() { selectedIds.clear() } if (selectedIds.isNotEmpty()) { BackHandler { clearSelectedIds() } } Scaffold( topBar = { val colors = TopAppBarDefaults.topAppBarColors( containerColor = MaterialTheme.colorScheme.primary, scrolledContainerColor = MaterialTheme.colorScheme.surfaceContainer, titleContentColor = MaterialTheme.colorScheme.onPrimary, navigationIconContentColor = MaterialTheme.colorScheme.onPrimary, actionIconContentColor = MaterialTheme.colorScheme.onSecondary ) if (selectedIds.isEmpty()) { TopAppBar( colors = colors, title = { Text(text = title) }, actions = { IconButton(onClick = onResetDatabase) { Icon( painter = painterResource(R.drawable.refresh_24), contentDescription = stringResource(R.string.reset_database), ) } } ) } else { TopAppBar( colors = colors, navigationIcon = { Icon( painter = painterResource(R.drawable.arrow_back_24), contentDescription = stringResource(R.string.clear_selections), modifier = Modifier.clickable(onClick = ::clearSelectedIds), ) }, title = { Text( text = selectedIds.size.toString(), modifier = Modifier.padding(8.dp) ) }, actions = { IconButton( onClick = { onDeleteSelectedItems(selectedIds.toSet()) // NOTE - this is a mutable set, and we need to be sure we just // pass a read-only copy out of here. If we don't, the // caller could modify the contents AND we set up a race // condition between the clear() call below and the read // of the data in the caller. selectedIds.clear() } ) { Icon( painter = painterResource(R.drawable.delete_24), contentDescription = stringResource(R.string.delete_selected_items) ) } }, ) } }, modifier = modifier, ) { innerPadding -> LazyColumn( modifier = Modifier.padding(innerPadding) ) { items( items = items, key = { it.id }, ) { item -> val containerColor = if (item.id in selectedIds) { MaterialTheme.colorScheme.primaryContainer } else { MaterialTheme.colorScheme.surface } val contentColor = MaterialTheme.colorScheme.contentColorFor(containerColor) Card( elevation = CardDefaults.cardElevation( defaultElevation = 8.dp, ), colors = CardDefaults.cardColors( containerColor = containerColor, contentColor = contentColor, ), modifier = Modifier .padding(8.dp) .combinedClickable(
// NOTE - use targetId for navigation, id for selections onClick = { if (selectedIds.isEmpty()) { onItemClicked(item.targetId) } else { onSelectionToggle(item.id) } }, onLongClick = { onSelectionToggle(item.id) },
) ) { Row( verticalAlignment = Alignment.CenterVertically, modifier = Modifier.padding(8.dp), ) {
Icon( painter = painterResource(itemIconId), contentDescription = stringResource(itemContentDescriptionId), modifier = Modifier.clickable { onSelectionToggle(item.id) } )
cardContent(item)
} } } } } }
CHANGED: app/src/main/java/com/androidbyexample/movies/screens/MovieListUi.kt
package com.androidbyexample.movies.screens

//import androidx.activity.compose.BackHandler
//import androidx.compose.foundation.BorderStroke
//import androidx.compose.foundation.clickable
//import androidx.compose.foundation.combinedClickable
//import androidx.compose.foundation.layout.Row
//import androidx.compose.foundation.layout.padding
//import androidx.compose.foundation.lazy.LazyColumn
//import androidx.compose.foundation.lazy.items
//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.TopAppBarDefaults
//import androidx.compose.material3.contentColorFor
import androidx.compose.runtime.Composable
//import androidx.compose.runtime.mutableStateSetOf
//import androidx.compose.runtime.saveable.rememberSaveable
//import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
//import androidx.compose.ui.res.painterResource
import androidx.compose.ui.res.stringResource
//import androidx.compose.ui.unit.dp
import com.androidbyexample.movies.R
import com.androidbyexample.movies.helper.Display
import com.androidbyexample.movies.helper.ListScaffold
import com.androidbyexample.movies.repository.MovieDto

@OptIn(ExperimentalMaterial3Api::class)
@Composable
fun MovieListUi(
    movies: List<MovieDto>,
    modifier: Modifier = Modifier,
// onMovieClicked: (MovieDto) -> Unit, // onDeleteSelectedMovies: (Set<String>) -> Unit, onMovieClicked: (String) -> Unit,
onDeleteSelectedMovies: (Set<String>) -> Unit, onResetDatabase: () -> Unit, ) {
// val selectedIds = rememberSaveable { mutableStateSetOf<String>()} // // fun onSelectionToggle(id: String) { // if (id in selectedIds) { // selectedIds -= id // } else { // selectedIds += id // } // } // // fun clearSelectedIds() { // selectedIds.clear() // } // // if (selectedIds.isNotEmpty()) { // BackHandler { // clearSelectedIds() // } // } // // Scaffold( // topBar = { // val colors = TopAppBarDefaults.topAppBarColors( // containerColor = MaterialTheme.colorScheme.primary, // scrolledContainerColor = MaterialTheme.colorScheme.surfaceContainer, // titleContentColor = MaterialTheme.colorScheme.onPrimary, // navigationIconContentColor = MaterialTheme.colorScheme.onPrimary, // actionIconContentColor = MaterialTheme.colorScheme.onSecondary // ) // if (selectedIds.isEmpty()) { // TopAppBar( // colors = colors, // title = { // Text(text = stringResource(R.string.movies)) // }, // actions = { // IconButton(onClick = onResetDatabase) { // Icon( // painter = painterResource(R.drawable.refresh_24), // contentDescription = stringResource(R.string.reset_database), // ) // } // } // ) // } else { // TopAppBar( // colors = colors, // navigationIcon = { // Icon( // painter = painterResource(R.drawable.arrow_back_24), // contentDescription = stringResource(R.string.clear_selections), // modifier = Modifier.clickable(onClick = ::clearSelectedIds), // ) // }, // title = { // Text( // text = selectedIds.size.toString(), // modifier = Modifier.padding(8.dp) // ) // }, // actions = { // IconButton( // onClick = { // onDeleteSelectedMovies(selectedIds.toSet()) // // NOTE - this is a mutable set, and we need to be sure we just // // pass a read-only copy out of here. If we don't, the // // caller could modify the contents AND we set up a race // // condition between the clear() call below and the read // // of the data in the caller. // selectedIds.clear() // } // ) { // Icon( // painter = painterResource(R.drawable.delete_24), // contentDescription = stringResource(R.string.delete_selected_items) // ) // } // }, // ) // } // }, // modifier = modifier, // ) { innerPadding -> // LazyColumn( // modifier = modifier // .padding(innerPadding) // ) { // items( ListScaffold( title = stringResource(R.string.movies), items = movies, // key = { it.id } onItemClicked = onMovieClicked, onDeleteSelectedItems = onDeleteSelectedMovies, onResetDatabase = onResetDatabase, itemIconId = R.drawable.movie_24, itemContentDescriptionId = R.string.movie, modifier = modifier ) { movie -> // val containerColor = // if (movie.id in selectedIds) { // MaterialTheme.colorScheme.primaryContainer // } else { // MaterialTheme.colorScheme.surface // } // val contentColor = MaterialTheme // .colorScheme // .contentColorFor(containerColor) // // Card( // elevation = CardDefaults.cardElevation( // defaultElevation = 8.dp, // ), // colors = CardDefaults.cardColors( // containerColor = containerColor, // contentColor = contentColor, // ), // border = BorderStroke(1.dp, MaterialTheme.colorScheme.outlineVariant), // 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( // painter = painterResource(R.drawable.movie_24), // contentDescription = stringResource(R.string.movie), // modifier = Modifier.clickable { // onSelectionToggle(movie.id) // } // ) Display(text = movie.title) } // } // } // } // }
}
CHANGED: app/src/main/java/com/androidbyexample/movies/screens/Ui.kt
package com.androidbyexample.movies.screens

import androidx.compose.material3.adaptive.ExperimentalMaterial3AdaptiveApi
import androidx.compose.material3.adaptive.navigation3.ListDetailSceneStrategy
import androidx.compose.material3.adaptive.navigation3.rememberListDetailSceneStrategy
import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue
import androidx.lifecycle.compose.collectAsStateWithLifecycle
import androidx.navigation3.runtime.entryProvider
import androidx.navigation3.ui.NavDisplay
import com.androidbyexample.movies.MovieViewModel

@OptIn(ExperimentalMaterial3AdaptiveApi::class)
@Composable
fun Ui(
    viewModel: MovieViewModel,
) {
    val listDetailStrategy = rememberListDetailSceneStrategy<Screen>()

    val backStack by viewModel.backStackFlow.collectAsStateWithLifecycle(listOf(MovieList))

    NavDisplay(
        backStack = backStack,
        onBack = viewModel::popScreen,
        sceneStrategies = listOf(listDetailStrategy),
        entryProvider = entryProvider {
            entry<MovieList>(
                metadata = ListDetailSceneStrategy.listPane()
            ) {
                val movies by viewModel.moviesFlow.collectAsStateWithLifecycle(
                    initialValue = emptyList()
                )

                MovieListUi(
                    movies = movies,
// onMovieClicked = { movie -> // viewModel.pushScreen(MovieDisplay(movie.id)) onMovieClicked = { movieId -> viewModel.pushScreen(MovieDisplay(movieId)) },
onDeleteSelectedMovies = { ids -> viewModel.deleteSelectedMovies(ids) },
onResetDatabase = viewModel::doResetDatabase, ) } entry<MovieDisplay>( metadata = ListDetailSceneStrategy.detailPane() ) { key -> MovieDisplayUi( id = key.id, fetchMovie = viewModel::getMovieWithCast, ) } } ) }
CHANGED: repository/src/main/java/com/androidbyexample/movies/repository/ActorDto.kt
package com.androidbyexample.movies.repository

import com.androidbyexample.movies.data.ActorEntity
import com.androidbyexample.movies.data.ActorWithFilmography
import com.androidbyexample.movies.data.RoleWithMovie

data class ActorDto( // val id: String, override val id: String, val name: String, //) ): HasId
internal fun ActorEntity.toDto() = ActorDto(id = id, name = name) internal fun ActorDto.toEntity() = ActorEntity(id = id, name = name) data class ActorWithFilmographyDto( val actor: ActorDto, val filmography: List<RoleWithMovieDto>, )
data class RoleWithMovieDto( val movie: MovieDto, val character: String, val orderInCredits: Int, //) ): HasId { override val id: String get() = "${movie.id}:$orderInCredits" override val targetId: String get() = movie.id }
internal fun RoleWithMovie.toDto() = RoleWithMovieDto( movie = movie.toDto(), character = role.character, orderInCredits = role.orderInCredits, ) internal fun ActorWithFilmography.toDto() = ActorWithFilmographyDto( actor = actor.toDto(), filmography = rolesWithMovies.map { it.toDto() } )
ADDED: repository/src/main/java/com/androidbyexample/movies/repository/HasId.kt
package com.androidbyexample.movies.repository

interface HasId { val id: String val targetId: String get() = id }
CHANGED: repository/src/main/java/com/androidbyexample/movies/repository/MovieDto.kt
package com.androidbyexample.movies.repository

import com.androidbyexample.movies.data.MovieEntity
import com.androidbyexample.movies.data.MovieWithCast
import com.androidbyexample.movies.data.RoleWithActor

data class MovieDto( // val id: String, override val id: String, val title: String, val description: String, val ratingId: String, //) ): HasId
internal fun MovieEntity.toDto() = MovieDto(id = id, title = title, description = description, ratingId = ratingId) internal fun MovieDto.toEntity() = MovieEntity(id = id, title = title, description = description, ratingId = ratingId) data class MovieWithCastDto( val movie: MovieDto, val cast: List<RoleWithActorDto>, )
data class RoleWithActorDto( val actor: ActorDto, val character: String, val orderInCredits: Int, //) ): HasId { override val id: String get() = "${actor.id}:$character" override val targetId: String get() = actor.id }
internal fun RoleWithActor.toDto() = RoleWithActorDto( actor = actor.toDto(), character = role.character, orderInCredits = role.orderInCredits, ) internal fun MovieWithCast.toDto() = MovieWithCastDto( movie = movie.toDto(), cast = rolesWithActors.map { it.toDto() } )
CHANGED: repository/src/main/java/com/androidbyexample/movies/repository/RatingDto.kt
package com.androidbyexample.movies.repository

import com.androidbyexample.movies.data.RatingEntity
import com.androidbyexample.movies.data.RatingWithMovies

data class RatingDto( // val id: String, override val id: String, val name: String, val description: String, //) ): HasId
internal fun RatingEntity.toDto() = RatingDto(id = id, name = name, description = description) internal fun RatingDto.toEntity() = RatingEntity(id = id, name = name, description = description) data class RatingWithMoviesDto( val rating: RatingDto, val movies: List<MovieDto>, ) // only need the toDto(); we don't use this to do database updates internal fun RatingWithMovies.toDto() = RatingWithMoviesDto( rating = rating.toDto(), movies = movies.map { it.toDto() }, )