Building a Compose-based screen
Let's say we want to build an application that showcases some restaurants. We will build the UI with Compose and go through the steps of creating a new Compose project. We will then build a list item for such a restaurant and finally display a dummy list of such items.
To summarize, in this section, we will build our first Compose-based application: a restaurant explorer app! To achieve that, we must display some restaurants, which we will do by covering the following topics:
- Creating your first Compose project
- Building a restaurant element layout
- Displaying a list of restaurants with Compose
Now that we have a clear path, let's get started.
Creating your first Compose project
To build a restaurant app, we have to create a new Compose-based project:
- Open Android Studio and select the New Project option:
If you already have Android Studio open, go to File, then New, and finally New Project.
Note
Make sure that you have Android Studio version Arctic Fox 2020.3.1 or newer. If you're using a newer version though, some files might have differences in the generated code.
- In the Phone and tablet template section, select Empty Compose Activity and then choose Next:
- Next, enter some details about your application. In the Name field, enter
Restaurants app
. Leave Kotlin as-is for Language and set Minimum SDK to API 21. Then, click Finish.Important note
The upcoming step is an essential configuration step. It makes sure that the project Android Studio has configured for you the same versions of dependencies (from Compose, to Kotlin and other dependencies) that we use throughout the book. By doing so, you will be able to follow the code snippets and inspect the code source without any API differences.
- Inside the newly generated project, before inspecting the code, make sure that the generated project uses the versions of dependencies that are used throughout the book.
To do so, first go to the project-level build.gradle
file and inside the dependencies
block, make sure that the Kotlin version is set to 1.6.10
:
buildscript { […] dependencies { classpath "com.android.tools.build:gradle:7.0.2" classpath "org.jetbrains.kotlin:kotlin-gradle- plugin:1.6.10" […] } }
Alternatively, if you're using a newer version of Android Studio, you might find the Kotlin version used in this project inside the plugins
block, like so:
plugins { […] id 'org.jetbrains.kotlin.android' version '1.6.10' apply false }
If you haven't already, you might need to install the 1.6.10 plugin version of Kotlin in Android Studio. To do that, click on the Tools option of Android Studio on the Kotlin and on the Configure Kotlin Plugin Updates options. In the newly opened window, you can update your Kotlin version to 1.6.10
.
Still in the project-level build.gradle
file, because Compose is tied to the Kotlin version used in our project, make sure that the Compose version is set to 1.1.1
inside the ext { }
block:
buildscript { ext { compose_version = '1.1.1' } repositories {…} dependencies {…} }
Then, move into the app-level build.gradle
file. First check that the composeOptions { }
block looks like this:
plugins { ... } android { [...] buildFeatures { compose true } composeOptions { kotlinCompilerExtensionVersion compose_version } packagingOptions { ... } }
In some versions of Android Studio, the composeOptions { }
block would add an outdated kotlinCompilerVersion '1.x.xx'
line that should be removed.
Finally, make sure that the dependencies
block of the app-level build.gradle
file includes the following versions for its dependencies:
dependencies { implementation 'androidx.core:core-ktx:1.7.0' implementation 'androidx.appcompat:appcompat:1.4.1' implementation 'com.google.android.material: material:1.5.0' implementation "androidx.compose.ui:ui: $compose_version" implementation "androidx.compose.material: material:$compose_version" implementation "androidx.compose.ui:ui-tooling- preview:$compose_version" implementation 'androidx.lifecycle:lifecycle- runtime-ktx:2.4.1' implementation 'androidx.activity:activity- compose:1.4.0' testImplementation 'junit:junit:4.+' androidTestImplementation 'androidx.test.ext:junit:1.1.3' androidTestImplementation 'androidx.test.espresso:espresso-core:3.4.0' androidTestImplementation "androidx.compose.ui:ui- test-junit4:$compose_version" debugImplementation "androidx.compose.ui:ui- tooling:$compose_version" }
If you had to make any changes, synchronize your project with its Gradle files by clicking on the Sync your project with Gradle files button in Android Studio or by pressing on the File menu option and then by selecting Sync Project with Gradle Files.
Now we're set. Let's return to the source code generated by Android Studio.
And here we are – our first Compose project has been set up! Let's check out the source code by navigating to the MainActivity.kt
file. We can conclude that it consists of three main parts:
- The
MainActivity
class - The
Greeting
composable function - The
DefaultPreview
composable function
The MainActivity
class is where content is passed to the setContent
method in the onCreate
callback. As we know by now, we need to call setContent
to set up a Compose UI and pass composable functions as our UI:
setContent { RestaurantsAppTheme { Surface(color = MaterialTheme.colors.background) { Greeting("Android") } } }
The IDE template has already implemented a Greeting
composable that is wrapped into a Surface
that uses the theme's background color. But what is that RestaurantsAppTheme
function that was passed as the parent composable to the setContent
method?
If you press Ctrl + B or Command + B on the function name, you will be taken to the Theme.kt
file, which is where our theme is generated. RestaurantsAppTheme
is a composable function that was auto-generated by the IDE as it holds the app's name:
@Composable fun RestaurantsAppTheme( darkTheme: Boolean = isSystemInDarkTheme(), content: @Composable() -> Unit ) { ... MaterialTheme( colors = colors, typography = Typography, shapes = Shapes, content = content) }
The app's theme is a wrapper over MaterialTheme
and if we pass it to the setContent
call, it allows us to reuse custom styles and color schemes defined within the app's theme. For it to take effect and reuse custom styles, we must pass our composables functions to the content
parameter of our theme composable – in our case, in MainActivity
, the Greeting
composable wrapped in the Surface
composable is passed to the RestaurantsAppTheme
composable.
Let's go back inside the MainActivity.kt
file to have a look at the other parts generated by Android studio. We can see that the Greeting
composable displays text through Text
, similar to our composable functions from the previous examples.
To preview the Greeting
composable, the IDE also generated a preview composable for us called DefaultPreview
, which allows us to preview the content that MainActivity
displays; that is, Greeting
. It also makes use of the theme composable to get the consistently themed UI.
Now that we've achieved a big milestone in that we've created a Compose-based application, it's time to start working on our Restaurants App!
Building a restaurant element layout
It's time to get our hands dirty and start building the layout for a restaurant within the app:
- Create a new file by left-clicking the application package and selecting New and then Kotlin Class/File. Enter
RestaurantsScreen
for the name and select the type as File. - Inside this file, let's create a
RestaurantsScreen
composable function for our first Compose screen:@Composable fun RestaurantsScreen() { RestaurantItem() }
- Next, inside the
RestaurantsScreen.kt
file, let's define theRestaurantItem
composable, which features aCard
composable with elevation and padding:@Composable fun RestaurantItem() { Card(elevation = 4.dp, modifier = Modifier.padding(8.dp) ) { Row(verticalAlignment = Alignment.CenterVertically, modifier = Modifier.padding(8.dp)) { RestaurantIcon( Icons.Filled.Place, Modifier.weight(0.15f)) RestaurantDetails(Modifier.weight(0.85f)) } } }
Make sure that every import you include is part of the androidx.compose.*
package. If you're unsure what imports to include, check out the source code for the RestaurantsScreen.kt
file at the following URL:
Getting back to the previous code snippet, we could say that the Card
composable is similar to Cardview
from the old View System as it allows us to beautify the UI piece that represents a restaurant with border or elevation.
In our case, Card
contains a Row
composable whose children composables are centered vertically and are surrounded by some padding. We used Row
since we will show some details about the restaurant in a horizontal fashion: an icon and some text details.
We passed the RestaurantIcon
and RestaurantDetails
composables as children of the Row
composable but these functions are not defined so we have compilation errors. For now, don't worry about the weight modifiers. Let's define the RestaurantIcon
composable first!
- Still inside the
RestaurantsScreen.kt
file, create another composable function entitledRestaurantIcon
with the following code:@Composable private fun RestaurantIcon(icon: ImageVector, modifier: Modifier) { Image(imageVector = icon, contentDescription = "Restaurant icon", modifier = modifier.padding(8.dp)) }
The RestaurantIcon
composable sets an ImageVector
icon to an Image
composable – in our case, a predefined Material Theme icon called Icons.Filled.Place
. It also sets a contentDescription
value and adds padding on top of the modifier it receives.
However, the most interesting part is the fact that RestaurantIcon
receives a Modifier
as an argument from its parent Row
. The argument it receives is Modifier.weight(0.15f)
, which means that our Row
assigns weights to each of its horizontally positioned children. The value – in this case, 0.15f
– means that this child RestaurantIcon
will take 15% of the horizontal space from its parent Row
.
- Now, still inside the
RestaurantsScreen.kt
file, create aRestaurantDetails
function that displays the restaurant's details:@Composable private fun RestaurantDetails(modifier: Modifier) { Column(modifier = modifier) { Text(text = "Alfredo's dishes", style = MaterialTheme.typography.h6) CompositionLocalProvider( LocalContentAlpha provides ContentAlpha.medium) { Text(text = "At Alfredo's … seafood dishes.", style = MaterialTheme.typography.body2) } } }
Similarly, RestaurantDetails
receives a Modifier.weight(0.85f)
modifier as an argument from Row
, which will make it occupy the remaining 85% of the horizontal space.
The RestaurantDetails
composable is a simple Column
that arranges two Text
composables vertically, with one being the title of the restaurant, and the other being its description.
But what's up with CompositionLocalProvider
? To display the description that's faded out in contrast to the title, we applied a LocalContentAlpha
of ContentAlpha.medium
. This way, the child Text
with the restaurant description will be faded or grayed out.
CompositionLocalProvider
allows us to pass data down to the composable hierarchy. In this case, we want the child Text
to be grayed out, so we passed a LocalContentAlpha
object with a ContentAlpha.medium
value using the infix provides
method.
- For a moment, go to
MainActivity.kt
and remove theDefaultPreview
composable function as we will define our own a@Preview
composable up next. - Go back inside the
RestaurantsScreen.kt
file, define a@Preview
composable:@Preview(showBackground = true) @Composable fun DefaultPreview() { RestaurantsAppTheme { RestaurantsScreen() } }
If you have chosen a different name for your app, you might need to update the previous snippet with the theme composable defined in the Theme.kt
file.
- Rebuild the project and let's inspect the
RestaurantsScreen()
composable by previewing the newly createdDefaultPreview
composable, which should display a restaurant item:
- Finally, go back to
MainActivity.kt
and remove theGreeting
composable. Also, remove theSurface
andGreeting
function calls in thesetContent
method and replace them withRestaurantScreen
:setContent { RestaurantsAppTheme { RestaurantsScreen() } }
By passing RestaurantScreen
to our MainActivity
's setContent
method, we ensure that the application will render the desired UI when built and run.
- Optionally, you can now Run the app to see the restaurant directly on your device or emulator.
Now that we have built a layout for a restaurant, it's time to learn how to display more of them!
Displaying a list of restaurants with Compose
So far, we've displayed a restaurant item, so it's time to display an entire list of them:
- First, create a new class in the root package, next to
MainActivity.kt
, calledRestaurant.kt
. Here, we will add adata class
calledRestaurant
and add the fields that we expect a restaurant to have:data class Restaurant(val id: Int, val title: String, val description: String)
- In the same
Restaurant.kt
file, create a dummy list ofRestaurant
items, preferably at least 10 to fill up the entire screen:data class Restaurant(val id: Int, val title: String, val description: String) val dummyRestaurants = listOf( Restaurant(0, "Alfredo foods", "At Alfredo's …"), [...], Restaurant(13, "Mike and Ben's food pub", "") )
You can find the pre-populated list in this book's GitHub repository, inside the Restaurant.kt
file:
- Go back inside the
RestaurantsScreen.kt
file and update yourRestaurantItem
so that it receives aRestaurant
object as an argument, while also passing the restaurant'stitle
anddescription
to theRestaurantDetails
composable as parameters:@Composable fun RestaurantItem(item: Restaurant) { Card(...) { Row(...) { RestaurantIcon(...) RestaurantDetails( item.title, item.description, Modifier.weight(0.85f) ) } } }
- We have passed the restaurant's
title
anddescription
to theRestaurantDetails
composable as parameters. Propagate these changes in theRestaurantDetails
composable and pass thetitle
into the firstText
composable and thedescription
into the secondText
composable:@Composable fun RestaurantDetails(title: String, description: String, modifier: Modifier){ Column(modifier = modifier) { Text(text = title, ...) CompositionLocalProvider( … ) { Text(text = description, ...) } } }
- Go back to the
RestaurantsScreen
composable and update it to display a vertical list ofRestaurant
objects. We already know that we can use aColumn
to achieve this. Then, iterate over each restaurant indummyRestaurants
and bind it to aRestaurantItem
:@Composable fun RestaurantsScreen() { Column { dummyRestaurants.forEach { restaurant -> RestaurantItem(restaurant) } } }
This will create a beautiful vertical list that we can preview through our DefaultPreview
composable.
- Rebuild the project to see the updated preview generated by the
DefaultPreview
composable:
Alternatively, you can Run the app to see the restaurants directly on your device or emulator.
We've finally created our first list with Compose! It looks very nice and beautiful, yet it has one huge issue – it doesn't scroll! We'll address this together in the next section.