Spotted a bug? Have a great idea? Help us make google.dev great!

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, which allows describing your UI by calling composable functions; the framework then handles UI optimizations under the hood and when the underlying state changes, automatically makes updates to the view hierarchy for you.

A Compose application is made up of composable functions that transform application data into a UI hierarchy. A function is all you need to create a new composable. Compose lets us structure our code into small chunks called composables. A composable is just a function marked with @Composable, and it can call other composables.

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

What you will learn

In this codelab, you will learn:

  • What Compose is
  • How to build UIs with Compose
  • How to manage state in Composable functions
  • Data flow principles in Compose

Prerequisites

  • Experienced with Kotlin syntax, including lambdas

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 you'd normally do. Make sure the minimumSdkVersion is at least 21 since Compose does not work with lower SDK versions.

You'll be able to understand and modify the Compose code that has been added to MainActivity.kt in the next section.

For more information about setting up Compose or adding Compose to an existing project, check out the documentation.

To create a composable function, just add the @Composable annotation to the function definition. This will enable your function to use other @Composable functions within it. The simple example below, when invoked in an Activity (covered in the next sections), will show the text on the screen. The example will produce a piece of UI hierarchy with a Text composable displaying the given input String.

import androidx.compose.*
import androidx.ui.core.*

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

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.

Compose in an Android app

We can add the Greeting composable function we defined before to an existing Android app. Open your project and add this code to MyActivity.kt.

import android.app.Activity
import android.os.Bundle
import androidx.compose.*
import androidx.ui.core.setContent
import androidx.ui.core.Text

class MyActivity : Activity() {

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

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

Activities remain as the entry point to an Android app. In the code above, MyActivity gets launched when the user opens the app. 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.

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 create another Composable function with the @Preview annotation, add the androidx.ui.tooling.preview.Preview import and place inside what you want to see.

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

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.

Compose follows the Single Responsibility Principle. @Composable functions have responsibility over a single part of functionality and that functionality is entirely encapsulated by that function. For example, if you want to set a background color for some Components, 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 covers it.

import android.app.Activity
import android.os.Bundle
import androidx.compose.*
import androidx.ui.core.setContent
import androidx.ui.core.Text
import androidx.ui.graphics.Color
import androidx.ui.material.MaterialTheme
import androidx.ui.material.surface.Surface
import androidx.ui.tooling.preview.Preview

class MyActivity : Activity() {

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

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

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

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

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

If we run it in a real device, you'd see the background being yellow as well.

Modifiers

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

The Spacing modifier will apply the same 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 in the modifier parameter:

@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 have, just like other functions in your codebase. This can affect readability if the function becomes really large. By making small reusable components it's easy to build up a small 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 that tells the compiler that this is a Composable function. The compiler also makes some enforcements such as the function has to be called from another Composable function.

We should place the minimum amount of code possible in our Android Activity class since it cannot be shared. The more code we have outside of the Activity, the more we can reuse.

Let's refactor our code to follow this style and create a new @Composable MyApp function that contains the Compose UI specific logic for this Activity. 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 UI placed on this screen. We can move the Surface from Greeting to our new MyApp function.

class MyActivity : Activity() {
    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))
}

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.

Making container functions

What if we wanted 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 (that we have called child) and returns Unit. We return Unit because, as you might have noticed, all Composable functions return Unit.

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

Inside the method of our container, we provide all the shared configuration we want its children to have. In this case, we want to apply MaterialTheme and a yellow background color to all our descendants. That's why we place child() as the parameter of Surface which is the last Composable function we call.

We can use it like this:

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

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

To see a preview of this, we can modify our preview Composable function to the following:

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

Calling Composable functions multiple times

We extract out UI Components into Composable functions because 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.

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

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

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

@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 get displayed. Let's see different examples, you don't need to type them in the IDE, just see how we use Kotlin to power our UI.

Adding elements to a Column that are part of a Collection:

val myNames = listOf("Manuel", "Nick", "Jose", "Florina", "Yacine")

@Composable
fun MyExampleFunction(names: List<String> = myNames) {
    Column {
        for (name in names) {
            Text(text = name)
        } 
    }
}

Changing the color of the button depending on whether or not it's enabled:

@Composable
fun EnabledButton(text: String, enabled: Boolean) {
    Button(
        text = text,
        style = ContainedButtonStyle(
            color = if (enabled) Color.White else Color.Gray
        )
    )
}

Full code for this section

