Movies UI - Updates

Deleting from the display screens

Now that we have selectable lists inside the display screens, let's allow deletions.

First, let's add icons to the items in the display screens:

This allows clicking on the icons to select, in addition to long-pressing the cards.

Note

For the movie and actor display screens, the data we're displaying is role information, and we're using the derived id for these objects in RoleWithMovieDto and RoleWithActorDto. This will not work for RoleEntity deletion. To properly delete the roles we would need to track the actor id, movie id, and order in credits so we can match them up for deletion. I'm leaving that as the obligatory "exercise for the interested reader", and we'll only focus on deleting movies via the rating display screen.

To test deletion, we:

  1. Go to the ratings list
  2. Select PG-13
  3. Click on the icon next to "Hobbs and Shaw"
  4. Click the trash can on the top bar to delete the movie And... nothing happens! Or at least nothing seems to happen. Deletion doesn't seem to work
  5. But if we click on the ratings list
  6. Then go back into PG-13

We see that "Hobbs and Shaw" has indeed been deleted.

What's happening?

Once again, it's all down to where the state is being set/read.

  1. When we select PG-13, we pass the id of the PG-13 entity into RatingDisplayUi
  2. RatingDisplayUi fetches a RatingWithMoviesDto using that id inside a LaunchedEffect
  3. When we delete a movie from the database, the only thing that changes is the data in the MovieEntity table, emitting a new MovieList to the Flow for all movies.
  4. We're not collecting that Flow for this screen; nothing triggers a refetch of the currently-displayed rating (the rating id hasn't changed)
  5. We're stuck with the previously-fetched RatingWithMoviesDto.

How do we fix this? We need something to tell us that our local movie list has changed.

We can:

  1. Pass in the RoleWithMoviesDto instead of fetching it (and get rid of the fetcher function)
  2. Create a DAO function to get a rating with its movies using a Flow.
  3. Forward that function through the repository, database repository implementation, and view model (automatically done via delegation)
  4. Collect the Flow and pass the result into the rating display

Poof! It works now!

The trick here was using data that's automatically updated, sending us a new RoleWithMoviesDto when a movie is deleted. We collect that flow in Ui, updating the value that we pass into the rating display, triggering recomposition.

The list will also display when the reset-database button on the top bar is pressed.

!Delete updates the Ui

  • add edit screen with edit button for movie display

  • add edit screens for actor and rating

  • use save button for actor
  • use back nav for rating
  • add + to lists to create new items
  • jumps to edit screen

  • skip adding roles/filmography for now

  • do with dialogs?

Code Changes

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

import androidx.compose.foundation.clickableimport androidx.compose.foundation.layout.Rowimport androidx.compose.foundation.layout.paddingimport androidx.compose.material.icons.Iconsimport androidx.compose.material.icons.filled.Movieimport androidx.compose.material3.Iconimport 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.Alignmentimport androidx.compose.ui.Modifierimport androidx.compose.ui.res.stringResource
import androidx.compose.ui.unit.dpimport 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,
        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/MovieDisplayUi.kt
package com.androidbyexample.movie.screens

import androidx.compose.foundation.clickableimport androidx.compose.foundation.layout.Rowimport androidx.compose.foundation.layout.paddingimport androidx.compose.material.icons.Iconsimport androidx.compose.material.icons.filled.Movieimport androidx.compose.material3.Iconimport 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.Alignmentimport androidx.compose.ui.Modifierimport androidx.compose.ui.res.stringResource
import androidx.compose.ui.unit.dpimport 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,
) {
    var movieWithCast by remember { mutableStateOf<MovieWithCastDto?>(null) }
    LaunchedEffect(key1 = id) {
        withContext(Dispatchers.IO) {
            movieWithCast = fetchMovie(id)
        }
    }
    ListScaffold(
        titleId = R.string.rating,
        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) } ) }
CHANGED: /app/src/main/java/com/androidbyexample/movie/screens/RatingDisplayUi.kt
package com.androidbyexample.movie.screens

