Jetpack Compose is a modern toolkit designed to simplify UI development. It combines a reactive programming model with the conciseness and ease of use of the Kotlin programming language. It is fully declarative, meaning you describe your UI by calling a series of functions which transform data into a UI hierarchy. When the underlying data changes, the framework automatically recalls these functions, updating the view hierarchy for you.

A Compose application is made up of composable functions; just regular functions marked with @Composable, which can call other composable functions. A function is all you need to create a new UI component. Compose lets us structure our code into small chunks called composables.

By making small reusable composables - it's easy to build up a library of UI elements used in your application. Each one is responsible for one part of the screen and can be edited independently.

What you will learn

In this codelab, you will learn:

Prerequisites

What you will need

To start a new Compose project, open Android Studio 4.0 and select Start a new Android Studio project as shown below:

If the screen above doesn't appear, go to File > New > New Project... instead.

When creating a new project, choose Empty Compose Activity from the available templates.

Click Next and configure your project as usual. Make sure you select a minimumSdkVersion of at least API 21 which is the minimum API Compose supports.

When choosing the Empty Compose Activity template, the following code is generated for you in your project. This project is already configured to use Compose: the AndroidManifest.xml file is created and the app/build.gradle (or build.gradle (Module: app)) file imports the Compose dependencies and enables Android Studio to work with Compose with the buildFeatures { compose true } flag.

Open MainActivity.kt and check out the code.

class MainActivity : AppCompatActivity() {
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContent {
            MaterialTheme {
                Greeting("Android")
            }
        }
    }
}

@Composable
fun Greeting(name: String) {
    Text(text = "Hello $name!")
}

@Preview
@Composable
fun DefaultPreview() {
    MaterialTheme {
        Greeting("Android")
    }
}

In the next section, we'll see what each method does and how you can improve them to create flexible and reusable layouts.

Let's go through the different classes and methods related to Compose that the template has generated for you.

Composable functions

A composable function is a regular function annotated with @Composable. This enables your function to call other @Composable functions within it. You can see how the Greeting function is marked as @Composable. This function will produce a piece of UI hierarchy displaying the given input String; Text is a composable function provided by the library:

@Composable
fun Greeting(name: String) {
   Text(text = "Hello $name!")
}

Compose in an Android app

With Compose, Activities remain the entry point to an Android app. In our project, MainActivity is launched when the user opens the app (as it's specified in the AndroidManifest.xml file). We use setContent to define our layout but instead of using an XML file as we've always done, we call Composable functions within it.

class MainActivity : AppCompatActivity() {
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContent {
            MaterialTheme {
                Greeting("Android")
            }
        }
    }
}

MaterialTheme is a way to style Composable functions, we'll see more about this in the Theming your app section. To see how the text displays on the screen, you can either run the app in an emulator or device or use the Android Studio preview.

To use the Android Studio preview, you just have to mark any parameterless Composable function with the @Preview annotation & build your project. You can already see a Preview Composable function in the MainActivity.kt file. You can have multiple previews in the same file and give them names.

@Preview("Text preview")
@Composable
fun DefaultPreview() {
    MaterialTheme {
        Greeting(name = "Android")
    }
}

The preview might not appear if the "only code" view mode is selected. Tap on the "split" view mode to see the preview as shown in the image below:

Compose follows the Single Responsibility Principle. @Composable functions have responsibility for a single piece of functionality which is entirely encapsulated by that function. For example, if you want to set a background color for some components then you have to use a Surface Composable function. You won't be able to set a background color with any other built-in Component.

Back to our example, in order to set a background color for the Text and the rest of the screen, we need to define a Surface that contains it:

@Composable
fun Greeting(name: String) {
    Surface(color = Color.Yellow) {
        Text (text = "Hello $name!")
    }
}

The components nested inside Surface will be drawn on top of that background color (unless specified otherwise by another Surface).

When you add that code to the project, you will see a Build & Refresh button at the top right corner of Android Studio. Tap on it or build the project to see the new changes in the preview.

You can see the new changes in the preview:

Modifiers