import android.app.Activity
import android.os.Bundle
import androidx.compose.*
import androidx.ui.core.setContent
import androidx.ui.core.Text
import androidx.ui.graphics.Color
import androidx.ui.material.MaterialTheme
import androidx.ui.material.surface.Surface
import androidx.ui.tooling.preview.Preview
import androidx.ui.core.dp
import androidx.ui.layout.Spacing
import androidx.ui.layout.Column
import androidx.ui.material.Divider

class MyActivity : Activity() {

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

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

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

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

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

It's important we define how data flows in a Compose application since it's different from the current way to do it in Android development.

Top-down data flow is done by passing objects as parameters in Composable functions. A parent Composable function is always in control of the data of its children. Children should not be reading from global variables or global data stores.

@Composable 
fun MyExampleFunction(items: List<Item>) {
    Column {
        for (item in items) {
            RenderItem(item = item)
        } 
    }
}

@Composable
fun RenderItem(item: Item) {
    Row {
        Text(text = item.name)
        WidthSpacer(4.dp)
        Text(text = item.description)
    }
}

In the example above, RenderItem receives the data it needs from the calling Composable function as a parameter.

Bottom-up data flow is done by means of lambdas. When a children Composable function receives an event, the change should propagate back up to the Composable that cares about that information.

In the example above, if we want to handle an Item click, the information is propagated from the bottom of the hierarchy (Clickable composable in RenderItem) to the top Composable using lambdas.

@Composable 
fun MyExampleFunction(items: List<Item>, onSelected: (Item) -> Unit) {
    Column {
        for (item in items) {
            RenderItem(item = item, onClick = { onSelected(item) })
        } 
    }
}

@Composable
fun RenderItem(item: Item, onClick: (Item) -> Unit) {
    Clickable(onClick = onClick) {
        Row {
            Text(text = item.name)
            WidthSpacer(4.dp)
            Text(text = item.description)
        }
    }
}

RenderItem tells MyExampleFunction that the Item was clicked by calling the lambda that the parent passed as a parameter. Since MyExampleFunction doesn't know how to handle that click either, it passes up that information to its calling Composable function by calling the onSelected lambda that it also takes as a parameter and passing the Item in. MyExampleFunction's calling function can use that information to move to a different screen, for example.

Data flow in Compose apps. Data flows down with parameters, events flow up with lambdas.

Managing State with @Model

Reacting to state changes is at the very heart of Compose.

A Composable function instance in the UI tree might not get processed again (recomposed) until one of its input parameters changes. For example, when we call Greeting("Android")in MyBody Composable function we are hard-coding the input (i.e. "Android"), so Greeting will get added to the UI tree once and won't 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, you can pass a model class and invalidate (recompose) the UI when fields of that model change. That model class should be annotated with @Model.

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 the mutable variables of the class. If they change, the UI created from the function 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 annotate CounterState with @Model because we want any Composable functions which take this class as a parameter to automatically recompose when the count value changes. We define Counter as a Composable function that takes CounterState as a parameter,and produces 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++
        }
    )
}

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:

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

@Composable
fun MyScreenContent(appState: AppState = AppState()) {
    Column {
        Greeting("Android")
        Divider(color = Color.Black)
        Greeting("there")
        Divider(color = Color.Transparent, height = 32.dp)
        Counter(appState.counterState)
    }
}

// Simplified version of a typical AppState
class AppState(val counterState: CounterState = CounterState())

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

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 Compose, Composable functions that respond to user events don't hold state. Generally, you create an application-level data structure that manages the user input values, the Composable functions don't own it.

In the following code sample, we're saving the state of a Checkbox in 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 own whether it's checked or not. Whenever the user has interacted with the Checkbox, it notifies this event with the onCheckedChange lambda parameter. In our case, we're updating a FormState instance that is managing the state of Form. When that happens, since Checkbox is reading the optionChecked variable, it'll get recomposed and will display the new value to the user.

Notice that neither Checkbox nor Form manage state whatsoever, everything goes through the state we defined in FormState. You should follow these principles when creating your own Composable function.

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 them horizontally. In those Composable functions, you can also specify the alignment of the items and the size of the Component.

Representation of main and cross Axis for Column and Row

To align the items to the center of the screen, we can use column's crossAxisAlignment parameter.

@Composable
fun MyScreenContent(appState: AppState = AppState()) {
    Column(crossAxisAlignment = CrossAxisAlignment.Center) {
        Greeting("Android")
        Divider(color = Color.Black)
        Greeting("there")
        Divider(color = Color.Transparent, height = 32.dp)
        Counter(appState.counterState)
    }
}

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

