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 fresh 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/compose/movies/screens/MovieDisplay.kt
// ...

@OptIn(ExperimentalMaterial3Api::class) // for TopAppBar
@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)
        }
    }

    Scaffold(
        // ...
        modifier = modifier,
    ) { innerPadding ->
        movieWithCast?.let { movieWithCast ->
            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,
                            )
                        )
                    }
            }
        }
    }
}

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 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/compose/movies/screens/Screens.kt
// ...
sealed interface Screen
data object MovieList: Screen
//data class MovieDisplay(val movie: MovieDto): Screen
data class MovieDisplay(val id: String): Screen
show in full file app/src/main/java/com/androidbyexample/compose/movies/screens/Ui.kt
// ...

@Composable
fun Ui(
    // ...
) {
    // ...
    when (val screen = viewModel.currentScreen) {
        // ...
        is MovieDisplay -> {
            MovieDisplayUi(
//              movie = screen.movie,
                id = screen.id,
                fetchMovie = viewModel::getMovieWithCast,
                modifier = modifier,
            )
        }
        MovieList -> {
            val movies by viewModel.moviesFlow.collectAsStateWithLifecycle(
                initialValue = emptyList()
            )

            MovieListUi(
                movies = movies,
                modifier = modifier,
                onResetDatabase = {
                    scope.launch (Dispatchers.IO) {
                        viewModel.resetDatabase()
                    }
                },
                onMovieClicked = { movie ->
//                  viewModel.pushScreen(MovieDisplay(movie))
                    viewModel.pushScreen(MovieDisplay(movie.id))
                }
            )
        }
    }
}

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)


All code changes

CHANGED: app/src/main/java/com/androidbyexample/compose/movies/screens/MovieDisplay.kt
package com.androidbyexample.compose.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.compose.movies.R
import com.androidbyexample.compose.movies.components.Display
import com.androidbyexample.compose.movies.components.Label
import com.androidbyexample.compose.movies.repository.MovieDto
import com.androidbyexample.compose.movies.repository.MovieWithCastDto
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.withContext

@OptIn(ExperimentalMaterial3Api::class) // for TopAppBar
@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) } }
Scaffold( topBar = { TopAppBar( title = {
// Text(text = movie.title) Text(text = movieWithCast?.movie?.title ?: stringResource(R.string.loading))
} ) }, modifier = modifier, ) { innerPadding ->
movieWithCast?.let { movieWithCast -> 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/compose/movies/screens/Screens.kt
package com.androidbyexample.compose.movies.screens

//import com.androidbyexample.compose.movies.repository.MovieDto
//
sealed interface Screen
data object MovieList: Screen
//data class MovieDisplay(val movie: MovieDto): Screen data class MovieDisplay(val id: String): Screen
CHANGED: app/src/main/java/com/androidbyexample/compose/movies/screens/Ui.kt
package com.androidbyexample.compose.movies.screens

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

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

val scope = rememberCoroutineScope()
when (val screen = viewModel.currentScreen) { null -> onExit() is MovieDisplay -> { MovieDisplayUi(
// movie = screen.movie, id = screen.id, fetchMovie = viewModel::getMovieWithCast, modifier = modifier, ) } MovieList -> {
val movies by viewModel.moviesFlow.collectAsStateWithLifecycle( initialValue = emptyList() ) MovieListUi( movies = movies,
modifier = modifier,
onResetDatabase = { scope.launch (Dispatchers.IO) { viewModel.resetDatabase() } }, onMovieClicked = { movie -> // viewModel.pushScreen(MovieDisplay(movie)) viewModel.pushScreen(MovieDisplay(movie.id))
}
) } } }
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="loading">…Loading…</string>
    <string name="cast">Cast</string>
    <string name="cast_entry">%1$s: %2$s</string>
</resources>