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 in

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 database repository, 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 "save to database as we type" approach for the movie edit process.

onEdit: (() -> Unit)? = null,

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.
  • Set up the onEdit call. This one's a bit tricky:

    • If we don't have a movie to edit, movieWithCast will be null, causing the let to be null, resulting in passing null to the ListScaffold - no edit button will be displayed.
    • If we have a movie to edit, we want to pass a lambda to ListScaffold for what to do when the user presses the edit button. The additional curly braces inside the let define a lambda expression as the last expression in the let, which will be returned from the let and passed to the ListScaffold.
    • The onEdit lambda we created in that let takes the id from the movie in the currently-displayed MovieWithRoleDto, and tells the caller to edit it.
  • Define a movie edit screen to perform the edits. This simple screen doesn't use our common scaffold. It fetches the movie (using our new getMovie function), and doesn't refetch until/unless a new id is passed in. This movie holds the title and description that's being edited. Whenever a change is made, we create a copy with the new value and send it out via onMovieChange so the caller can make a database update.

  • To finish things up, we set up onEdit in Ui to navigate from the display screen to the edit screen.

When we run, we can now edit our movie information!

I'll leave the other pages as exercises for the interested reader.

Code Changes

CHANGED: /app/src/main/java/com/androidbyexample/movie/MovieViewModel.kt
package com.androidbyexample.movie

import android.util.Logimport androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.setValue
import androidx.lifecycle.ViewModel
import androidx.lifecycle.ViewModelProvider
import androidx.lifecycle.viewModelScope
import androidx.lifecycle.viewmodel.CreationExtras
import com.androidbyexample.movie.repository.ActorDtoimport com.androidbyexample.movie.repository.MovieDatabaseRepository
import com.androidbyexample.movie.repository.MovieDtoimport com.androidbyexample.movie.repository.MovieRepository
import com.androidbyexample.movie.repository.RatingDtoimport com.androidbyexample.movie.screens.MovieList
import com.androidbyexample.movie.screens.Screen
import kotlinx.coroutines.Dispatchersimport kotlinx.coroutines.FlowPreviewimport kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.asStateFlow
import kotlinx.coroutines.flow.debounceimport kotlinx.coroutines.launch

