Movies Database

Fetch data for movie display

MovieDisplayUi still works the same way it used to. We wrap the selected MovieDto in a MovieDisplay instance and push it on the screen stack. The Ui composable gets the current screen and calls MovieDisplayUi passing in the movie.

There are some problems with this approach:

  • If the user jumps to the home screen and later, back into the application, it's possible that Android may have disposed the application stack. You may want to persist the data for the current screen. That data may be stale by the time you return.

  • Once on the screen with that data, something else may change the data in the database, and we only have a fixed view of the data from the time the list was displayed.

We'll solve the second problem later. For the first problem, we won't persist the data here, but we'll change what's being passed to just the ID of the data, and fetch the data inside the screen. Then, if we decide to persist the data as the user exits, we won't have stale data, as we'll fetch it freshly each time we display it.

To do this, we'll use a controlled-side effect called LaunchedEffect in our screen composable. LaunchedEffect launches a coroutine to perform some processing, and that coroutine keeps running until:

  • it finishes, or
  • its parent composable is no longer part of the UI tree, or
  • its key changes, in which case the current run is canceled and the code in its lambda is re-executed

We start by passing a movie id instead of a movie itself, and a fetch function to allow the composable to fetch the movie when needed.

We add a Launched Effect to fetch the movie. On initial composition, this starts a coroutine to perform the fetch, which will run and return a MovieDto. On recomposition, it will only restart if the id has changed (or the MovieDisplayUi was removed from the UI tree and re-added.) If the user selected a different movie fast enough, the existing fetch run would be canceled and a few fetch started.