import androidx.compose.foundation.clickableimport androidx.compose.foundation.layout.Rowimport androidx.compose.foundation.layout.paddingimport androidx.compose.material.icons.Iconsimport androidx.compose.material.icons.filled.Movieimport androidx.compose.material3.Iconimport 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.setValueimport androidx.compose.ui.Alignmentimport androidx.compose.ui.Modifierimport androidx.compose.ui.res.stringResource
import androidx.compose.ui.unit.dpimport com.androidbyexample.movie.R
import com.androidbyexample.movie.components.Display
import com.androidbyexample.movie.components.Label
import com.androidbyexample.movie.repository.MovieDto
import com.androidbyexample.movie.repository.RatingWithMoviesDto
//import kotlinx.coroutines.Dispatchers//import kotlinx.coroutines.withContext
@Composable
fun RatingDisplayUi(
// id: String,// fetchRating: suspend (String) -> RatingWithMoviesDto, ratingWithMovies: RatingWithMoviesDto?,
onMovieClicked: (MovieDto) -> Unit,
selectedIds: Set<String>, onSelectionToggle: (id: String) -> Unit, onClearSelections: () -> Unit, onDeleteSelectedMovies: () -> Unit,
currentScreen: Screen, onSelectListScreen: (Screen) -> Unit, onResetDatabase: () -> Unit,
) { // var ratingWithMovies by remember { mutableStateOf<RatingWithMoviesDto?>(null) }// LaunchedEffect(key1 = id) {// withContext(Dispatchers.IO) {// ratingWithMovies = fetchRating(id)// }// } ListScaffold( titleId = R.string.rating, items = ratingWithMovies?.movies?.sortedBy { it.title } ?: emptyList(), onItemClicked = onMovieClicked, selectedIds = selectedIds, onSelectionToggle = onSelectionToggle, onClearSelections = onClearSelections, onDeleteSelectedItems = onDeleteSelectedMovies, currentScreen = currentScreen, onSelectListScreen = onSelectListScreen, onResetDatabase = onResetDatabase, itemContent = { movie ->
// Display(// text = movie.title Row( verticalAlignment = Alignment.CenterVertically, modifier = Modifier.padding(8.dp) ) { Icon( imageVector = Icons.Default.Movie, contentDescription = stringResource(id = R.string.movie), modifier = Modifier.clickable { onSelectionToggle(movie.id) } ) Display(text = movie.title) }
}, topContent = { ratingWithMovies?.let { ratingWithMovies -> Label(textId = R.string.name) Display(text = ratingWithMovies.rating.name) Label(textId = R.string.description) Display(text = ratingWithMovies.rating.description) Label( text = stringResource( id = R.string.movies_rated, ratingWithMovies.rating.name ) ) } } ) }
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 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( // id = screen.id,// fetchRating = viewModel::getRatingWithMovies, 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: /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 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>
@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) @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/ActorDto.kt
package com.androidbyexample.movie.repository

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

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

internal fun ActorEntity.toDto() =
    ActorDto(id = id, name = name)
internal fun ActorDto.toEntity() =
    ActorEntity(id = id, name = name)

data class ActorWithFilmographyDto(
    val actor: ActorDto,
    val filmography: List<RoleWithMovieDto>,
)

data class RoleWithMovieDto(
    val movie: MovieDto,
    val character: String,
    val orderInCredits: Int,
): HasId {
override val id: String get() = "${movie.id}:$character"
} internal fun RoleWithMovie.toDto() = RoleWithMovieDto( movie = movie.toDto(), character = role.character, orderInCredits = role.orderInCredits, ) internal fun ActorWithFilmography.toDto() = ActorWithFilmographyDto( actor = actor.toDto(), filmography = rolesWithMovies.map { it.toDto() } )
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.Flowimport 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 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 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/MovieDto.kt
package com.androidbyexample.movie.repository

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

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

internal fun MovieEntity.toDto() =
    MovieDto(id = id, title = title, description = description, ratingId = ratingId)
internal fun MovieDto.toEntity() =
    MovieEntity(id = id, title = title, description = description, ratingId = ratingId)

data class MovieWithCastDto(
    val movie: MovieDto,
    val cast: List<RoleWithActorDto>,
)

data class RoleWithActorDto(
    val actor: ActorDto,
    val character: String,
    val orderInCredits: Int,
): HasId {
override val id: String get() = "${actor.id}:$character"
} internal fun RoleWithActor.toDto() = RoleWithActorDto( actor = actor.toDto(), character = role.character, orderInCredits = role.orderInCredits, ) internal fun MovieWithCast.toDto() = MovieWithCastDto( movie = movie.toDto(), cast = rolesWithActors.map { it.toDto() } )
CHANGED: /repository/src/main/java/com/androidbyexample/movie/repository/MovieRepository.kt
package com.androidbyexample.movie.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>
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 deleteMoviesById(ids: Set<String>) suspend fun deleteActorsById(ids: Set<String>) suspend fun deleteRatingsById(ids: Set<String>) suspend fun resetDatabase() }