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
- Upsert functions in the DAO - these will insert if it doesn't exist, update if it does.
- Pass through in the repository and repository database implementation
- The view model automatically delegates to the repository, but we need to add
update
Flow
s (queues) that will debounce and send the updates
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.
- Add
upsert
to DAO - Pass through the repository
- Pass through the repository database implementation
- Add new screen data
- Add an event function to
ListScaffold
to tell the caller to start editing. Note the definition of this function:
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
ifonEdit
is not null. The button will callonEdit
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 thelet
to be null, resulting in passingnull
to theListScaffold
- 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 thelet
define a lambda expression as the last expression in thelet
, which will be returned from thelet
and passed to theListScaffold
. - The
onEdit
lambda we created in thatlet
takes the id from the movie in the currently-displayedMovieWithRoleDto
, and tells the caller to edit it.
- If we don't have a movie to edit,
-
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 viaonMovieChange
so the caller can make a database update. - To finish things up, we set up
onEdit
inUi
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 = { 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): Screendata 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,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): MovieDtosuspend 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() }