A modifier is a list of properties that provide additional decoration/context for a UI component. The current available modifiers are: Spacing, AspectRatio and modifiers for Row and Column that we'll see in the Flexible Layouts section.

The Spacing modifier will apply an amount of space around the element it decorates. In order to add padding to our text on the screen, we can add the Spacing modifier to our Text:

@Composable
fun Greeting(name: String) {
    Surface(color = Color.Yellow) {
        Text(text = "Hello $name!", modifier = Spacing(24.dp))
    }
}

Tap on the Build & Refresh button to see the new changes:

Compose Reusability

The more components we add to the UI, the more levels of nesting we create, just like other functions in your codebase. This can affect readability if a function becomes really large. By making small reusable components it's easy to build up a library of UI elements used in your application. Each one is responsible for one small part of the screen and can be edited independently.

Let's differentiate what's a common configuration for our app and what's specific to a particular view. When we refactor our UI code, we have to mark our function with the @Composable annotation to tell the compiler that it is a Composable function, enabling it to call other composable functions. The compiler also enforces that the function has to be called from another Composable function.

Notice how the composable functions in MainActivity.kt are outside of the MainActivity class, declared as top-level functions. The more code we have outside of the Activity, the more we can share and reuse.

First, let's refactor our code to make it more reusable and create a new @Composable MyApp function that contains the Compose UI logic specific to this Activity. Second, it doesn't make sense that the background color of the app is placed in the reusable Greeting Composable, that configuration should be applied to every piece of UI on this screen, so let's move the Surface from Greeting to our new MyApp function:

class MainActivity : AppCompatActivity() {
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContent {
            MyApp()
        }
    }
}

@Composable
fun MyApp() {
    MaterialTheme {
        Surface(color = Color.Yellow) {
            Greeting(name = "Android")
        }
    }
}

@Composable
fun Greeting(name: String) {
    Text (text = "Hello $name!", modifier = Spacing(24.dp))
}

@Preview
@Composable
fun DefaultPreview() {
    MyApp()
}

We'd like to reuse MyApp Composable function in different activities since it defines top level configuration that can be used in multiple places. However, its current state doesn't allow it since it has the Greeting embedded in it. Keep reading to learn how to create a container that holds common app configuration.

Making container functions

What if we want to create a container that has all the common configuration of our app?

To make a generic container, we create a Composable function that takes as a parameter a lambda of a Composable function (here called children) which returns Unit. We return Unit because, as you might have noticed, all Composable functions must return Unit:

@Composable
fun MyApp(children: @Composable() () -> Unit) {
    MaterialTheme {
        Surface(color = Color.Yellow) {
            children()
        }
    }
}

Inside our function, we define all of the shared configuration we want our container to provide and then invoke the passed children composable. In this case, we want to apply a MaterialTheme and a yellow surface and then call children().

We can use it like this (utilizing Kotlin's trailing lambda syntax):

class MainActivity : AppCompatActivity() {
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContent {
            MyApp {
                Greeting("Android")
            }
        }
    }
}

@Preview("Text preview")
@Composable
fun DefaultPreview() {
    MyApp {
        Greeting("Android")
    }
}

This code is equivalent to what we had in the previous section, but is now more flexible. Making container composable functions is a good practice that improves readability and encourages reusing code.

Calling Composable functions multiple times using Layouts

We extract UI components into Composable functions so that we can reuse them without duplicating code. In the following example, we can show two greetings reusing the same Composable function with different parameters. To place items in a vertical sequence, we use the Column Composable function (similar to a vertical LinearLayout).

@Composable
fun MyScreenContent() {
    Column {
        Greeting("Android")
        Divider(color = Color.Black)
        Greeting("there")
    }
}

As we want MyScreenContent to be what our users see when they open the app, we have to modify the MainActivity code accordingly. Also the preview code, so that we can iterate faster in Android Studio without deploying the app to a device or emulator:

class MainActivity : AppCompatActivity() {
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContent {
            MyApp {
                MyScreenContent()
            }
        }
    }
}

@Preview("MyScreen preview")
@Composable
fun DefaultPreview() {
    MyApp {
        MyScreenContent()
    }
}

If we refresh the preview, we'll see the items placed vertically:

