Initial Movies UI

Create initial screens

Let's create a starter user interface!

In this step, we'll create placeholder screens and simple navigation.

Note

We'll be using a trivial custom navigation scheme for this class. It's not robust, though; you should look at available navigation frameworks (such as Navigating with Compose or others. These can be complex, and everyone has their own preferences on which are the best. I didn't want to spend a lot of time on navigation in this class, so I'm keeping it to a simple stack-based approach.

To implement our simple navigation, we need to

  • Define state representing our screens
  • Create a private screen stack in the view model
  • Expose the current top-of-stack screen for the UI to consume
  • Choose the screen to display based on the screen state

First, we create screen state using a Kotlin sealed interface. Sealed interfaces limit possible implementing classes or objects to only those defined in the current module. This is useful in applications or libraries because they know exhaustively which possible subclasses exist, and, in the case of a library, no external users can create new subclasses or implementations.

Note

We're creating a file to hold all of these definitions together. In Kotlin, you can define multiple public types inside a single file (vs Java, where you can only have a single public type per file.)

We're using a data class to represent a movie-display screen. Data classes automatically generate equals, hashCode, toString and some other interesting functions for all properties defined in their primary constructor. We'll use this to hold onto the movie that was selected.

Holding onto the movie is not a good idea. If it changes in the data store, we won't be able to automatically load the changes in the UI. We'll fix this when we set up our database. For now, we pass the movie for convenience, as we're focusing on the user interface in this module.

We use a Kotlin object for the movie list state. Because the movie list will just display all movies, we don't need to keep any state in the screen instance for it. Kotlin objects are singletons; you never create instances of them and can access them globally. We could use a class here (without data) and create instances, but that doesn't do anything helpful, as all instances would be effectively equal.

In the view model, we add a screen stack to track screens that the user has visited. We keep this stack private; the IU doesn't need to know about it. This stack defines a set() function that updates the backing field, and sets the current screen. The current screen is delegated to a Compose MutableState so it can be accessed and observed for changes from the UI.

Note the private set on currentScreen - this allows us to modify it from inside the view model, but it cannot be modified from outside.

The last thing we need to do in the view model, is add external stack support so the UI can navigate to a new screen or go back to an existing screen. When these functions modify the stack, its set() updates currentScreen automatically.

Now the placeholder screens. We define simple movie display and movie list screens.

Our MovieDisplayUi takes a Movie as a parameter and displays a single Text to temporarily represent the screen. MovieListUi takes a list of movies and a onMovieClicked callback to indicate to the caller that a movie was selected. For now, we just display "Movie list", and when it's clicked, tell the caller that the first movie in the list was selected.

Finally, we define a starter UI. I like to define a top-level composable function as a starting point. All it does is collect data from the view model and pass whatever is needed to the current screen. If the current screen is null, that means we've popped all screens off the stack and should exit the application. This is done by passing an event function that calls finish() in the Activity, ending the application (because we only have a single activity in the application.)

Note

Following this approach, the top-level UI function should be the only composable function to be passed the view model. This ensures that lower-level composable functions are more easily testable, as you can pass them just the data they need, and not a view model that needs to be set up.

This application can now be run, showing the placeholder movie list screen. When clicked, the placeholder movie display screen is pushed on the stack and becomes visible. When back is pressed, we return to the movie list screen. Pressing back again exits the application.

Code Changes

CHANGED: /app/src/main/java/com/androidbyexample/movieui1/MainActivity.kt
package com.androidbyexample.movieui1

import android.os.Bundle
import androidx.activity.ComponentActivity
import androidx.activity.compose.setContent
import androidx.activity.viewModels
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Surface
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
import androidx.compose.ui.tooling.preview.Preview
import com.androidbyexample.movieui1.screens.Uiimport com.androidbyexample.movieui1.ui.theme.MovieUi1Theme

class MainActivity : ComponentActivity() {
private val viewModel by viewModels<MovieViewModel>()
override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) setContent { MovieUi1Theme { // A surface container using the 'background' color from the theme Surface( modifier = Modifier.fillMaxSize(), color = MaterialTheme.colorScheme.background ) {
// Greeting("Android") Ui(viewModel) { finish() }
} } } } } //@Composable//fun Greeting(name: String, modifier: Modifier = Modifier) {// Text(// text = "Hello $name!",// modifier = modifier// )//}//@Preview(showBackground = true)//@Composable//fun GreetingPreview() {// MovieUi1Theme {// Greeting("Android")// }//}
CHANGED: /app/src/main/java/com/androidbyexample/movieui1/MovieViewModel.kt
package com.androidbyexample.movieui1

import androidx.compose.runtime.getValueimport androidx.compose.runtime.mutableStateOfimport androidx.compose.runtime.setValueimport androidx.lifecycle.ViewModel
import com.androidbyexample.movieui1.screens.MovieListimport com.androidbyexample.movieui1.screens.Screen
class MovieViewModel: ViewModel() {
private var screenStack = listOf<Screen>(MovieList) set(value) { field = value
currentScreen = value.lastOrNull()
}
var currentScreen by mutableStateOf<Screen?>(MovieList) private set
fun pushScreen(screen: Screen) { screenStack = screenStack + screen } fun popScreen() { screenStack = screenStack.dropLast(1) }
val movies: List<Movie> = listOf( Movie("The Transporter", "Jason Statham kicks a guy in the face"), Movie("Transporter 2", "Jason Statham kicks a bunch of guys in the face"), Movie("Hobbs and Shaw", "Cars, Explosions and Stuff"), Movie("Jumanji - Welcome to the Jungle", "The Rock smolders"), ) }
ADDED: /app/src/main/java/com/androidbyexample/movieui1/screens/MovieDisplayUi.kt
package com.androidbyexample.movieui1.screensimport androidx.compose.material3.Textimport androidx.compose.runtime.Composableimport com.androidbyexample.movieui1.Movie
@Composablefun MovieDisplayUi( movie: Movie) { Text(text = "Movie Display: ${movie.title}")}
ADDED: /app/src/main/java/com/androidbyexample/movieui1/screens/MovieListUi.kt
package com.androidbyexample.movieui1.screensimport androidx.compose.foundation.clickableimport androidx.compose.material3.ExperimentalMaterial3Apiimport androidx.compose.material3.Textimport androidx.compose.runtime.Composableimport androidx.compose.ui.Modifierimport com.androidbyexample.movieui1.Movie
@Composablefun MovieListUi( movies: List<Movie>, onMovieClicked: (Movie) -> Unit,) { Text( text = "Movie List",
modifier = Modifier.clickable { onMovieClicked(movies[0]) }
)}
ADDED: /app/src/main/java/com/androidbyexample/movieui1/screens/Screens.kt
package com.androidbyexample.movieui1.screensimport com.androidbyexample.movieui1.Movie
sealed interface Screenobject MovieList: Screendata class MovieDisplay(val movie: Movie): Screen
ADDED: /app/src/main/java/com/androidbyexample/movieui1/screens/Ui.kt
package com.androidbyexample.movieui1.screensimport androidx.activity.compose.BackHandlerimport androidx.compose.runtime.Composableimport com.androidbyexample.movieui1.MovieViewModel
@Composablefun Ui( viewModel: MovieViewModel, onExit: () -> Unit,) {
BackHandler { viewModel.popScreen() }
when (val screen = viewModel.currentScreen) { null -> onExit() is MovieDisplay -> { MovieDisplayUi(screen.movie) } MovieList -> { MovieListUi(viewModel.movies) { movie -> viewModel.pushScreen(MovieDisplay(movie)) } } }
}