@OptIn(FlowPreview::class)class MovieViewModel(
    private val repository: MovieRepository,
): ViewModel(), MovieRepository by repository {
    private val _selectedIdsFlow = MutableStateFlow<Set<String>>(emptySet())
    val selectedIdsFlow: Flow<Set<String>> = _selectedIdsFlow.asStateFlow()

    fun clearSelectedIds() {
        _selectedIdsFlow.value = emptySet()
    }
    fun toggleSelection(id: String) {
        if (id in _selectedIdsFlow.value) {
            _selectedIdsFlow.value -= id
        } else {
            _selectedIdsFlow.value += id
        }
    }

    private var screenStack = listOf<Screen>(MovieList)
        set(value) {
            field = value
            clearSelectedIds()
            currentScreen = value.lastOrNull()
        }

    // NOTE: We're keep this as a Compose State for comparison.
    //       You can use Compose state to expose anything from the view model,
    //       but our example will be using Flow from now on to demonstrate how
    //       the view model can be used without Compose, perhaps for other
    //       platforms such as iOS, desktop, web or command line
    var currentScreen by mutableStateOf<Screen?>(MovieList)
        private set

    fun pushScreen(screen: Screen) {
        screenStack = screenStack + screen
    }
    fun popScreen() {
        screenStack = screenStack.dropLast(1)
    }

    fun setScreen(screen: Screen) {
        screenStack = listOf(screen)
    }

fun update(movie: MovieDto) { movieUpdateFlow.value = movie } fun update(actor: ActorDto) { actorUpdateFlow.value = actor } fun update(rating: RatingDto) { ratingUpdateFlow.value = rating } // using a debounced flow as a person-update queue private val movieUpdateFlow = MutableStateFlow<MovieDto?>(null) private val actorUpdateFlow = MutableStateFlow<ActorDto?>(null) private val ratingUpdateFlow = MutableStateFlow<RatingDto?>(null) init { viewModelScope.launch { movieUpdateFlow.debounce(500).collect { movie -> movie?.let { repository.upsert(it) } } } viewModelScope.launch { actorUpdateFlow.debounce(500).collect { actor -> actor?.let { repository.upsert(it) } } } viewModelScope.launch { ratingUpdateFlow.debounce(500).collect { rating -> rating?.let { repository.upsert(it) } } } }
fun deleteSelectedMovies() { viewModelScope.launch { deleteMoviesById(_selectedIdsFlow.value) _selectedIdsFlow.value = emptySet() } } fun deleteSelectedActors() { viewModelScope.launch { deleteActorsById(_selectedIdsFlow.value) _selectedIdsFlow.value = emptySet() } } fun deleteSelectedRatings() { viewModelScope.launch { deleteRatingsById(_selectedIdsFlow.value) _selectedIdsFlow.value = emptySet() } } companion object { val Factory: ViewModelProvider.Factory = object: ViewModelProvider.Factory { @Suppress("UNCHECKED_CAST") override fun <T : ViewModel> create( modelClass: Class<T>, extras: CreationExtras ): T { // Get the Application object from extras val application = checkNotNull(extras[ViewModelProvider.AndroidViewModelFactory.APPLICATION_KEY]) return MovieViewModel( MovieDatabaseRepository.create(application) ) as T } } } }
CHANGED: /app/src/main/java/com/androidbyexample/movie/screens/ActorDisplayUi.kt
package com.androidbyexample.movie.screens

import androidx.compose.foundation.clickable
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.padding
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.Movie
import androidx.compose.material3.Icon
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.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.unit.dp
import com.androidbyexample.movie.R
import com.androidbyexample.movie.components.Display
import com.androidbyexample.movie.components.Label
import com.androidbyexample.movie.repository.ActorWithFilmographyDto
import com.androidbyexample.movie.repository.MovieDto
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.withContext

@Composable
fun ActorDisplayUi(
    id: String,
    fetchActor: suspend (String) -> ActorWithFilmographyDto,
    onMovieClicked: (MovieDto) -> Unit,

    selectedIds: Set<String>,
    onSelectionToggle: (id: String) -> Unit,
    onClearSelections: () -> Unit,
    onDeleteSelectedMovies: () -> Unit,

    currentScreen: Screen,
    onSelectListScreen: (Screen) -> Unit,
    onResetDatabase: () -> Unit,
) {
    var actorWithFilmography by remember { mutableStateOf<ActorWithFilmographyDto?>(null) }
    LaunchedEffect(key1 = id) {
        withContext(Dispatchers.IO) {
            actorWithFilmography = fetchActor(id)
        }
    }
    ListScaffold(
//      titleId = R.string.rating,        titleId = R.string.actor,        items = actorWithFilmography?.filmography?.sortedBy { it.movie.title } ?: emptyList(),
        onItemClicked = { onMovieClicked(it.movie) },
        selectedIds = selectedIds,
        onSelectionToggle = onSelectionToggle,
        onClearSelections = onClearSelections,
        onDeleteSelectedItems = onDeleteSelectedMovies,
        currentScreen = currentScreen,
        onSelectListScreen = onSelectListScreen,
        onResetDatabase = onResetDatabase,
        itemContent = { role ->
Row( verticalAlignment = Alignment.CenterVertically, modifier = Modifier.padding(8.dp) ) { Icon( imageVector = Icons.Default.Movie, contentDescription = stringResource(id = R.string.movie), modifier = Modifier.clickable { onSelectionToggle(role.id) } ) Display(text = role.movie.title) }
}, topContent = { Label(textId = R.string.name) Display(text = actorWithFilmography?.actor?.name ?: "") Label( text = stringResource( id = R.string.movies_starring, actorWithFilmography?.actor?.name ?: "" ) ) } ) }
CHANGED: /app/src/main/java/com/androidbyexample/movie/screens/ListScaffold.kt
package com.androidbyexample.movie.screens

import androidx.annotation.StringRes
import androidx.compose.animation.AnimatedVisibility
import androidx.compose.animation.expandVertically
import androidx.compose.animation.fadeIn
import androidx.compose.animation.fadeOut
import androidx.compose.animation.shrinkVertically
import androidx.compose.foundation.clickable
import androidx.compose.foundation.layout.ColumnScope
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.lazy.LazyListState
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.ArrowBack
import androidx.compose.material.icons.filled.Delete
import androidx.compose.material.icons.filled.Editimport androidx.compose.material.icons.filled.Movie
import androidx.compose.material.icons.filled.Person
import androidx.compose.material.icons.filled.Refresh
import androidx.compose.material.icons.filled.Star
import androidx.compose.material3.ExperimentalMaterial3Api
import androidx.compose.material3.Icon
import androidx.compose.material3.IconButton
import androidx.compose.material3.NavigationBar
import androidx.compose.material3.Scaffold
import androidx.compose.material3.Text
import androidx.compose.material3.TopAppBar
import androidx.compose.runtime.Composable
import androidx.compose.runtime.derivedStateOf
import androidx.compose.runtime.getValue
import androidx.compose.runtime.remember
import androidx.compose.ui.Modifier
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.unit.dp
import com.androidbyexample.movie.R
import com.androidbyexample.movie.components.ScreenSelectButton
import com.androidbyexample.movie.repository.HasId
import com.androidbyexample.movie.repository.MovieDto
@OptIn(ExperimentalMaterial3Api::class)
@Composable
fun <T: HasId> ListScaffold(
    @StringRes titleId: Int,
    items: List<T>,
    onItemClicked: (T) -> Unit,

    selectedIds: Set<String>,
    onSelectionToggle: (id: String) -> Unit,
    onClearSelections: () -> Unit,
    onDeleteSelectedItems: () -> Unit,

    currentScreen: Screen,
    onSelectListScreen: (Screen) -> Unit,

    onResetDatabase: () -> Unit,
onEdit: (() -> Unit)? = null,
topContent: (@Composable () -> Unit)? = null,
itemContent: @Composable ColumnScope.(T) -> Unit, ) {
val lazyListState = remember { LazyListState() }
val showBottomBar by remember(selectedIds) { derivedStateOf { lazyListState.firstVisibleItemScrollOffset == 0 && selectedIds.isEmpty() } }
Scaffold( topBar = { if (selectedIds.isEmpty()) { TopAppBar( title = { Text(text = stringResource(titleId)) }, actions = {
onEdit?.let { IconButton(onClick = onEdit) { Icon( imageVector = Icons.Default.Edit, contentDescription = stringResource(R.string.edit) ) } }
IconButton(onClick = onResetDatabase) { Icon( imageVector = Icons.Default.Refresh, contentDescription = stringResource(R.string.reset_database) ) } } ) } else { TopAppBar( navigationIcon = { Icon( imageVector = Icons.Default.ArrowBack, contentDescription = stringResource(R.string.clear_selections), modifier = Modifier.clickable(onClick = onClearSelections), ) }, title = { Text(text = selectedIds.size.toString(), modifier = Modifier.padding(8.dp)) }, actions = { IconButton(onClick = onDeleteSelectedItems) { Icon( imageVector = Icons.Default.Delete, contentDescription = stringResource(R.string.delete_selected_items) ) } }, ) } }, bottomBar = {
AnimatedVisibility( visible = showBottomBar, enter = expandVertically() + fadeIn(), exit = shrinkVertically() + fadeOut(), ) {
NavigationBar { ScreenSelectButton( targetScreen = RatingList, imageVector = Icons.Default.Star, labelId = R.string.ratings, currentScreen = currentScreen, onSelectListScreen = onSelectListScreen ) ScreenSelectButton( targetScreen = MovieList, imageVector = Icons.Default.Movie, labelId = R.string.movies, currentScreen = currentScreen, onSelectListScreen = onSelectListScreen ) ScreenSelectButton( targetScreen = ActorList, imageVector = Icons.Default.Person, labelId = R.string.actors, currentScreen = currentScreen, onSelectListScreen = onSelectListScreen ) } } }, modifier = Modifier.fillMaxSize() ) { paddingValues -> List(
state = lazyListState,
items = items, onItemClicked = onItemClicked, selectedIds = selectedIds, onSelectionToggle = onSelectionToggle, onClearSelections = onClearSelections,
topContent = topContent,
modifier = Modifier .padding(paddingValues) .fillMaxSize(), itemContent = itemContent, ) } }
CHANGED: /app/src/main/java/com/androidbyexample/movie/screens/MovieDisplayUi.kt
package com.androidbyexample.movie.screens

import androidx.compose.foundation.clickable
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.padding
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.Movie
import androidx.compose.material3.Icon
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.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.unit.dp
import com.androidbyexample.movie.R
import com.androidbyexample.movie.components.Display
import com.androidbyexample.movie.components.Label
import com.androidbyexample.movie.repository.MovieWithCastDto
import com.androidbyexample.movie.repository.RoleWithActorDto
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.withContext

@Composable
fun MovieDisplayUi(
    id: String,
    fetchMovie: suspend (String) -> MovieWithCastDto,
    onActorClicked: (RoleWithActorDto) -> Unit,

    selectedIds: Set<String>,
    onSelectionToggle: (id: String) -> Unit,
    onClearSelections: () -> Unit,
    onDeleteSelectedMovies: () -> Unit,

    currentScreen: Screen,
    onSelectListScreen: (Screen) -> Unit,
    onResetDatabase: () -> Unit,
    onEdit: (String) -> Unit,) {
    var movieWithCast by remember { mutableStateOf<MovieWithCastDto?>(null) }
    LaunchedEffect(key1 = id) {
        withContext(Dispatchers.IO) {
            movieWithCast = fetchMovie(id)
        }
    }
    ListScaffold(
//      titleId = R.string.rating,        titleId = R.string.movie,
onEdit = movieWithCast?.let { { // if we have a movie, pass this lambda as the edit function (else null from let) onEdit(it.movie.id) } },
items = movieWithCast?.cast?.sortedBy { it.orderInCredits } ?: emptyList(), onItemClicked = onActorClicked, selectedIds = selectedIds, onSelectionToggle = onSelectionToggle, onClearSelections = onClearSelections, onDeleteSelectedItems = onDeleteSelectedMovies, currentScreen = currentScreen, onSelectListScreen = onSelectListScreen, onResetDatabase = onResetDatabase, itemContent = { role ->
Row( verticalAlignment = Alignment.CenterVertically, modifier = Modifier.padding(8.dp) ) { Icon( imageVector = Icons.Default.Movie, contentDescription = stringResource(id = R.string.movie), modifier = Modifier.clickable { onSelectionToggle(role.id) } ) Display( text = stringResource( R.string.cast_entry, role.character, role.actor.name, ) ) }
}, topContent = { Label(textId = R.string.title) Display(text = movieWithCast?.movie?.title ?: "") Label(textId = R.string.description) Display(text = movieWithCast?.movie?.description ?: "") Label(textId = R.string.cast) } ) }
ADDED: /app/src/main/java/com/androidbyexample/movie/screens/MovieEditUi.kt
package com.androidbyexample.movie.screensimport androidx.compose.foundation.layout.Columnimport androidx.compose.foundation.layout.paddingimport androidx.compose.material3.ExperimentalMaterial3Apiimport androidx.compose.material3.OutlinedTextFieldimport androidx.compose.material3.Scaffoldimport androidx.compose.material3.Textimport androidx.compose.material3.TopAppBarimport androidx.compose.runtime.Composableimport androidx.compose.runtime.LaunchedEffectimport androidx.compose.runtime.getValueimport androidx.compose.runtime.mutableStateOfimport androidx.compose.runtime.rememberimport androidx.compose.runtime.setValueimport androidx.compose.ui.Modifierimport androidx.compose.ui.res.stringResourceimport com.androidbyexample.movie.Rimport com.androidbyexample.movie.repository.MovieDtoimport kotlinx.coroutines.Dispatchersimport kotlinx.coroutines.withContext
@OptIn(ExperimentalMaterial3Api::class)@Composablefun MovieEditUi( id: String, fetchMovie: suspend (String) -> MovieDto, onMovieChange: (MovieDto) -> Unit,) { var movie by remember { mutableStateOf<MovieDto?>(null) } LaunchedEffect(key1 = id) { withContext(Dispatchers.IO) { movie = fetchMovie(id) } } Scaffold( topBar = { TopAppBar(title = { Text(text = movie?.title ?: "") }) } ) { paddingValues -> Column( modifier = Modifier.padding(paddingValues) ) { movie?.let { fetchedMovie -> OutlinedTextField( value = fetchedMovie.title, label = { Text(text = stringResource(id = R.string.title))}, onValueChange = { val newMovie = fetchedMovie.copy(title = it) movie = newMovie onMovieChange(newMovie) }, ) OutlinedTextField( value = fetchedMovie.description, label = { Text(text = stringResource(id = R.string.description))}, onValueChange = { val newMovie = fetchedMovie.copy(description = it) movie = newMovie onMovieChange(newMovie) }, ) } } }}
CHANGED: /app/src/main/java/com/androidbyexample/movie/screens/Screens.kt
package com.androidbyexample.movie.screens

sealed interface Screen

object MovieList: Screen
object ActorList: Screen
object RatingList: Screen
data class MovieDisplay(val id: String): Screen
data class ActorDisplay(val id: String): Screen
data class RatingDisplay(val id: String): Screen
data class MovieEdit(val id: String): Screen//data class ActorEdit(val id: String): Screen//data class RatingEdit(val id: String): Screen
CHANGED: /app/src/main/java/com/androidbyexample/movie/screens/Ui.kt
package com.androidbyexample.movie.screens

import androidx.activity.compose.BackHandler
import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue
import androidx.compose.runtime.rememberCoroutineScope
import androidx.lifecycle.compose.collectAsStateWithLifecycle
import com.androidbyexample.movie.MovieViewModel
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch

@Composable
fun Ui(
    viewModel: MovieViewModel,
    onExit: () -> Unit,
) {
    BackHandler {
        viewModel.popScreen()
    }

    val scope = rememberCoroutineScope()

    when (val screen = viewModel.currentScreen) {
        null -> onExit()
        is MovieDisplay -> {
            val selectedIds by viewModel.selectedIdsFlow.collectAsStateWithLifecycle(initialValue = emptySet())
            MovieDisplayUi(
                id = screen.id,
                fetchMovie = viewModel::getMovieWithCast,
                onActorClicked = { viewModel.pushScreen(ActorDisplay(it.actor.id)) },

                selectedIds = selectedIds,
                onClearSelections = viewModel::clearSelectedIds,
                onSelectionToggle = viewModel::toggleSelection,
                onDeleteSelectedMovies = viewModel::deleteSelectedMovies,
                currentScreen = screen,
                onSelectListScreen = viewModel::setScreen,
onEdit = { viewModel.pushScreen(MovieEdit(it)) },
onResetDatabase = { scope.launch(Dispatchers.IO) { viewModel.resetDatabase() } } ) } is MovieEdit -> { MovieEditUi( id = screen.id, fetchMovie = viewModel::getMovie, onMovieChange = viewModel::update ) } is ActorDisplay -> { val selectedIds by viewModel.selectedIdsFlow.collectAsStateWithLifecycle(initialValue = emptySet()) ActorDisplayUi( id = screen.id, fetchActor = viewModel::getActorWithFilmography, onMovieClicked = { viewModel.pushScreen(MovieDisplay(it.id)) }, selectedIds = selectedIds, onClearSelections = viewModel::clearSelectedIds, onSelectionToggle = viewModel::toggleSelection, onDeleteSelectedMovies = viewModel::deleteSelectedMovies, currentScreen = screen, onSelectListScreen = viewModel::setScreen, onResetDatabase = { scope.launch(Dispatchers.IO) { viewModel.resetDatabase() } } ) } is RatingDisplay -> { val selectedIds by viewModel.selectedIdsFlow.collectAsStateWithLifecycle(initialValue = emptySet())
val ratingWithMovies by viewModel.getRatingWithMoviesFlow(screen.id).collectAsStateWithLifecycle(initialValue = null) RatingDisplayUi( ratingWithMovies = ratingWithMovies,
onMovieClicked = { viewModel.pushScreen(MovieDisplay(it.id)) },
selectedIds = selectedIds, onClearSelections = viewModel::clearSelectedIds, onSelectionToggle = viewModel::toggleSelection, onDeleteSelectedMovies = viewModel::deleteSelectedMovies, currentScreen = screen, onSelectListScreen = viewModel::setScreen, onResetDatabase = { scope.launch(Dispatchers.IO) { viewModel.resetDatabase() } }
) } MovieList -> { val movies by viewModel.moviesFlow.collectAsStateWithLifecycle(initialValue = emptyList()) val selectedIds by viewModel.selectedIdsFlow.collectAsStateWithLifecycle(initialValue = emptySet()) MovieListUi( movies = movies, onMovieClicked = { movie -> viewModel.pushScreen(MovieDisplay(movie.id)) }, selectedIds = selectedIds, onClearSelections = viewModel::clearSelectedIds, onSelectionToggle = viewModel::toggleSelection, onDeleteSelectedMovies = viewModel::deleteSelectedMovies, currentScreen = screen, onSelectListScreen = viewModel::setScreen, onResetDatabase = { scope.launch(Dispatchers.IO) { viewModel.resetDatabase() } } ) } ActorList -> { val actors by viewModel.actorsFlow.collectAsStateWithLifecycle(initialValue = emptyList()) val selectedIds by viewModel.selectedIdsFlow.collectAsStateWithLifecycle(initialValue = emptySet()) ActorListUi( actors = actors, onActorClicked = { actor -> viewModel.pushScreen(ActorDisplay(actor.id)) }, selectedIds = selectedIds, onClearSelections = viewModel::clearSelectedIds, onSelectionToggle = viewModel::toggleSelection, onDeleteSelectedActors = viewModel::deleteSelectedActors, currentScreen = screen, onSelectListScreen = viewModel::setScreen, onResetDatabase = { scope.launch(Dispatchers.IO) { viewModel.resetDatabase() } } ) } RatingList -> { val ratings by viewModel.ratingsFlow.collectAsStateWithLifecycle(initialValue = emptyList()) val selectedIds by viewModel.selectedIdsFlow.collectAsStateWithLifecycle(initialValue = emptySet()) RatingListUi( ratings = ratings,
onRatingClicked = { rating -> viewModel.pushScreen(RatingDisplay(rating.id)) },
selectedIds = selectedIds, onClearSelections = viewModel::clearSelectedIds, onSelectionToggle = viewModel::toggleSelection, onDeleteSelectedRatings = viewModel::deleteSelectedRatings, currentScreen = screen, onSelectListScreen = viewModel::setScreen, onResetDatabase = { scope.launch(Dispatchers.IO) { viewModel.resetDatabase() } } ) } } }
CHANGED: /app/src/main/res/values/strings.xml
<resources>
    <string name="app_name">MovieUi3</string>
    <string name="movies">Movies</string>
    <string name="movie">Movie</string>
    <string name="title">Title</string>
    <string name="description">Description</string>
    <string name="reset_database">Reset Database</string>
    <string name="edit">Edit</string>    <string name="loading">(loading)</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="ratings">Ratings</string>
    <string name="rating">Rating</string>
    <string name="actors">Actors</string>
    <string name="actor">Actor</string>
    <string name="name">Name</string>
    <string name="movies_rated">Movies rated %1$s</string>
    <string name="movies_starring">Movies starring %1$s</string>
</resources>
CHANGED: /data/src/main/java/com/androidbyexample/movie/data/MovieDao.kt
package com.androidbyexample.movie.data

import androidx.room.Dao
import androidx.room.Insert
import androidx.room.Query
import androidx.room.Transaction
import androidx.room.Upsertimport 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 suspend fun getRatingWithMovies(id: String): RatingWithMovies

@Transaction @Query("SELECT * FROM RatingEntity WHERE id = :id") abstract fun getRatingWithMoviesFlow(id: String): Flow<RatingWithMovies>
@Query("SELECT * FROM MovieEntity WHERE id = :id") abstract suspend fun getMovie(id: String): MovieEntity
@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 @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)
@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)
@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/movie/repository/MovieDatabaseRepository.kt
package com.androidbyexample.movie.repository

import android.content.Context
import com.androidbyexample.movie.data.MovieDao
import com.androidbyexample.movie.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 suspend fun getMovie(id: String): MovieDto = dao.getMovie(id).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 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 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 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/movie/repository/MovieRepository.kt
package com.androidbyexample.movie.repository

import com.androidbyexample.movie.data.ActorEntityimport com.androidbyexample.movie.data.MovieEntityimport com.androidbyexample.movie.data.RatingEntityimport com.androidbyexample.movie.data.RoleEntityimport 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>
suspend fun getMovie(id: String): MovieDto
suspend fun getRatingWithMovies(id: String): RatingWithMoviesDto suspend fun getMovieWithCast(id: String): MovieWithCastDto suspend fun getActorWithFilmography(id: String): ActorWithFilmographyDto 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() }