Compose and Kotlin

Compose functions can be called like any other function in Kotlin. This makes building UIs really powerful since we can add statements to influence how the UI will be displayed.

For example, we can use a for loop to add elements to the MyScreenContent's Column:

@Composable
fun MyScreenContent(names: List<String> = listOf("Android", "there")) {
    Column {
        for (name in names) {
            Greeting(name = name)
            Divider(color = Color.Black)
        }
    }
}

Managing State with @Model

Reacting to state changes is at the very heart of Compose. Compose applications transform data into UI by calling Composable functions. If your data changes, you recall these functions with the new data, creating an updated UI. Compose offers tools for observing changes in your app's data which will automatically recall your functions; we call this recomposing. Compose also looks at what data is needed by an individual composable, so that it only needs to recompose components whose data has changed and can skip composing those that are not affected.

Under the hood, Compose uses a custom Kotlin compiler plug-in so when the underlying data changes, the composable functions can be re-invoked to update the UI hierarchy.

For example, when we call Greeting("Android")in MyScreenContent Composable function we are hard-coding the input (i.e. "Android"), so Greeting will get added to the UI tree once and won't ever change, even if the body of MyBody gets recomposed.

Instead of calling Composable functions with different input parameters to update what's displayed on the screen, Compose offers the @Model annotation which can be placed on any class. Composable functions who read values from @Model parameters will automatically be recompose if the data changes. The @Model annotation will cause the Compose compiler to rewrite the class to make it observable and thread-safe. The composable function will automatically be subscribed to any mutable variables of the class that it reads. If they change, composables which read these fields will be recomposed.

Let's make a counter that keeps track of how many times the user has clicked a Button. The state for the counter will be an integer:

@Model 
class CounterState(var count: Int = 0)

We have annotated CounterState with @Model because we want any Composable functions which take this class as a parameter to automatically recompose when the count value changes. Next, let's define Counter as a Composable function that takes CounterState as a parameter, and emits a Button which shows how many times it has been clicked:

@Composable 
fun Counter(state: CounterState) {
    Button(
        text = "I've been clicked ${state.count} times",
        onClick = {
            state.count++
        },
        style = OutlinedButtonStyle()
    )
}

Since Button reads count, whenever count changes, Button will be recomposed again and will display the new value of count.

We now can add a Counter to our screen:

@Composable
fun MyScreenContent(
    names: List<String> = listOf("Android", "there"),
    counterState: CounterState = CounterState()
) {
    Column {
        for (name in names) {
            Greeting(name = name)
            Divider(color = Color.Black)
        }
        Divider(color = Color.Transparent, height = 32.dp)
        Counter(counterState)
    }
}

User interactions doesn't work with the preview at the moment. To see it working, we have to run the app in an emulator or a real device. If we run the app, we can see how Counter maintains state and it increases on every click.

Source of Truth

In Composable functions, state that can be useful to calling functions should be exposed since it's the only way it can be consumed or controlled — and this process is called state hoisting. There are situations where the consumer might not care about certain state (e.g. In a Scroller, the scrollerPosition state is exposed whereas the maxPosition is not). The source of truth belongs to who creates and controls that state.

In our Counter example, since consumers of MyScreenContent can be interested in CounterState, we expose it as a parameter instead of declaring it inside the method. In this way, we make it possible for the caller (i.e. MainActivity) to hoist the state. If MainActivity wants to control the counter's state, it can pass a CounterState object as a parameter. If not, MyScreenContent's default parameter provides an initial state.

In the following code sample that doesn't need to be added to our project, we're saving the state of a Checkbox to a data structure with @Model (so it propagates changes to the Composables that read its value).

@Model
class FormState(var optionChecked: Boolean)

@Composable
fun Form(formState: FormState) {
    Checkbox(
        checked = formState.optionChecked,
        onCheckedChange = { newState -> formState.optionChecked = newState })
}

As you can see, Checkbox doesn't store whether it is checked or not. Whenever the user interacts with the Checkbox, it notifies this via the onCheckedChange callback. In our case, we're updating the FormState instance that manages the state of Form. When that happens, since Checkbox is reading the optionChecked variable, it will be recomposed and display the new value. If we did not provide an onCheckedChange lambda, the Checkbox would not change.