Because we might not yet have a title, we need to provide a fallback, which we can easily do using our friend the "elvis operator" ?: (which if you turn your head 90 degrees to the left and squint looks a little like Elvis Presley's eyes and hair, ahthankyouverymuch).

Using the let function allows us to easily omit the user interface while the movie is loading. Note that we've also added the cast information to the display, as it's now available!

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

@OptIn(ExperimentalMaterial3Api::class)
@Composable
fun MovieDisplayUi(
//  movie: MovieDto,
    id: String,
    fetchMovie: suspend (String) -> MovieWithCastDto,
    modifier: Modifier = Modifier,
) {
    var movieWithCast by remember { mutableStateOf<MovieWithCastDto?>(null) }
    LaunchedEffect(key1 = id) {
        withContext(Dispatchers.IO) {
            movieWithCast = fetchMovie(id)
        }
    }

    movieWithCast?.let { movieWithCast ->
        Scaffold(
            topBar = {
                // ...
    }
}

We can also remove the temporary Parcelize support that we added:

show in full file repository/build.gradle.kts
plugins {
    alias(libs.plugins.android.library)
//  id("kotlin-parcelize")
}

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

//@Parcelize
data class MovieDto(
    val id: String,
    val title: String,
    val description: String,
    val ratingId: String,
//): Parcelable
)

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

The drawback to this approach is that the screen will likely blink from a blank screen to one containing the movie data. The main way around this is to perform the data fetch outside the function. We'll come back to this approach later.

We've added some code to display the cast list, which requires a couple of strings. Note that cast_entry is a template string that allows us to pass in the role and actor name

show in full file app/src/main/res/values/strings.xml
<resources>
    // ...
    <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>
</resources>

We modify MovieDisplay to take a String id instead of the movie itself, and pass it in the call to MovieDisplayUi, along with an event function. We also need to tweak the MovieDisplay created when the user clicks on a movie in the list.

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

@Parcelize
//data class MovieDisplay(val movie: MovieDto): Screen
data class MovieDisplay(val id: String): Screen
show in full file app/src/main/java/com/androidbyexample/movies/screens/Ui.kt
// ...

@OptIn(ExperimentalMaterial3AdaptiveApi::class)
@Composable
fun Ui(
    // ...
) {
    // ...
    NavDisplay(
        // ...
        entryProvider = entryProvider {
            entry<MovieList>(
                // ...
            ) {
                // ...
                MovieListUi(
                    movies = movies,
                    onMovieClicked = { movie ->
//                      viewModel.pushScreen(MovieDisplay(movie))
                        viewModel.pushScreen(MovieDisplay(movie.id))
                    },
                    onResetDatabase = viewModel::doResetDatabase,
                )
            }
            entry<MovieDisplay>(
                metadata = ListDetailSceneStrategy.detailPane()
            ) { key ->
                MovieDisplayUi(
//                  movie = key.movie,
                    id = key.id,
                    fetchMovie = viewModel::getMovieWithCast,
                )
            }
        }
    )
}

We're using a Kotlin function reference here. If the signature of a function matches the required functional type, we can just pass in an object::function specification for it. In this case,

fetchMovie = viewModel::getMovieWithCast

is effectively the same as

fetchMovie = { viewModel.getMovieWithCast() }

(I say "effectively" because the second example creates an additional function layer to call, which may be optimized away by the compiler)

Finally, we add a loop to display the cast

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

@OptIn(ExperimentalMaterial3Api::class)
@Composable
fun MovieDisplayUi(
    // ...
) {
    // ...
    movieWithCast?.let { movieWithCast ->
        Scaffold(
            // ...
        ) { innerPadding ->
            Column(
                // ...
            ) {
                // ...
                Display(text = movieWithCast.movie.description)
                Label(textId = R.string.cast)
                movieWithCast
                    .cast
                    .sortedBy { it.orderInCredits }
                    .forEach { role ->
                        Display(
                            text = stringResource(
                                R.string.cast_entry,
                                role.character,
                                role.actor.name,
                            )
                        )
                    }
            }
        }
    }
}

We're using a template string named cast_entry in app/src/main/res/values/strings.xml by passing in the character and actor name to stringResource. While it's not critical here, some structuring of combinations of strings may need to change for different locales, and can be handled nicely by using different resource values for the formats.


All code changes

CHANGED: app/src/main/java/com/androidbyexample/movies/screens/MovieDisplayUi.kt
package com.androidbyexample.movies.screens

import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.rememberScrollState
import androidx.compose.foundation.verticalScroll
import androidx.compose.material3.ExperimentalMaterial3Api
import androidx.compose.material3.Scaffold
import androidx.compose.material3.Text
import androidx.compose.material3.TopAppBar
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.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.repository.MovieDto
import com.androidbyexample.movies.repository.MovieWithCastDto
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.withContext

@OptIn(ExperimentalMaterial3Api::class)
@Composable
fun MovieDisplayUi(
// movie: MovieDto, id: String, fetchMovie: suspend (String) -> MovieWithCastDto,
modifier: Modifier = Modifier,
) {
var movieWithCast by remember { mutableStateOf<MovieWithCastDto?>(null) } LaunchedEffect(key1 = id) { withContext(Dispatchers.IO) { movieWithCast = fetchMovie(id) } }
movieWithCast?.let { movieWithCast ->
Scaffold( topBar = { TopAppBar( title = { // Text(text = movie.title) Text(text = movieWithCast.movie.title) } ) }, modifier = modifier, ) { innerPadding -> Column( modifier = modifier .padding(innerPadding) .verticalScroll(rememberScrollState()) ) { Label(textId = R.string.title) // Display(text = movie.title) Display(text = movieWithCast.movie.title) Label(textId = R.string.description) // Display(text = movie.description) Display(text = movieWithCast.movie.description)
Label(textId = R.string.cast) movieWithCast .cast .sortedBy { it.orderInCredits } .forEach { role -> Display( text = stringResource( R.string.cast_entry, role.character, role.actor.name, ) ) }
} } } }
CHANGED: app/src/main/java/com/androidbyexample/movies/screens/Screens.kt
package com.androidbyexample.movies.screens

import android.os.Parcelable
//import com.androidbyexample.movies.repository.MovieDto
import kotlinx.parcelize.Parcelize

sealed interface Screen: Parcelable

@Parcelize
data object MovieList: Screen

@Parcelize //data class MovieDisplay(val movie: MovieDto): Screen data class MovieDisplay(val id: String): Screen
CHANGED: app/src/main/java/com/androidbyexample/movies/screens/Ui.kt
package com.androidbyexample.movies.screens

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

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

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

    NavDisplay(
        backStack = backStack,
        onBack = viewModel::popScreen,
        sceneStrategies = listOf(listDetailStrategy),
        entryProvider = entryProvider {
            entry<MovieList>(
                metadata = ListDetailSceneStrategy.listPane()
            ) {
val movies by viewModel.moviesFlow.collectAsStateWithLifecycle( initialValue = emptyList() ) MovieListUi( movies = movies,
onMovieClicked = { movie -> // viewModel.pushScreen(MovieDisplay(movie)) viewModel.pushScreen(MovieDisplay(movie.id)) },
onResetDatabase = viewModel::doResetDatabase,
) } entry<MovieDisplay>( metadata = ListDetailSceneStrategy.detailPane() ) { key -> MovieDisplayUi( // movie = key.movie, id = key.id, fetchMovie = viewModel::getMovieWithCast, ) }
} ) }
CHANGED: app/src/main/res/values/strings.xml
<resources>
    <string name="app_name">Movies</string>
    <string name="movies">Movies</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>
</resources>
CHANGED: repository/build.gradle.kts
plugins { alias(libs.plugins.android.library) // id("kotlin-parcelize") }
android { namespace = "com.androidbyexample.movies.repository" compileSdk { version = release(libs.versions.compileSdk.get().toInt()) } defaultConfig { minSdk = libs.versions.minSdk.get().toInt() testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner" consumerProguardFiles("consumer-rules.pro") } compileOptions { sourceCompatibility = JavaVersion.valueOf(libs.versions.javaVersion.get()) targetCompatibility = JavaVersion.valueOf(libs.versions.javaVersion.get()) } } dependencies {
implementation(project(":data"))
implementation(libs.androidx.appcompat) implementation(libs.androidx.core.ktx) implementation(libs.material) testImplementation(libs.junit) androidTestImplementation(libs.androidx.espresso.core) androidTestImplementation(libs.androidx.junit) }
CHANGED: repository/src/main/java/com/androidbyexample/movies/repository/MovieDto.kt
package com.androidbyexample.movies.repository

//import android.os.Parcelable
import com.androidbyexample.movies.data.MovieEntity
import com.androidbyexample.movies.data.MovieWithCast
import com.androidbyexample.movies.data.RoleWithActor
//import kotlinx.parcelize.Parcelize

//@Parcelize data class MovieDto( val id: String, val title: String, val description: String, val ratingId: String, //): Parcelable )
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, ) 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() } )