
Kickstart Modern Android Development with Jetpack and Kotlin
By :

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:
Now that we have a clear path, let's get started.
To build a restaurant app, we have to create a new Compose-based project:
Figure 1.20 – Starting a new project with Android Studio
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.
Figure 1.21 – Starting a new project with Android Studio
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.
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:
MainActivity
classGreeting
composable functionDefaultPreview
composable functionThe 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!
It's time to get our hands dirty and start building the layout for a restaurant within the app:
RestaurantsScreen
for the name and select the type as File. RestaurantsScreen
composable function for our first Compose screen:@Composable fun RestaurantsScreen() { RestaurantItem() }
RestaurantsScreen.kt
file, let's define the RestaurantItem
composable, which features a Card
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!
RestaurantsScreen.kt
file, create another composable function entitled RestaurantIcon
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
.
RestaurantsScreen.kt
file, create a RestaurantDetails
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.
MainActivity.kt
and remove the DefaultPreview
composable function as we will define our own a @Preview
composable up next.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.
RestaurantsScreen()
composable by previewing the newly created DefaultPreview
composable, which should display a restaurant item:Figure 1.22 – Previewing a restaurant item
MainActivity.kt
and remove the Greeting
composable. Also, remove the Surface
and Greeting
function calls in the setContent
method and replace them with RestaurantScreen
: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.
Now that we have built a layout for a restaurant, it's time to learn how to display more of them!
So far, we've displayed a restaurant item, so it's time to display an entire list of them:
MainActivity.kt
, called Restaurant.kt
. Here, we will add a data class
called Restaurant
and add the fields that we expect a restaurant to have:data class Restaurant(val id: Int, val title: String, val description: String)
Restaurant.kt
file, create a dummy list of Restaurant
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:
RestaurantsScreen.kt
file and update your RestaurantItem
so that it receives a Restaurant
object as an argument, while also passing the restaurant's title
and description
to the RestaurantDetails
composable as parameters:@Composable fun RestaurantItem(item: Restaurant) { Card(...) { Row(...) { RestaurantIcon(...) RestaurantDetails( item.title, item.description, Modifier.weight(0.85f) ) } } }
title
and description
to the RestaurantDetails
composable as parameters. Propagate these changes in the RestaurantDetails
composable and pass the title
into the first Text
composable and the description
into the second Text
composable:@Composable fun RestaurantDetails(title: String, description: String, modifier: Modifier){ Column(modifier = modifier) { Text(text = title, ...) CompositionLocalProvider( … ) { Text(text = description, ...) } } }
RestaurantsScreen
composable and update it to display a vertical list of Restaurant
objects. We already know that we can use a Column
to achieve this. Then, iterate over each restaurant in dummyRestaurants
and bind it to a RestaurantItem
:@Composable fun RestaurantsScreen() { Column { dummyRestaurants.forEach { restaurant -> RestaurantItem(restaurant) } } }
This will create a beautiful vertical list that we can preview through our DefaultPreview
composable.
DefaultPreview
composable:Figure 1.23 – Previewing RestaurantsScreen with the Column 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.
Change the font size
Change margin width
Change background colour