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 = {) { paddingValues ->} ) }// Text(text = movie.title)Text(text = movieWithCast?.movie?.title ?: stringResource(R.string.loading))}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.MovieDtosealed 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>