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, thankyouverymuch).

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!

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'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)

Code Changes

CHANGED: /app/src/main/java/com/androidbyexample/movie/screens/MovieDisplayUi.kt
package com.androidbyexample.movie.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.LaunchedEffectimport androidx.compose.runtime.getValueimport androidx.compose.runtime.mutableStateOfimport androidx.compose.runtime.rememberimport androidx.compose.runtime.setValueimport androidx.compose.ui.Modifier
import androidx.compose.ui.res.stringResourceimport com.androidbyexample.movie.R
import com.androidbyexample.movie.components.Display
import com.androidbyexample.movie.components.Label
//import com.androidbyexample.movie.repository.MovieDtoimport com.androidbyexample.movie.repository.MovieWithCastDtoimport kotlinx.coroutines.Dispatchersimport kotlinx.coroutines.withContext
@OptIn(ExperimentalMaterial3Api::class)
@Composable
fun MovieDisplayUi(
// movie: MovieDto, id: String,
fetchMovie: suspend (String) -> MovieWithCastDto,
) {
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))
} ) }
) { paddingValues ->
movieWithCast?.let { movieWithCast -> Column( modifier = Modifier .padding(paddingValues) .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/movie/screens/Screens.kt
package com.androidbyexample.movie.screens

import com.androidbyexample.movie.repository.MovieDto

sealed interface Screen object MovieList: Screen
//data class MovieDisplay(val movie: MovieDto): Screendata class MovieDisplay(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 -> { // MovieDisplayUi(screen.movie) MovieDisplayUi(
id = screen.id, fetchMovie = viewModel::getMovieWithCast,
) } MovieList -> {
val movies by viewModel.moviesFlow.collectAsStateWithLifecycle(initialValue = emptyList())
MovieListUi( movies = movies, onMovieClicked = { movie -> // viewModel.pushScreen(MovieDisplay(movie)) viewModel.pushScreen(MovieDisplay(movie.id)) },
onResetDatabase = { scope.launch(Dispatchers.IO) { viewModel.resetDatabase() } }
) } }
}
CHANGED: /app/src/main/res/values/strings.xml
<resources> <string name="app_name">MovieUi1</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="loading">(loading)</string> <string name="cast">Cast</string> <string name="cast_entry">%1$s: %2$s</string></resources>