Since Form hoists its state, everything flows down from the given FormState parameter. This makes the component easier to test, reuseable in different contexts, and reduces possible bugs from state duplication. You should follow these principles when creating your own Composable function. You will see this pattern in most provided components.

We briefly introduced Column before, which is used to place items in a vertical sequence. In the same way, you can use Row to place items horizontally.

Row and Column place their items one after the other. If you want to make some items flexible so they occupy the screen with a certain weight, you can use the Flexible modifier (Inflexible is the default behavior).

Let's say we want to put the Button at the bottom of the screen while the other content remains at the top. We can do this with the following steps:

  1. Wrap the flexible items inside another Column with a Flexible modifier. Since this Column is flexible and the rest of the content is inflexible, it will take up as much space as it can. It will be able to use all the remaining height after the outer Column is sized.
  2. Leave the Counter in the outer Column that by default is inflexible.
@Composable
fun MyScreenContent(
    names: List<String> = listOf("Android", "there"),
    counterState: CounterState = CounterState()
) {
    Column(modifier = ExpandedHeight) {
        Column(modifier = Flexible(1f)) {
            for (name in names) {
                Greeting(name = name)
                Divider(color = Color.Black)
            }
        }
        Counter(counterState)
    }
}

We use the ExpandedHeight modifier on the outer Column to make it occupy as much screen as it can (Expanded and ExpandedWidth modifiers are also available). If you refresh the preview, you can see the new changes:

As another example of how you can leverage Kotlin in Compose, we could change the Button's background color depending on the number of times the user tapped on it with an if else statement:

@Composable
fun Counter(state: CounterState) {
    Button(
        text = "I've been clicked ${state.count} times",
        onClick = {
            state.count++
        },
        style = ContainedButtonStyle(color = if (state.count > 5) Color.Green else Color.White)
    )
}

Full code for this section

import android.os.Bundle
import androidx.appcompat.app.AppCompatActivity
import androidx.compose.Composable
import androidx.compose.Model
import androidx.ui.core.Text
import androidx.ui.core.dp
import androidx.ui.core.setContent
import androidx.ui.graphics.Color
import androidx.ui.layout.Column
import androidx.ui.layout.ExpandedHeight
import androidx.ui.layout.Spacing
import androidx.ui.material.Button
import androidx.ui.material.ContainedButtonStyle
import androidx.ui.material.Divider
import androidx.ui.material.MaterialTheme
import androidx.ui.material.surface.Surface
import androidx.ui.tooling.preview.Preview


@Model
class CounterState(var count: Int = 0)

class MainActivity : AppCompatActivity() {
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContent {
            MyApp {
                MyScreenContent()
            }
        }
    }
}

@Composable
fun MyApp(children: @Composable() () -> Unit) {
    MaterialTheme {
        Surface(color = Color.Yellow) {
            children()
        }
    }
}

@Composable
fun MyScreenContent(
    names: List<String> = listOf("Android", "there"),
    counterState: CounterState = CounterState()
) {
    Column(modifier = ExpandedHeight) {
        Column(modifier = Flexible(1f)) {
            for (name in names) {
                Greeting(name = name)
                Divider(color = Color.Black)
            }
        }
        Counter(counterState)
    }
}

@Composable
fun Greeting(name: String) {
    Text (
        text = "Hello $name!",
        modifier = Spacing(24.dp)
    )
}

@Composable
fun Counter(state: CounterState) {
    Button(
        text = "I've been clicked ${state.count} times",
        onClick = {
            state.count++
        },
        style = ContainedButtonStyle(color = if (state.count > 5) Color.Green else Color.White)
    )
}

@Preview("MyScreen preview")
@Composable
fun DefaultPreview() {
    MyApp {
        MyScreenContent()
    }
}

We didn't define any styling for any Composables in the previous examples of the codelab. How can we theme our app? A theme is part of the hierarchy of Components as any other Composable function. We can see MaterialTheme as an example.