Refresh the preview:

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

Let's say we want to put the button at the bottom of the screen while the other content remains at the top. We could do that by expanding the Greetings inside a Column so that they expand as much as they can while MyButton remains inflexible. Since the Column will be the only Flexible child of its parent Column, it will be able to use all the remaining height after Counter is sized.

@Composable
fun MyScreenContent(appState: AppState = AppState()) {
    Column(modifier = ExpandedHeight, crossAxisAlignment = CrossAxisAlignment.Center) {
        Column(modifier = Flexible(1f), crossAxisAlignment = CrossAxisAlignment.Center) {
            Greeting("Android")
            Divider(color = Color.Black)
            Greeting("there")
        }
        Counter(appState.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:

Full code for this section

import android.app.Activity
import android.os.Bundle
import androidx.compose.*
import androidx.ui.core.setContent
import androidx.ui.core.Text
import androidx.ui.graphics.Color
import androidx.ui.material.MaterialTheme
import androidx.ui.material.surface.Surface
import androidx.ui.tooling.preview.Preview
import androidx.ui.core.dp
import androidx.ui.layout.Column
import androidx.ui.layout.CrossAxisAlignment
import androidx.ui.layout.Spacing
import androidx.ui.material.Button
import androidx.ui.material.Divider

class AppState(val counterState: CounterState = CounterState())

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

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

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

@Composable
fun MyScreenContent(appState: AppState = AppState()) {
    Column(modifier = ExpandedHeight, crossAxisAlignment = CrossAxisAlignment.Center) {
        Column(modifier = Flexible(1f), crossAxisAlignment = CrossAxisAlignment.Center) {
            Greeting("Android")
            Divider(color = Color.Black)
            Greeting("there")
        }
        Counter(appState.counterState)
    }
}

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

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

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

We didn't define the style 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 already defines 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 MyActivity : Activity() {

    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 define the style of the Text in this way:

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

The Text composable in the example above takes three arguments, a String, modifiers and a TextStyle. You can create your own TextStyle, or you can retrieve a theme-defined style by using +themeTextStyle. 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 a custom theme

Let's create a theme for our example. Create a new file called CustomTheme.kt.

Since we want to use our CustomTheme in multiple places of our app (likely in all Activities), we have to 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. We define CustomTheme as:

import androidx.compose.Composable

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

We'd like to reuse as much as possible from MaterialTheme so we're leaving the typography as it is and changing colors to match the design we want.

import androidx.compose.Composable
import androidx.ui.core.CurrentTextStyleProvider
import androidx.ui.graphics.Color
import androidx.ui.material.MaterialColors
import androidx.ui.material.MaterialTheme
import androidx.ui.text.TextStyle

val customGreen = Color(0xFF1EB980.toInt())
val customSurface = Color(0xFF26282F.toInt())
private val themeColors = MaterialColors(
    primary = customGreen,
    surface = customSurface,
    onSurface = Color.Black
)

@Composable
fun CustomTheme(children: @Composable() () -> Unit) {
    MaterialTheme(colors = themeColors) {
        val textStyle = TextStyle(color = Color.Red)
        CurrentTextStyleProvider(value = textStyle) {
            children()
        }
    }
}

We set our custom colors out of the MaterialColors data class that contains the default colors defined by the Material design specification. That is passed to the constructor of MaterialTheme that as we saw before, it contains the styling principles from the Material design specification.

To define a generic text color, we create a TextStyleProvider (another Composable function) that is nested inside MaterialTheme.

Since we want all Composable functions to inherit the colors we defined, we place the children() as a parameter inside our custom CurrentTextStyleProvider. In this way, our CustomTheme's children will get applied with the values of the modified MaterialTheme and the default text color.

We can use CustomTheme in our app now as the substitution of MaterialTheme.

import android.app.Activity
import android.os.Bundle
import androidx.compose.*
import androidx.ui.core.Text
import androidx.ui.core.dp
import androidx.ui.core.setContent
import androidx.ui.layout.Spacing
import androidx.ui.material.themeTextStyle
import androidx.ui.tooling.preview.Preview

class MyActivity : Activity() {

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

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

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

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

To learn more about Compose, check out the official documentation. There you can learn how to get started with it and how to set it up in Android Studio.

Jetpack Compose is in a tech-preview status, please do not add Compose to production apps. To provide feedback or report bugs, please refer to the main AndroidX contribution guide and report your bugs here.