Movies UI - Updates
Editing a movie
Let's edit some movies!
To be able to save the updates, we need to add and expose update functionality.
Add "upsert" functions in the DAO - these will insert if it doesn't exist, update if it does.
show in full file data/src/main/java/com/androidbyexample/movies/data/MovieDao.kt
// ...
@Dao
abstract class MovieDao {
// ...
abstract suspend fun getMovie(id: String): MovieEntity
@Upsert
abstract suspend fun upsert(vararg ratings: RatingEntity)
@Upsert
abstract suspend fun upsert(vararg movies: MovieEntity)
@Upsert
abstract suspend fun upsert(vararg actors: ActorEntity)
@Upsert
abstract suspend fun upsert(vararg roles: RoleEntity)
@Insert
// ...
}
Pass the upserts through in the repository database implementation
show in full file repository/src/main/java/com/androidbyexample/movies/repository/MovieRepository.kt
// ...
interface MovieRepository {
// ...
suspend fun insert(actor: ActorDto)
suspend fun insert(rating: RatingDto)
suspend fun upsert(movie: MovieDto)
suspend fun upsert(actor: ActorDto)
suspend fun upsert(rating: RatingDto)
suspend fun deleteMoviesById(ids: Set<String>)
// ...
}
show in full file repository/src/main/java/com/androidbyexample/movies/repository/MovieDatabaseRepository.kt
// ...
class MovieDatabaseRepository(
// ...
): MovieRepository {
// ...
override suspend fun insert(actor: ActorDto) = dao.insert(actor.toEntity())
override suspend fun insert(rating: RatingDto) = dao.insert(rating.toEntity())
override suspend fun upsert(movie: MovieDto) = dao.upsert(movie.toEntity())
override suspend fun upsert(actor: ActorDto) = dao.upsert(actor.toEntity())
override suspend fun upsert(rating: RatingDto) = dao.upsert(rating.toEntity())
override suspend fun deleteMoviesById(ids: Set<String>) = dao.deleteMoviesById(ids)
// ...
}
Note
Keep in mind that Room ensures its suspend functions are run using an IO dispatcher.
It uses a call like withContext(Dispatchers.IO) { ... } to wrap all work.
This ensures that the calls are "main-thread safe", meaning they don't block the main thread.
Some other libraries will do similar context switching, but you may want to be extra careful
and make your repository calls main-thread safe. For example, you could write
override suspend fun upsert(movie: MovieDto) =
withContext(Dispatchers.IO) {
dao.insert(movie.toEntity())
}
in the MovieDatabaseRepository, ensuring that the called function is switched to the IO
dispatcher. Not necessary with Room, but if you use other databases, files, or network,
you should do so.
We'll follow the "saving with an explicit save button" approach for the movie edit process.
We'll start by adding an "edit" button to our display screens. This button will appear on the
top bar, so we need to add it to the ListScaffold.
show in full file app/src/main/java/com/androidbyexample/movies/helper/ListScaffold.kt
// ...
@OptIn(ExperimentalFoundationApi::class, ExperimentalMaterial3Api::class)
@Composable
fun <T: HasId> ListScaffold(
// ...
@StringRes itemContentDescriptionId: Int,
modifier: Modifier = Modifier,
onEdit: (() -> Unit)? = null,
fixedContentAbove: @Composable () -> Unit = {},
cardContent: @Composable (T) -> Unit,
) {
// ...
}
That might look a bit odd, but what we're defining is a () -> Unit function that can be
nullable by wrapping the function type in (...)?, then giving it a default value of null.
This allows us to pass it if we need it, otherwise its value is null.
Conditionally add an edit button to the ListScaffold, if onEdit is not null.
The button will call onEdit with no parameters, informing the caller that the user wants to edit.
We'll use an "edit" icon from https://fonts.google.com/icons.
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(
topBar = {
// ...
if (selectedIds.isEmpty()) {
TopAppBar(
// ...
},
actions = {
onEdit?.let {
IconButton(onClick = onEdit) {
Icon(
painter = painterResource(R.drawable.edit_24),
contentDescription = stringResource(R.string.edit)
)
}
}
onResetDatabase?.let {
IconButton(onClick = onResetDatabase) {
// ...
}
)
} else {
// ...
}
},
// ...
) { innerPadding ->
// ...
}
}
Set up the onEdit parameter and call im MovieDisplayUi, ActorDisplayUi and RatingDisplayUi:
show in full file app/src/main/java/com/androidbyexample/movies/screens/MovieDisplayUi.kt
// ...
@OptIn(ExperimentalMaterial3Api::class)
@Composable
fun MovieDisplayUi(
movieWithCast: MovieWithCastDto?,
onActorClicked: (String) -> Unit,
onEdit: (String) -> Unit,
onDeleteSelectedActors: (Set<String>) -> Unit,
modifier: Modifier = Modifier,
) {
movieWithCast?.let { movieWithCast ->
ListScaffold(
// ...
items = movieWithCast.cast,
onItemClicked = onActorClicked,
onEdit = { onEdit(movieWithCast.movie.id) },
onDeleteSelectedItems = onDeleteSelectedActors,
onResetDatabase = null,
// ...
)
} ?: run {
// ...
}
}
show in full file app/src/main/java/com/androidbyexample/movies/screens/ActorDisplayUi.kt
// ...
@OptIn(ExperimentalMaterial3Api::class)
@Composable
fun ActorDisplayUi(
actorWithFilmography: ActorWithFilmographyDto?,
onMovieClicked: (String) -> Unit,
onEdit: (String) -> Unit,
onDeleteSelectedMovies: (Set<String>) -> Unit,
modifier: Modifier = Modifier,
) {
actorWithFilmography?.let { actorWithFilmography ->
ListScaffold(
// ...
items = actorWithFilmography.filmography,
onItemClicked = onMovieClicked,
onEdit = { onEdit(actorWithFilmography.actor.id) },
onDeleteSelectedItems = onDeleteSelectedMovies,
onResetDatabase = null,
// ...
)
}
}
show in full file app/src/main/java/com/androidbyexample/movies/screens/RatingDisplayUi.kt
// ...
@OptIn(ExperimentalMaterial3Api::class)
@Composable
fun RatingDisplayUi(
ratingWithMovies: RatingWithMoviesDto?,
onMovieClicked: (String) -> Unit,
onEdit: (String) -> Unit,
onDeleteSelectedMovies: (Set<String>) -> Unit,
modifier: Modifier = Modifier,
) {
ratingWithMovies?.let { ratingWithMovies ->
ListScaffold(
// ...
items = ratingWithMovies.movies,
onItemClicked = onMovieClicked,
onEdit = { onEdit(ratingWithMovies.rating.id) },
onDeleteSelectedItems = onDeleteSelectedMovies,
onResetDatabase = null,
// ...
)
}
}
Before we wire things up in Ui, we need to define the screen data for the new edit screens:
show in full file app/src/main/java/com/androidbyexample/movies/screens/Screens.kt
// ...
data class RatingDisplay(val id: String): Screen
@Parcelize
data class MovieEdit(val id: String): Screen
@Parcelize
data class ActorEdit(val id: String): Screen
@Parcelize
data class RatingEdit(val id: String): Screen
We'll have to look up the entities by id before passing them to the edit screens. We only want the entities themselves, not their associated data, so we need to add some new "get" functions to our DAO:
show in full file data/src/main/java/com/androidbyexample/movies/data/MovieDao.kt
// ...
@Dao
abstract class MovieDao {
// ...
abstract suspend fun getMovieWithCast(id: String): MovieWithCast
@Query("SELECT * FROM RatingEntity WHERE id = :id")
abstract suspend fun getRating(id: String): RatingEntity
@Query("SELECT * FROM ActorEntity WHERE id = :id")
abstract suspend fun getActor(id: String): ActorEntity
@Query("SELECT * FROM MovieEntity WHERE id = :id")
abstract suspend fun getMovie(id: String): MovieEntity
@Upsert
// ...
}
Passing these through the repository and its implementation as usual:
show in full file repository/src/main/java/com/androidbyexample/movies/repository/MovieRepository.kt
// ...
interface MovieRepository {
// ...
suspend fun getMovieWithCast(id: String): MovieWithCastDto
suspend fun getActorWithFilmography(id: String): ActorWithFilmographyDto
suspend fun getRating(id: String): RatingDto
suspend fun getMovie(id: String): MovieDto
suspend fun getActor(id: String): ActorDto
suspend fun insert(movie: MovieDto)
suspend fun insert(actor: ActorDto)
// ...
}
show in full file repository/src/main/java/com/androidbyexample/movies/repository/MovieDatabaseRepository.kt
// ...
class MovieDatabaseRepository(
// ...
): MovieRepository {
// ...
override suspend fun getActorWithFilmography(id: String): ActorWithFilmographyDto =
dao.getActorWithFilmography(id).toDto()
override suspend fun getRating(id: String): RatingDto =
dao.getRating(id).toDto()
override suspend fun getMovie(id: String): MovieDto =
dao.getMovie(id).toDto()
override suspend fun getActor(id: String): ActorDto =
dao.getActor(id).toDto()
override suspend fun insert(movie: MovieDto) = dao.insert(movie.toEntity())
override suspend fun insert(actor: ActorDto) = dao.insert(actor.toEntity())
// ...
}
To perform the actual updates, we need to start a coroutine and call the upsert functions. We can do these in the Ui or view model; prefer the view model as its coroutine scope lives across configuration changes. If the user rotates the screen while the edit is in progress it will be canceled.
show in full file app/src/main/java/com/androidbyexample/movies/MovieViewModel.kt
// ...
class MovieViewModel(
// ...
): ViewModel(), MovieRepository by repository {
// ...
}
fun update(movie: MovieDto) {
viewModelScope.launch {
upsert(movie)
}
}
fun update(actor: ActorDto) {
viewModelScope.launch {
upsert(actor)
}
}
fun update(rating: RatingDto) {
viewModelScope.launch {
upsert(rating)
}
}
companion object {
// ...
}
Now we define the actual edit screens:
show in full file app/src/main/java/com/androidbyexample/movies/screens/MovieEditUi.kt
// ...
import com.androidbyexample.movies.repository.MovieDto
@OptIn(ExperimentalMaterial3Api::class)
@Composable
fun MovieEditUi(
movie: MovieDto?,
onMovieChange: (MovieDto) -> Unit,
) {
movie?.let {
val titleState = rememberTextFieldState(movie.title)
val descriptionState = rememberTextFieldState(movie.description)
val backDispatcher =
LocalOnBackPressedDispatcherOwner
.current
?.onBackPressedDispatcher
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
)
TopAppBar(
colors = colors,
title = { Text(text = movie.title) },
actions = {
IconButton(
onClick = {
onMovieChange(
movie.copy(
title = titleState.text.toString(),
description = descriptionState.text.toString(),
)
)
backDispatcher?.onBackPressed()
}
) {
Icon(
painter = painterResource(R.drawable.check_24),
contentDescription = stringResource(R.string.save)
)
}
}
)
}
) { innerPadding ->
Column(
modifier = Modifier.padding(innerPadding)
) {
OutlinedTextField(
state = titleState,
label = { Text(text = stringResource(id = R.string.title))},
)
OutlinedTextField(
state = descriptionState,
label = { Text(text = stringResource(id = R.string.description))},
)
}
}
} ?: run {
DetailPlaceholder(messageId = R.string.loading)
}
}
show in full file app/src/main/java/com/androidbyexample/movies/screens/ActorEditUi.kt
// ...
import com.androidbyexample.movies.repository.ActorDto
@OptIn(ExperimentalMaterial3Api::class)
@Composable
fun ActorEditUi(
actor: ActorDto?,
onActorChange: (ActorDto) -> Unit,
) {
actor?.let {
val nameState = rememberTextFieldState(actor.name)
val backDispatcher =
LocalOnBackPressedDispatcherOwner
.current
?.onBackPressedDispatcher
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
)
TopAppBar(
colors = colors,
title = { Text(text = actor.name) },
actions = {
IconButton(
onClick = {
onActorChange(
// ActorDto(
// id = actor.id,
// name = actor.name
// )
actor.copy(
name = nameState.text.toString(),
)
)
backDispatcher?.onBackPressed()
}
) {
Icon(
painter = painterResource(R.drawable.check_24),
contentDescription = stringResource(R.string.save)
)
}
}
)
}
) { innerPadding ->
Column(
modifier = Modifier.padding(innerPadding)
) {
OutlinedTextField(
state = nameState,
label = { Text(text = stringResource(id = R.string.name))},
)
}
}
} ?: run {
DetailPlaceholder(messageId = R.string.loading)
}
}
show in full file app/src/main/java/com/androidbyexample/movies/screens/RatingEditUi.kt
// ...
import com.androidbyexample.movies.repository.RatingDto
@OptIn(ExperimentalMaterial3Api::class)
@Composable
fun RatingEditUi(
rating: RatingDto?,
onRatingChange: (RatingDto) -> Unit,
) {
rating?.let {
val nameState = rememberTextFieldState(rating.name)
val descriptionState = rememberTextFieldState(rating.description)
val backDispatcher =
LocalOnBackPressedDispatcherOwner
.current
?.onBackPressedDispatcher
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
)
TopAppBar(
colors = colors,
title = { Text(text = rating.name) },
actions = {
IconButton(
onClick = {
onRatingChange(
rating.copy(
name = nameState.text.toString(),
description = descriptionState.text.toString(),
)
)
backDispatcher?.onBackPressed()
}
) {
Icon(
painter = painterResource(R.drawable.check_24),
contentDescription = stringResource(R.string.save)
)
}
}
)
}
) { innerPadding ->
Column(
modifier = Modifier.padding(innerPadding)
) {
OutlinedTextField(
state = nameState,
label = { Text(text = stringResource(id = R.string.name))},
)
OutlinedTextField(
state = descriptionState,
label = { Text(text = stringResource(id = R.string.description))},
)
}
}
} ?: run {
DetailPlaceholder(messageId = R.string.loading)
}
}
Note the save button action
show in full file app/src/main/java/com/androidbyexample/movies/screens/MovieEditUi.kt
// ...
@OptIn(ExperimentalMaterial3Api::class)
@Composable
fun MovieEditUi(
// ...
) {
movie?.let {
// ...
Scaffold(
topBar = {
// ...
TopAppBar(
// ...
title = { Text(text = movie.title) },
actions = {
IconButton(
onClick = {
onMovieChange(
movie.copy(
title = titleState.text.toString(),
description = descriptionState.text.toString(),
)
)
backDispatcher?.onBackPressed()
}
) {
Icon(
painter = painterResource(R.drawable.check_24),
// ...
}
}
)
}
) { innerPadding ->
// ...
}
} ?: run {
// ...
}
}
We want to perform the update, and leave the screen. To do this, we need to talk with the "back dispatcher", asking it to go back. We access the back dispatcher through its composition local.
show in full file app/src/main/java/com/androidbyexample/movies/screens/MovieEditUi.kt
// ...
@OptIn(ExperimentalMaterial3Api::class)
@Composable
fun MovieEditUi(
// ...
) {
movie?.let {
// ...
val descriptionState = rememberTextFieldState(movie.description)
val backDispatcher =
LocalOnBackPressedDispatcherOwner
.current
?.onBackPressedDispatcher
Scaffold(
// ...
} ?: run {
// ...
}
}
Finally, we wire up our edit screens in Ui:
show in full file app/src/main/java/com/androidbyexample/movies/screens/Ui.kt
// ...
@OptIn(ExperimentalMaterial3AdaptiveApi::class)
@Composable
fun Ui(
// ...
) {
// ...
NavigationSuiteScaffold(
// ...
) {
NavDisplay(
// ...
entryProvider = entryProvider {
// ...
)
}
entry<MovieEdit> { screen ->
var movie by remember { mutableStateOf<MovieDto?>(null) }
LaunchedEffect(key1 = screen.id) {
withContext(Dispatchers.IO) {
movie = viewModel.getMovie(screen.id)
}
}
MovieEditUi(
movie = movie,
onMovieChange = viewModel::update,
)
}
entry<ActorList>(
metadata = ListDetailSceneStrategy.listPane(
// ...
)
}
entry<ActorEdit> { screen ->
var actor by remember { mutableStateOf<ActorDto?>(null) }
LaunchedEffect(key1 = screen.id) {
withContext(Dispatchers.IO) {
actor = viewModel.getActor(screen.id)
}
}
ActorEditUi(
actor = actor,
onActorChange = viewModel::update,
)
}
entry< RatingList>(
metadata = ListDetailSceneStrategy.listPane(
// ...
)
}
entry<RatingEdit> { screen ->
var rating by remember { mutableStateOf<RatingDto?>(null) }
LaunchedEffect(key1 = screen.id) {
withContext(Dispatchers.IO) {
rating = viewModel.getRating(screen.id)
}
}
RatingEditUi(
rating = rating,
onRatingChange = viewModel::update,
)
}
}
)
}
}
And add in the "onEdit" calls
show in full file app/src/main/java/com/androidbyexample/movies/screens/Ui.kt
// ...
@OptIn(ExperimentalMaterial3AdaptiveApi::class)
@Composable
fun Ui(
// ...
) {
// ...
NavigationSuiteScaffold(
// ...
) {
NavDisplay(
// ...
entryProvider = entryProvider {
// ...
entry<MovieDisplay>(
// ...
) { key ->
// ...
MovieDisplayUi(
movieWithCast = movieWithCast,
onActorClicked = { viewModel.pushScreen(ActorDisplay(it)) },
onEdit = { viewModel.pushScreen(MovieEdit(it)) },
onDeleteSelectedActors = viewModel::deleteSelectedActors,
)
}
// ...
entry<ActorDisplay>(
// ...
) { key ->
// ...
ActorDisplayUi(
actorWithFilmography = actorWithFilmography,
onMovieClicked = { viewModel.pushScreen(MovieDisplay(it)) },
onEdit = { viewModel.pushScreen(ActorEdit(it)) },
onDeleteSelectedMovies = viewModel::deleteSelectedMovies,
)
}
// ...
entry<RatingDisplay>(
// ...
) { key ->
// ...
RatingDisplayUi(
ratingWithMovies = ratingWithMovies,
onMovieClicked = { viewModel.pushScreen(MovieDisplay(it)) },
onEdit = { viewModel.pushScreen(RatingEdit(it)) },
onDeleteSelectedMovies = viewModel::deleteSelectedMovies,
)
}
// ...
}
)
}
}
When we run, we can now edit our movie information!
I'll leave the other screens as exercises for the interested reader.
All code changes
CHANGED: app/src/main/java/com/androidbyexample/movies/MovieViewModel.kt
package com.androidbyexample.movies
import androidx.lifecycle.SavedStateHandle
import androidx.lifecycle.ViewModel
import androidx.lifecycle.ViewModelProvider
import androidx.lifecycle.createSavedStateHandle
import androidx.lifecycle.viewModelScope
import androidx.lifecycle.viewmodel.initializer
import androidx.lifecycle.viewmodel.viewModelFactory
import com.androidbyexample.movies.repository.ActorDto
import com.androidbyexample.movies.repository.MovieDatabaseRepository
import com.androidbyexample.movies.repository.MovieDto
import com.androidbyexample.movies.repository.MovieRepository
import com.androidbyexample.movies.repository.RatingDto
import com.androidbyexample.movies.screens.ListScreen
import com.androidbyexample.movies.screens.MovieList
import com.androidbyexample.movies.screens.Screen
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch
class MovieViewModel(
private val savedStateHandle: SavedStateHandle,
private val repository: MovieRepository,
): ViewModel(), MovieRepository by repository {
val currentListScreenFlow = savedStateHandle.getMutableStateFlow<ListScreen>(
key = "current_list_screen",
initialValue = MovieList
)
fun goToListScreen(screen: ListScreen) {
currentListScreenFlow.value = screen
backStackFlow.value = listOf(screen)
}
val backStackFlow = savedStateHandle.getMutableStateFlow<List<Screen>>(
key = "back_stack",
initialValue = listOf(MovieList)
)
fun pushScreen(screen: Screen) {
backStackFlow.value += screen
}
fun popScreen() {
backStackFlow.value = backStackFlow.value.dropLast(1)
}
fun doResetDatabase() {
viewModelScope.launch(Dispatchers.IO) {
repository.resetDatabase()
}
}
fun deleteSelectedMovies(ids: Set<String>) {
viewModelScope.launch {
deleteMoviesById(ids)
}
}
fun deleteSelectedActors(ids: Set<String>) {
viewModelScope.launch {
deleteActorsById(ids)
}
}
fun deleteSelectedRatings(ids: Set<String>) {
viewModelScope.launch {
deleteRatingsById(ids)
}
}
fun update(movie: MovieDto) {
viewModelScope.launch {
upsert(movie)
}
}
fun update(actor: ActorDto) {
viewModelScope.launch {
upsert(actor)
}
}
fun update(rating: RatingDto) {
viewModelScope.launch {
upsert(rating)
}
}
companion object {
val Factory = viewModelFactory {
initializer {
val application =
checkNotNull(this[ViewModelProvider.AndroidViewModelFactory.APPLICATION_KEY])
val savedStateHandle = this.createSavedStateHandle()
MovieViewModel(
savedStateHandle = savedStateHandle,
repository = MovieDatabaseRepository.create(application)
)
}
}
}
}
CHANGED: 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,
onEdit: (() -> Unit)? = null,
fixedContentAbove: @Composable () -> Unit = {},
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 = {
onResetDatabase?.let {
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 = {
// We might be tracking derived ids in the set. We need to
// delete the targetIds instead. We'll map the selectedIds
// to targetIds and send those to be deleted.
val targetIds =
selectedIds.mapNotNull { id ->
items.find { it.id == id }?.targetId
}.toSet()
onDeleteSelectedItems(targetIds)
selectedIds.clear()
}
) {
Icon(
painter = painterResource(R.drawable.delete_24),
contentDescription = stringResource(R.string.delete_selected_items)
)
}
},
)
}
},
modifier = modifier,
) { innerPadding ->
LazyColumn(
modifier = Modifier.padding(innerPadding)
) {
item {
fixedContentAbove()
}
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)
.animateItem()
.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/ActorDisplayUi.kt
package com.androidbyexample.movies.screens
import androidx.compose.material3.ExperimentalMaterial3Api
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
import androidx.compose.ui.res.stringResource
import com.androidbyexample.movies.R
import com.androidbyexample.movies.helper.Display
import com.androidbyexample.movies.helper.Label
import com.androidbyexample.movies.helper.ListScaffold
import com.androidbyexample.movies.repository.ActorWithFilmographyDto
@OptIn(ExperimentalMaterial3Api::class)
@Composable
fun ActorDisplayUi(
actorWithFilmography: ActorWithFilmographyDto?,
onMovieClicked: (String) -> Unit,
onEdit: (String) -> Unit,
onDeleteSelectedMovies: (Set<String>) -> Unit,
modifier: Modifier = Modifier,
) {
actorWithFilmography?.let { actorWithFilmography ->
ListScaffold(
title = actorWithFilmography.actor.name,
items = actorWithFilmography.filmography,
onItemClicked = onMovieClicked,
onEdit = { onEdit(actorWithFilmography.actor.id) },
onDeleteSelectedItems = onDeleteSelectedMovies,
onResetDatabase = null,
itemIconId = R.drawable.movie_24,
itemContentDescriptionId = R.string.movie,
modifier = modifier,
fixedContentAbove = {
Label(textId = R.string.title)
Display(text = actorWithFilmography.actor.name)
Label(
textId = R.string.movies_starring,
actorWithFilmography.actor.name
)
},
cardContent = { role ->
Display(
text = stringResource(
R.string.cast_entry,
role.character,
role.movie.title,
)
)
},
)
}
}
ADDED: app/src/main/java/com/androidbyexample/movies/screens/ActorEditUi.kt
package com.androidbyexample.movies.screens
import androidx.activity.compose.LocalOnBackPressedDispatcherOwner
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.text.input.rememberTextFieldState
import androidx.compose.material3.ExperimentalMaterial3Api
import androidx.compose.material3.Icon
import androidx.compose.material3.IconButton
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.OutlinedTextField
import androidx.compose.material3.Scaffold
import androidx.compose.material3.Text
import androidx.compose.material3.TopAppBar
import androidx.compose.material3.TopAppBarDefaults
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
import androidx.compose.ui.res.painterResource
import androidx.compose.ui.res.stringResource
import com.androidbyexample.movies.R
import com.androidbyexample.movies.repository.ActorDto
@OptIn(ExperimentalMaterial3Api::class)
@Composable
fun ActorEditUi(
actor: ActorDto?,
onActorChange: (ActorDto) -> Unit,
) {
actor?.let {
val nameState = rememberTextFieldState(actor.name)
val backDispatcher =
LocalOnBackPressedDispatcherOwner
.current
?.onBackPressedDispatcher
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
)
TopAppBar(
colors = colors,
title = { Text(text = actor.name) },
actions = {
IconButton(
onClick = {
onActorChange(
// ActorDto(
// id = actor.id,
// name = actor.name
// )
actor.copy(
name = nameState.text.toString(),
)
)
backDispatcher?.onBackPressed()
}
) {
Icon(
painter = painterResource(R.drawable.check_24),
contentDescription = stringResource(R.string.save)
)
}
}
)
}
) { innerPadding ->
Column(
modifier = Modifier.padding(innerPadding)
) {
OutlinedTextField(
state = nameState,
label = { Text(text = stringResource(id = R.string.name))},
)
}
}
} ?: run {
DetailPlaceholder(messageId = R.string.loading)
}
}
CHANGED: app/src/main/java/com/androidbyexample/movies/screens/MovieDisplayUi.kt
package com.androidbyexample.movies.screens
import androidx.compose.material3.ExperimentalMaterial3Api
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
import androidx.compose.ui.res.stringResource
import com.androidbyexample.movies.R
import com.androidbyexample.movies.helper.Display
import com.androidbyexample.movies.helper.Label
import com.androidbyexample.movies.helper.ListScaffold
import com.androidbyexample.movies.repository.MovieWithCastDto
@OptIn(ExperimentalMaterial3Api::class)
@Composable
fun MovieDisplayUi(
movieWithCast: MovieWithCastDto?,
onActorClicked: (String) -> Unit,
onEdit: (String) -> Unit,
onDeleteSelectedActors: (Set<String>) -> Unit,
modifier: Modifier = Modifier,
) {
movieWithCast?.let { movieWithCast ->
ListScaffold(
title = movieWithCast.movie.title,
items = movieWithCast.cast,
onItemClicked = onActorClicked,
onEdit = { onEdit(movieWithCast.movie.id) },
onDeleteSelectedItems = onDeleteSelectedActors,
onResetDatabase = null,
itemIconId = R.drawable.movie_24,
itemContentDescriptionId = R.string.movie,
modifier = modifier,
fixedContentAbove = {
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)
},
cardContent = { role ->
Display(
text = stringResource(
R.string.cast_entry,
role.character,
role.actor.name,
)
)
},
)
} ?: run {
DetailPlaceholder(messageId = R.string.loading)
}
}
ADDED: app/src/main/java/com/androidbyexample/movies/screens/MovieEditUi.kt
package com.androidbyexample.movies.screens
import androidx.activity.compose.LocalOnBackPressedDispatcherOwner
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.text.input.rememberTextFieldState
import androidx.compose.material3.ExperimentalMaterial3Api
import androidx.compose.material3.Icon
import androidx.compose.material3.IconButton
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.OutlinedTextField
import androidx.compose.material3.Scaffold
import androidx.compose.material3.Text
import androidx.compose.material3.TopAppBar
import androidx.compose.material3.TopAppBarDefaults
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
import androidx.compose.ui.res.painterResource
import androidx.compose.ui.res.stringResource
import com.androidbyexample.movies.R
import com.androidbyexample.movies.repository.MovieDto
@OptIn(ExperimentalMaterial3Api::class)
@Composable
fun MovieEditUi(
movie: MovieDto?,
onMovieChange: (MovieDto) -> Unit,
) {
movie?.let {
val titleState = rememberTextFieldState(movie.title)
val descriptionState = rememberTextFieldState(movie.description)
val backDispatcher =
LocalOnBackPressedDispatcherOwner
.current
?.onBackPressedDispatcher
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
)
TopAppBar(
colors = colors,
title = { Text(text = movie.title) },
actions = {
IconButton(
onClick = {
onMovieChange(
movie.copy(
title = titleState.text.toString(),
description = descriptionState.text.toString(),
)
)
backDispatcher?.onBackPressed()
}
) {
Icon(
painter = painterResource(R.drawable.check_24),
contentDescription = stringResource(R.string.save)
)
}
}
)
}
) { innerPadding ->
Column(
modifier = Modifier.padding(innerPadding)
) {
OutlinedTextField(
state = titleState,
label = { Text(text = stringResource(id = R.string.title))},
)
OutlinedTextField(
state = descriptionState,
label = { Text(text = stringResource(id = R.string.description))},
)
}
}
} ?: run {
DetailPlaceholder(messageId = R.string.loading)
}
}
CHANGED: app/src/main/java/com/androidbyexample/movies/screens/RatingDisplayUi.kt
package com.androidbyexample.movies.screens
import androidx.compose.material3.ExperimentalMaterial3Api
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
import com.androidbyexample.movies.R
import com.androidbyexample.movies.helper.Display
import com.androidbyexample.movies.helper.Label
import com.androidbyexample.movies.helper.ListScaffold
import com.androidbyexample.movies.repository.RatingWithMoviesDto
@OptIn(ExperimentalMaterial3Api::class)
@Composable
fun RatingDisplayUi(
ratingWithMovies: RatingWithMoviesDto?,
onMovieClicked: (String) -> Unit,
onEdit: (String) -> Unit,
onDeleteSelectedMovies: (Set<String>) -> Unit,
modifier: Modifier = Modifier,
) {
ratingWithMovies?.let { ratingWithMovies ->
ListScaffold(
title = ratingWithMovies.rating.name,
items = ratingWithMovies.movies,
onItemClicked = onMovieClicked,
onEdit = { onEdit(ratingWithMovies.rating.id) },
onDeleteSelectedItems = onDeleteSelectedMovies,
onResetDatabase = null,
itemIconId = R.drawable.movie_24,
itemContentDescriptionId = R.string.movie,
modifier = modifier,
fixedContentAbove = {
Label(textId = R.string.name)
Display(text = ratingWithMovies.rating.name)
Label(textId = R.string.description)
Display(text = ratingWithMovies.rating.description)
Label(
textId = R.string.movies_rated,
ratingWithMovies.rating.name
)
},
cardContent = { movie ->
Display(
text = movie.title
)
},
)
}
}
ADDED: app/src/main/java/com/androidbyexample/movies/screens/RatingEditUi.kt
package com.androidbyexample.movies.screens
import androidx.activity.compose.LocalOnBackPressedDispatcherOwner
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.text.input.rememberTextFieldState
import androidx.compose.material3.ExperimentalMaterial3Api
import androidx.compose.material3.Icon
import androidx.compose.material3.IconButton
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.OutlinedTextField
import androidx.compose.material3.Scaffold
import androidx.compose.material3.Text
import androidx.compose.material3.TopAppBar
import androidx.compose.material3.TopAppBarDefaults
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
import androidx.compose.ui.res.painterResource
import androidx.compose.ui.res.stringResource
import com.androidbyexample.movies.R
import com.androidbyexample.movies.repository.RatingDto
@OptIn(ExperimentalMaterial3Api::class)
@Composable
fun RatingEditUi(
rating: RatingDto?,
onRatingChange: (RatingDto) -> Unit,
) {
rating?.let {
val nameState = rememberTextFieldState(rating.name)
val descriptionState = rememberTextFieldState(rating.description)
val backDispatcher =
LocalOnBackPressedDispatcherOwner
.current
?.onBackPressedDispatcher
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
)
TopAppBar(
colors = colors,
title = { Text(text = rating.name) },
actions = {
IconButton(
onClick = {
onRatingChange(
rating.copy(
name = nameState.text.toString(),
description = descriptionState.text.toString(),
)
)
backDispatcher?.onBackPressed()
}
) {
Icon(
painter = painterResource(R.drawable.check_24),
contentDescription = stringResource(R.string.save)
)
}
}
)
}
) { innerPadding ->
Column(
modifier = Modifier.padding(innerPadding)
) {
OutlinedTextField(
state = nameState,
label = { Text(text = stringResource(id = R.string.name))},
)
OutlinedTextField(
state = descriptionState,
label = { Text(text = stringResource(id = R.string.description))},
)
}
}
} ?: run {
DetailPlaceholder(messageId = R.string.loading)
}
}
CHANGED: app/src/main/java/com/androidbyexample/movies/screens/Screens.kt
package com.androidbyexample.movies.screens
import android.os.Parcelable
import androidx.annotation.DrawableRes
import androidx.annotation.StringRes
import com.androidbyexample.movies.R
import kotlinx.parcelize.Parcelize
sealed interface Screen: Parcelable
sealed class ListScreen(
@DrawableRes val iconId: Int,
@StringRes val labelId: Int,
): Screen
@Parcelize
data object MovieList: ListScreen(
iconId = R.drawable.movie_24,
labelId = R.string.movies,
)
@Parcelize
data object RatingList: ListScreen(
iconId = R.drawable.star_24,
labelId = R.string.ratings,
)
@Parcelize
data object ActorList: ListScreen(
iconId = R.drawable.person_24,
labelId = R.string.actors,
)
@Parcelize
data class MovieDisplay(val id: String): Screen
@Parcelize
data class ActorDisplay(val id: String): Screen
@Parcelize
data class RatingDisplay(val id: String): Screen
@Parcelize
data class MovieEdit(val id: String): Screen
@Parcelize
data class ActorEdit(val id: String): Screen
@Parcelize
data class RatingEdit(val id: String): Screen
CHANGED: app/src/main/java/com/androidbyexample/movies/screens/Ui.kt
package com.androidbyexample.movies.screens
import androidx.compose.material3.Icon
import androidx.compose.material3.Text
import androidx.compose.material3.adaptive.ExperimentalMaterial3AdaptiveApi
import androidx.compose.material3.adaptive.currentWindowAdaptiveInfoV2
import androidx.compose.material3.adaptive.layout.calculatePaneScaffoldDirective
import androidx.compose.material3.adaptive.navigation3.ListDetailSceneStrategy
import androidx.compose.material3.adaptive.navigation3.rememberListDetailSceneStrategy
import androidx.compose.material3.adaptive.navigationsuite.NavigationSuiteScaffold
import 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.res.painterResource
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.unit.dp
import androidx.lifecycle.compose.collectAsStateWithLifecycle
import androidx.navigation3.runtime.entryProvider
import androidx.navigation3.ui.NavDisplay
import com.androidbyexample.movies.MovieViewModel
import com.androidbyexample.movies.R
import com.androidbyexample.movies.repository.ActorDto
import com.androidbyexample.movies.repository.MovieDto
import com.androidbyexample.movies.repository.RatingDto
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.withContext
@OptIn(ExperimentalMaterial3AdaptiveApi::class)
@Composable
fun Ui(
viewModel: MovieViewModel,
) {
val windowAdaptiveInfo = currentWindowAdaptiveInfoV2()
val directive = remember(windowAdaptiveInfo) {
calculatePaneScaffoldDirective(windowAdaptiveInfo)
.copy(horizontalPartitionSpacerSize = 0.dp)
}
val listDetailStrategy = rememberListDetailSceneStrategy<Screen>(directive = directive)
val backStack by viewModel.backStackFlow.collectAsStateWithLifecycle(listOf(MovieList))
val currentListScreen by viewModel.currentListScreenFlow.collectAsStateWithLifecycle(MovieList)
val listScreens = remember { listOf(RatingList, MovieList, ActorList) }
NavigationSuiteScaffold(
navigationSuiteItems = {
listScreens.forEach { target ->
item(
icon = {
Icon(
painter = painterResource(target.iconId),
contentDescription = stringResource(target.labelId)
)
},
label = { Text(stringResource(target.labelId)) },
selected = currentListScreen == target,
onClick = {
viewModel.goToListScreen(target)
}
)
}
}
) {
NavDisplay(
backStack = backStack,
onBack = viewModel::popScreen,
sceneStrategies = listOf(listDetailStrategy),
entryProvider = entryProvider {
entry<MovieList>(
metadata = ListDetailSceneStrategy.listPane(
detailPlaceholder = { DetailPlaceholder(R.string.select_a_movie_to_view) }
)
) {
val movies by viewModel.moviesFlow.collectAsStateWithLifecycle(
initialValue = emptyList()
)
MovieListUi(
movies = movies,
onMovieClicked = { movieId ->
viewModel.pushScreen(MovieDisplay(movieId))
},
onDeleteSelectedMovies = { ids ->
viewModel.deleteSelectedMovies(ids)
},
onResetDatabase = viewModel::doResetDatabase,
)
}
entry<MovieDisplay>(
metadata = ListDetailSceneStrategy.detailPane()
) { key ->
val movieWithCast by
viewModel
.getMovieWithCastFlow(key.id)
.collectAsStateWithLifecycle(null)
MovieDisplayUi(
movieWithCast = movieWithCast,
onActorClicked = { viewModel.pushScreen(ActorDisplay(it)) },
onEdit = { viewModel.pushScreen(MovieEdit(it)) },
onDeleteSelectedActors = viewModel::deleteSelectedActors,
)
}
entry<MovieEdit> { screen ->
var movie by remember { mutableStateOf<MovieDto?>(null) }
LaunchedEffect(key1 = screen.id) {
withContext(Dispatchers.IO) {
movie = viewModel.getMovie(screen.id)
}
}
MovieEditUi(
movie = movie,
onMovieChange = viewModel::update,
)
}
entry<ActorList>(
metadata = ListDetailSceneStrategy.listPane(
detailPlaceholder = { DetailPlaceholder(R.string.select_an_actor_to_view) }
)
) {
val actors by viewModel.actorsFlow.collectAsStateWithLifecycle(
initialValue = emptyList()
)
ActorListUi(
actors = actors,
onActorClicked = { actorId ->
viewModel.pushScreen(ActorDisplay(actorId))
},
onDeleteSelectedActors = { ids ->
viewModel.deleteSelectedActors(ids)
},
onResetDatabase = viewModel::doResetDatabase,
)
}
entry<ActorDisplay>(
metadata = ListDetailSceneStrategy.detailPane()
) { key ->
val actorWithFilmography by
viewModel
.getActorWithFilmographyFlow(key.id)
.collectAsStateWithLifecycle(null)
ActorDisplayUi(
actorWithFilmography = actorWithFilmography,
onMovieClicked = { viewModel.pushScreen(MovieDisplay(it)) },
onEdit = { viewModel.pushScreen(ActorEdit(it)) },
onDeleteSelectedMovies = viewModel::deleteSelectedMovies,
)
}
entry<ActorEdit> { screen ->
var actor by remember { mutableStateOf<ActorDto?>(null) }
LaunchedEffect(key1 = screen.id) {
withContext(Dispatchers.IO) {
actor = viewModel.getActor(screen.id)
}
}
ActorEditUi(
actor = actor,
onActorChange = viewModel::update,
)
}
entry< RatingList>(
metadata = ListDetailSceneStrategy.listPane(
detailPlaceholder = { DetailPlaceholder(R.string.select_a_rating_to_view) }
)
) {
val ratings by viewModel.ratingsFlow.collectAsStateWithLifecycle(
initialValue = emptyList()
)
RatingListUi(
ratings = ratings,
onRatingClicked = { ratingId ->
viewModel.pushScreen(RatingDisplay(ratingId))
},
onDeleteSelectedRatings = { ids ->
viewModel.deleteSelectedRatings(ids)
},
onResetDatabase = viewModel::doResetDatabase,
)
}
entry<RatingDisplay>(
metadata = ListDetailSceneStrategy.detailPane()
) { key ->
val ratingWithMovies by
viewModel
.getRatingWithMoviesFlow(key.id)
.collectAsStateWithLifecycle(null)
RatingDisplayUi(
ratingWithMovies = ratingWithMovies,
onMovieClicked = { viewModel.pushScreen(MovieDisplay(it)) },
onEdit = { viewModel.pushScreen(RatingEdit(it)) },
onDeleteSelectedMovies = viewModel::deleteSelectedMovies,
)
}
entry<RatingEdit> { screen ->
var rating by remember { mutableStateOf<RatingDto?>(null) }
LaunchedEffect(key1 = screen.id) {
withContext(Dispatchers.IO) {
rating = viewModel.getRating(screen.id)
}
}
RatingEditUi(
rating = rating,
onRatingChange = viewModel::update,
)
}
}
)
}
}
ADDED: app/src/main/res/drawable/check_24.xml
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportWidth="960"
android:viewportHeight="960">
<path
android:pathData="M382,720 L154,492l57,-57 171,171 367,-367 57,57 -424,424Z"
android:fillColor="#e3e3e3"/>
</vector>
ADDED: app/src/main/res/drawable/edit_24.xml
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportWidth="960"
android:viewportHeight="960">
<path
android:pathData="M200,760h57l391,-391 -57,-57 -391,391v57ZM120,840v-170l528,-527q12,-11 26.5,-17t30.5,-6q16,0 31,6t26,18l55,56q12,11 17.5,26t5.5,30q0,16 -5.5,30.5T817,313L290,840L120,840ZM760,256 L704,200 760,256ZM619,341 L591,312 648,369 619,341Z"
android:fillColor="#e3e3e3"/>
</vector>
CHANGED: app/src/main/res/values/strings.xml
<resources>
<string name="app_name">movies</string>
<string name="movies">Movies</string>
<string name="actors">Actors</string>
<string name="ratings">Ratings</string>
<string name="actor">Actor</string>
<string name="rating">Rating</string>
<string name="loading">Loading…</string>
<string name="name">Name</string>
<string name="movies_starring">Movies Starring %1$s</string>
<string name="movies_rated">Movies Rated %1$s</string>
<string name="title">Title</string>
<string name="description">Description</string>
<string name="movie">Movie</string>
<string name="reset_database">Reset database</string>
<string name="cast">Cast</string>
<string name="cast_entry">%1$s: %2$s</string>
<string name="clear_selections">Clear Selections</string>
<string name="delete_selected_items">Delete selected items</string>
<string name="select_a_movie_to_view">Select a movie to view</string>
<string name="select_an_actor_to_view">Select an actor to view</string>
<string name="select_a_rating_to_view">Select a rating to view</string>
<string name="edit">Edit</string>
<string name="save">Save</string>
</resources>
CHANGED: data/src/main/java/com/androidbyexample/movies/data/MovieDao.kt
package com.androidbyexample.movies.data
import androidx.room.Dao
import androidx.room.Insert
import androidx.room.Query
import androidx.room.Transaction
import androidx.room.Upsert
import kotlinx.coroutines.flow.Flow
@Dao
abstract class MovieDao {
@Query("SELECT * FROM RatingEntity")
abstract fun getRatingsFlow(): Flow<List<RatingEntity>>
@Query("SELECT * FROM MovieEntity")
abstract fun getMoviesFlow(): Flow<List<MovieEntity>>
@Query("SELECT * FROM ActorEntity")
abstract fun getActorsFlow(): Flow<List<ActorEntity>>
@Transaction
@Query("SELECT * FROM RatingEntity WHERE id = :id")
abstract fun getRatingWithMoviesFlow(id: String): Flow<RatingWithMovies>
@Transaction
@Query("SELECT * FROM ActorEntity WHERE id = :id")
abstract fun getActorWithFilmographyFlow(id: String): Flow<ActorWithFilmography>
@Transaction
@Query("SELECT * FROM MovieEntity WHERE id = :id")
abstract fun getMovieWithCastFlow(id: String): Flow<MovieWithCast>
@Transaction
@Query("SELECT * FROM RatingEntity WHERE id = :id")
abstract suspend fun getRatingWithMovies(id: String): RatingWithMovies
@Transaction
@Query("SELECT * FROM ActorEntity WHERE id = :id")
abstract suspend fun getActorWithFilmography(id: String): ActorWithFilmography
@Transaction
@Query("SELECT * FROM MovieEntity WHERE id = :id")
abstract suspend fun getMovieWithCast(id: String): MovieWithCast
@Query("SELECT * FROM RatingEntity WHERE id = :id")
abstract suspend fun getRating(id: String): RatingEntity
@Query("SELECT * FROM ActorEntity WHERE id = :id")
abstract suspend fun getActor(id: String): ActorEntity
@Query("SELECT * FROM MovieEntity WHERE id = :id")
abstract suspend fun getMovie(id: String): MovieEntity
@Upsert
abstract suspend fun upsert(vararg ratings: RatingEntity)
@Upsert
abstract suspend fun upsert(vararg movies: MovieEntity)
@Upsert
abstract suspend fun upsert(vararg actors: ActorEntity)
@Upsert
abstract suspend fun upsert(vararg roles: RoleEntity)
@Insert
abstract suspend fun insert(vararg ratings: RatingEntity)
@Insert
abstract suspend fun insert(vararg movies: MovieEntity)
@Insert
abstract suspend fun insert(vararg actors: ActorEntity)
@Insert
abstract suspend fun insert(vararg roles: RoleEntity)
@Query("DELETE FROM MovieEntity WHERE id IN (:ids)")
abstract suspend fun deleteMoviesById(ids: Set<String>)
@Query("DELETE FROM ActorEntity WHERE id IN (:ids)")
abstract suspend fun deleteActorsById(ids: Set<String>)
@Query("DELETE FROM RatingEntity WHERE id IN (:ids)")
abstract suspend fun deleteRatingsById(ids: Set<String>)
@Query("DELETE FROM MovieEntity")
abstract suspend fun clearMovies()
@Query("DELETE FROM ActorEntity")
abstract suspend fun clearActors()
@Query("DELETE FROM RatingEntity")
abstract suspend fun clearRatings()
@Query("DELETE FROM RoleEntity")
abstract suspend fun clearRoles()
@Transaction
open suspend fun resetDatabase() {
clearMovies()
clearActors()
clearRoles()
clearRatings()
insert(
RatingEntity(id = "r0", name = "Not Rated", description = "Not yet rated"),
RatingEntity(id = "r1", name = "G", description = "General Audiences"),
RatingEntity(id = "r2", name = "PG", description = "Parental Guidance Suggested"),
RatingEntity(id = "r3", name = "PG-13", description = "Unsuitable for those under 13"),
RatingEntity(id = "r4", name = "R", description = "Restricted - 17 and older"),
)
insert(
MovieEntity("m1", "The Transporter", "Jason Statham kicks a guy in the face", "r3"),
MovieEntity("m2", "Transporter 2", "Jason Statham kicks a bunch of guys in the face", "r4"),
MovieEntity("m3", "Hobbs and Shaw", "Cars, Explosions and Stuff", "r3"),
MovieEntity("m4", "Jumanji - Welcome to the Jungle", "The Rock smolders", "r3"),
)
insert(
ActorEntity("a1", "Jason Statham"),
ActorEntity("a2", "The Rock"),
ActorEntity("a3", "Shu Qi"),
ActorEntity("a4", "Amber Valletta"),
ActorEntity("a5", "Kevin Hart"),
)
insert(
RoleEntity("m1", "a1", "Frank Martin", 1),
RoleEntity("m1", "a3", "Lai", 2),
RoleEntity("m2", "a1", "Frank Martin", 1),
RoleEntity("m2", "a4", "Audrey Billings", 2),
RoleEntity("m3", "a2", "Hobbs", 1),
RoleEntity("m3", "a1", "Shaw", 2),
RoleEntity("m4", "a2", "Spencer", 1),
RoleEntity("m4", "a5", "Fridge", 2),
)
}
}
CHANGED: repository/src/main/java/com/androidbyexample/movies/repository/MovieDatabaseRepository.kt
package com.androidbyexample.movies.repository
import android.content.Context
import com.androidbyexample.movies.data.MovieDao
import com.androidbyexample.movies.data.createDao
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.map
class MovieDatabaseRepository(
private val dao: MovieDao
): MovieRepository {
override val ratingsFlow =
dao.getRatingsFlow()
.map { ratings ->// for each List<RatingEntity> that's emitted
// create a list of RatingDto
ratings.map { rating -> rating.toDto() } // map each entity to Dto
}
override val moviesFlow =
dao.getMoviesFlow()
.map { movies ->
movies.map { it.toDto() }
}
override val actorsFlow =
dao.getActorsFlow()
.map { actors ->
actors.map { it.toDto() }
}
override fun getRatingWithMoviesFlow(id: String): Flow<RatingWithMoviesDto> =
dao.getRatingWithMoviesFlow(id).map { it.toDto() }
override fun getMovieWithCastFlow(id: String): Flow<MovieWithCastDto> =
dao.getMovieWithCastFlow(id).map { it.toDto() }
override fun getActorWithFilmographyFlow(id: String): Flow<ActorWithFilmographyDto> =
dao.getActorWithFilmographyFlow(id).map { it.toDto() }
override suspend fun getRatingWithMovies(id: String): RatingWithMoviesDto =
dao.getRatingWithMovies(id).toDto()
override suspend fun getMovieWithCast(id: String): MovieWithCastDto =
dao.getMovieWithCast(id).toDto()
override suspend fun getActorWithFilmography(id: String): ActorWithFilmographyDto =
dao.getActorWithFilmography(id).toDto()
override suspend fun getRating(id: String): RatingDto =
dao.getRating(id).toDto()
override suspend fun getMovie(id: String): MovieDto =
dao.getMovie(id).toDto()
override suspend fun getActor(id: String): ActorDto =
dao.getActor(id).toDto()
override suspend fun insert(movie: MovieDto) = dao.insert(movie.toEntity())
override suspend fun insert(actor: ActorDto) = dao.insert(actor.toEntity())
override suspend fun insert(rating: RatingDto) = dao.insert(rating.toEntity())
override suspend fun upsert(movie: MovieDto) = dao.upsert(movie.toEntity())
override suspend fun upsert(actor: ActorDto) = dao.upsert(actor.toEntity())
override suspend fun upsert(rating: RatingDto) = dao.upsert(rating.toEntity())
override suspend fun deleteMoviesById(ids: Set<String>) = dao.deleteMoviesById(ids)
override suspend fun deleteActorsById(ids: Set<String>) = dao.deleteActorsById(ids)
override suspend fun deleteRatingsById(ids: Set<String>) = dao.deleteRatingsById(ids)
override suspend fun resetDatabase() = dao.resetDatabase()
companion object {
fun create(context: Context) =
MovieDatabaseRepository(createDao(context))
}
}
CHANGED: repository/src/main/java/com/androidbyexample/movies/repository/MovieRepository.kt
package com.androidbyexample.movies.repository
import kotlinx.coroutines.flow.Flow
interface MovieRepository {
val ratingsFlow: Flow<List<RatingDto>>
val moviesFlow: Flow<List<MovieDto>>
val actorsFlow: Flow<List<ActorDto>>
fun getRatingWithMoviesFlow(id: String): Flow<RatingWithMoviesDto>
fun getMovieWithCastFlow(id: String): Flow<MovieWithCastDto>
fun getActorWithFilmographyFlow(id: String): Flow<ActorWithFilmographyDto>
suspend fun getRatingWithMovies(id: String): RatingWithMoviesDto
suspend fun getMovieWithCast(id: String): MovieWithCastDto
suspend fun getActorWithFilmography(id: String): ActorWithFilmographyDto
suspend fun getRating(id: String): RatingDto
suspend fun getMovie(id: String): MovieDto
suspend fun getActor(id: String): ActorDto
suspend fun insert(movie: MovieDto)
suspend fun insert(actor: ActorDto)
suspend fun insert(rating: RatingDto)
suspend fun upsert(movie: MovieDto)
suspend fun upsert(actor: ActorDto)
suspend fun upsert(rating: RatingDto)
suspend fun deleteMoviesById(ids: Set<String>)
suspend fun deleteActorsById(ids: Set<String>)
suspend fun deleteRatingsById(ids: Set<String>)
suspend fun resetDatabase()
}