MaterialTheme is a Composable function that reflects the styling principles from the Material design specification. That styling information will cascade down to the components that are inside it, which may read the information to style themselves. In our original simple UI, we can use MaterialTheme as follows:

class MainActivity : AppCompatActivity() {

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContent {
            MaterialTheme {
                Greeting(name = "Android")
            }
        }
    }
}

Since MaterialTheme wraps Greeting, Greeting will be styled with the properties defined in the theme. We can retrieve properties of MaterialTheme and use them to define the style of the Text in this way:

@Composable
fun Greeting(name: String) {
    Text (
        text = "Hello $name!",
        modifier = Spacing(24.dp),
        style = (+MaterialTheme.typography()).h1
    )
}

The Text composable in the example above sets three arguments, a String, modifiers and a TextStyle. You can create your own TextStyle, or you can retrieve a theme-defined style by using +MaterialTheme.typography(). This construct gives you access to the Material defined text styles, such as h1, body1 or subtitle1. In our example, we're using the h1 style defined in the theme.

If you see the preview with this code, you'll see the following screenshot:

@Preview("Text preview")
@Composable
fun DefaultPreview() {
    MaterialTheme {
        Greeting("Android")
    }
}

Create your app's theme

Let's create a theme for our app. Create a new file called MyAppTheme.kt.

Since we might want to use our MyAppTheme in multiple places of our app (likely in all Activities), we'll create a reusable component.

As we saw in the ‘Theming your app' section, a theme is a Composable function that takes other children Composable functions. To make it reusable, we create a container Composable function as we did in the ‘Declarative UI' section:

import androidx.compose.Composable

@Composable
fun MyAppTheme(children: @Composable() () -> Unit) {
    // TODO 
}

MaterialTheme holds configuration for colors and typography, we'll just change colors at this point to achieve the design we want.

import androidx.compose.Composable
import androidx.ui.graphics.Color
import androidx.ui.material.ColorPalette
import androidx.ui.material.MaterialTheme

val green = Color(0xFF1EB980)
private val themeColors = ColorPalette(
    primary = green,
    surface = Color.DarkGray,
    onSurface = green
)

@Composable
fun MyAppTheme(children: @Composable() () -> Unit) {
    MaterialTheme(colors = themeColors) {
        children()
    }
}

We provided our custom colors overriding those from ColorPalette (which default colors to the Material baseline theme unless otherwise provided). That is passed to the constructor of MaterialTheme that as we saw before, implements the styling principles from the Material design specification.

We can use MyAppTheme in our app now as the substitution of MaterialTheme. By setting the surface color to DarkGray and the onSurface color to a custom green, we make all Text functions inside a Surface to use onSurface as text color. This works because the default color for a Surface is the surface color from the theme, so the onSurface color will be used for Text. The same works for other colors, if the Surface uses the primary color from the theme, the text color will be onPrimary.

You can see all of that in the following code:

import android.os.Bundle
import androidx.appcompat.app.AppCompatActivity
import androidx.compose.Composable
import androidx.compose.unaryPlus
import androidx.ui.core.Text
import androidx.ui.core.dp
import androidx.ui.core.setContent
import androidx.ui.layout.Spacing
import androidx.ui.material.MaterialTheme
import androidx.ui.material.surface.Surface
import androidx.ui.tooling.preview.Preview

class MainActivity : AppCompatActivity() {

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContent {
            MyAppTheme {
                Surface {
                    Greeting("Android")
                }
            }
        }
    }
}

@Composable
fun Greeting(name: String) {
    Text (
        text = "Hello $name!",
        modifier = Spacing(24.dp),
        style = (+MaterialTheme.typography()).h1
    )
}

@Preview("Text preview")
@Composable
fun DefaultPreview() {
    MyAppTheme {
        Surface {
            Greeting("Android")
        }
    }
}

If you refresh the preview, you'll see this:

To learn more about Compose, check out the Jetnews code sample that showcases the current UI capabilities of Compose. Moreover, if you want to add Compose to your application, learn how to do that in the documentation.

You can also watch these Google talks about Jetpack Compose:

Jetpack Compose is in Technical Preview, please do not use Compose in production apps. You can provide feedback or report bugs here.