Initial Movies UI
Refactor Scaffold Use
Before we flesh out the screens, we'll make a couple of helper functions and refactor our use of
the Scaffold
.
First, we create a couple of helper components for consistency. Display()
and Label()
are just Text()
s with some parameters set for styling and padding. Using this
approach of creating wrapper functions gives us "extended" components.
show in full file app/src/main/java/com/androidbyexample/compose/movies/components/Display.kt
// ...
import androidx.compose.ui.unit.dp
@Composable
fun Display(
text: String,
modifier: Modifier = Modifier,
) {
Text(
text = text,
style = MaterialTheme.typography.titleLarge,
modifier = modifier
.padding(8.dp)
.padding(start = 16.dp)
.fillMaxWidth(),
)
}
show in full file app/src/main/java/com/androidbyexample/compose/movies/components/Label.kt
// ...
import androidx.compose.ui.unit.dp
@Composable
fun Label(
@StringRes textId: Int,
modifier: Modifier = Modifier,
) {
Text(
text = stringResource(id = textId),
style = MaterialTheme.typography.titleMedium,
modifier = modifier
.padding(8.dp)
.fillMaxWidth(),
)
}
Note
Note how Display
has two padding modifiers.
These are applied in the order they're defined. In this case,
we apply an 8dp padding all around, and then an extra 16dp padding only at
the start of the text. This causes the text to appear indented.
If we did something like
Modifier
.padding(8.dp)
.border(2.dp, Color.Blue, RoundedCornerShape(4.dp))
.padding(8.dp)
we'd see 8dp padding, a blue border inside it, and 8dp more padding inside the border. This is a much simpler system than the margin/padding combinations that existed with the old "views" UI approach.
We're going to want more control of the Scaffold
ing, per-screen, so we'll remove it
from MainActivity
show in full file app/src/main/java/com/androidbyexample/compose/movies/MainActivity.kt
// ...
class MainActivity : ComponentActivity() {
// ...
override fun onCreate(savedInstanceState: Bundle?) {
// ...
setContent {
MoviesTheme {
// Scaffold(modifier = Modifier.fillMaxSize()) { innerPadding ->
// Ui (
Ui(
viewModel = viewModel,
// modifier = Modifier.padding(innerPadding),
) {
finish()
}
// }
}
}
}
}
and add it in our MovieDisplay()
show in full file app/src/main/java/com/androidbyexample/compose/movies/screens/MovieDisplay.kt
// ...
@OptIn(ExperimentalMaterial3Api::class) // for TopAppBar
@Composable
fun MovieDisplayUi(
// ...
modifier: Modifier = Modifier,
) {
Scaffold(
topBar = {
TopAppBar(
title = {
Text(text = movie.title)
}
)
},
modifier = modifier,
) { innerPadding ->
Text(
text = "Movie Display: ${movie.title}",
// modifier = modifier,
modifier = Modifier.padding(innerPadding),
)
}
}
and our MovieList()
show in full file app/src/main/java/com/androidbyexample/compose/movies/screens/MovieList.kt
// ...
@OptIn(ExperimentalMaterial3Api::class) // for TopAppBar
@Composable
fun MovieListUi(
// ...
onMovieClicked: (Movie) -> Unit,
) {
Scaffold(
topBar = {
TopAppBar(
title = {
Text(text = stringResource(R.string.movies))
}
)
},
modifier = modifier,
) { innerPadding ->
Text(
text = "Movie List",
// modifier = modifier.clickable {
modifier = Modifier
.padding(innerPadding)
.clickable {
onMovieClicked(movies[0])
}
)
}
}
Note that this also required moving the Modifier.padding()
setup.
The Scaffold
is a
"Slot API"
composable function. This is an implementation of the Template Method Pattern,
an algorithm with replaceable steps. The Scaffold
defines an algorithm for managing
several "slots" on the screen, areas like "stuff at the top", "stuff at the bottom",
"stuff in between", etc. Scaffold
defines the layout algorithm for measuring and placing
elements in the specified position; you define what goes in those positions.
In this example, we define topBar
, passing a lambda for which elements should appear in that
section of the Scaffold
. When the scaffold is performing its layout work, it calls that lambda
to obtain the components, measures them, and places them. (Note that some slotted apis may
conditionally execute lambdas such as these; for example, if you define a navigation drawer,
it might not appear unless the user has swiped in from the side.)
Our topBar
declares that a TopAppBar
should appear. This is a common tool bar
element, which can contain a title, navigation icon, and action icons. We'll do
much more with it in later modules. Here we're just setting a title.
Note
Pay close attention to how the passed-in Modifier
is being used. The caller may pass one
in to control how the UI for MovieDisplay()
or MovieList()
appears. We pass the modifier
parameter to the Scaffold
, as that's the top-level Composable inside these functions.
In the Text()
, we start a new Modifier
by using upper-case Modifier
. This is a nested
Composable that we want to fully control.
For the MovieList
, we need to define a String
resource for the word "Movies". All String
literals that appear for the user should be externalized into the strings.xml
file:
show in full file app/src/main/res/values/strings.xml
<resources>
<string name="app_name">Movies</string>
<string name="movies">Movies</string>
</resources>
This allows easy overriding for alternative device configurations, in particular, localization to
other languages. We add a string entry to strings.xml
with an id, and the Android Gradle Plugin
generates a helper class for typesafe resource references. In our code, we use this generated R
class using
stringResource(R.id.movies)
which will fetch the value from the closest-matching resource file.
See [App Resources Overview] for more details on how resource files work. Note that some of the resource types, such as layouts, aren't used when writing Compose code.
All code changes
CHANGED: app/src/main/java/com/androidbyexample/compose/movies/MainActivity.kt
package com.androidbyexample.compose.movies
import android.os.Bundle
import androidx.activity.ComponentActivity
import androidx.activity.compose.setContent
import androidx.activity.enableEdgeToEdge
import androidx.activity.viewModels
//import androidx.compose.foundation.layout.fillMaxSize
//import androidx.compose.foundation.layout.padding
//import androidx.compose.material3.Scaffold
//import androidx.compose.ui.Modifier
import com.androidbyexample.compose.movies.screens.Ui
import com.androidbyexample.compose.movies.ui.theme.MoviesTheme
class MainActivity : ComponentActivity() {
private val viewModel by viewModels<MovieViewModel>()
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
enableEdgeToEdge()
setContent {
MoviesTheme {
// Scaffold(modifier = Modifier.fillMaxSize()) { innerPadding ->
// Ui (
Ui(
viewModel = viewModel,
// modifier = Modifier.padding(innerPadding),
) {
finish()
}
// }
}
}
}
}
ADDED: app/src/main/java/com/androidbyexample/compose/movies/components/Display.kt
package com.androidbyexample.compose.movies.components
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.padding
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
import androidx.compose.ui.unit.dp
@Composable
fun Display(
text: String,
modifier: Modifier = Modifier,
) {
Text(
text = text,
style = MaterialTheme.typography.titleLarge,
modifier = modifier
.padding(8.dp)
.padding(start = 16.dp)
.fillMaxWidth(),
)
}
ADDED: app/src/main/java/com/androidbyexample/compose/movies/components/Label.kt
package com.androidbyexample.compose.movies.components
import androidx.annotation.StringRes
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.padding
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.unit.dp
@Composable
fun Label(
@StringRes textId: Int,
modifier: Modifier = Modifier,
) {
Text(
text = stringResource(id = textId),
style = MaterialTheme.typography.titleMedium,
modifier = modifier
.padding(8.dp)
.fillMaxWidth(),
)
}
CHANGED: app/src/main/java/com/androidbyexample/compose/movies/screens/MovieDisplay.kt
package com.androidbyexample.compose.movies.screens
import androidx.compose.foundation.layout.padding
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.ui.Modifier
import com.androidbyexample.compose.movies.Movie
@OptIn(ExperimentalMaterial3Api::class) // for TopAppBar
@Composable
fun MovieDisplayUi(
movie: Movie,
modifier: Modifier = Modifier,
) {
Scaffold(
topBar = {
TopAppBar(
title = {
Text(text = movie.title)
}
)
},
modifier = modifier,
) { innerPadding ->
Text(
text = "Movie Display: ${movie.title}",
// modifier = modifier,
modifier = Modifier.padding(innerPadding),
)
}
}
CHANGED: app/src/main/java/com/androidbyexample/compose/movies/screens/MovieList.kt
package com.androidbyexample.compose.movies.screens
import androidx.compose.foundation.clickable
import androidx.compose.foundation.layout.padding
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.ui.Modifier
import androidx.compose.ui.res.stringResource
import com.androidbyexample.compose.movies.Movie
import com.androidbyexample.compose.movies.R
@OptIn(ExperimentalMaterial3Api::class) // for TopAppBar
@Composable
fun MovieListUi(
movies: List<Movie>,
modifier: Modifier = Modifier,
onMovieClicked: (Movie) -> Unit,
) {
Scaffold(
topBar = {
TopAppBar(
title = {
Text(text = stringResource(R.string.movies))
}
)
},
modifier = modifier,
) { innerPadding ->
Text(
text = "Movie List",
// modifier = modifier.clickable {
modifier = Modifier
.padding(innerPadding)
.clickable {
onMovieClicked(movies[0])
}
)
}
}
CHANGED: app/src/main/res/values/strings.xml
<resources>
<string name="app_name">Movies</string>
<string name="movies">Movies</string>
</resources>