In the Jetpack Compose basics codelab, you learnt how to build simple UIs with Compose using composables like Text as well as flexible layout composables such as Column and Row that allow you to place items (vertically and horizontally, respectively) on the screen and configuring the alignment of the elements within it. On the other hand, if you don't want items to be displayed vertically or horizontally, Box allows you to have items behind and/or in front of others.

You can use these standard layout components to build UIs like this one:

@Composable
fun PhotographerProfile(photographer: Photographer) {
    Row(verticalAlignment = Alignment.CenterVertically) {
        Image(...)
        Column {
            Text(photographer.name)
            Text(photographer.lastSeenOnline, ...)
        }
    }
}

Thanks to Compose's reusability and composability, you can build your own composables by combining the different parts needed at the correct level of abstraction together in a new composable function.

In this codelab, you'll learn how to use Compose's highest level of UI abstraction, Material Design, as well as low-level composables like Layout that allows you to measure and place elements on the screen.

If you want to create a Material Design-based UI, Compose has built-in Material components composables that you can use as we'll see in the codelab. If you don't want to use Material Design or want to build something that is not in the Material Design specs, you'll also learn about how to create custom layouts.

What you will learn

In this codelab, you will learn:

Prerequisites

What you will need

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

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

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 level 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: YourApplicationName.app)) file imports the Compose dependencies and enables Android Studio to work with Compose with the buildFeatures { compose true } flag.

android {
    ...
    kotlinOptions {
        jvmTarget = '1.8'
        useIR = true
    }
    buildFeatures {
        compose true
    }
    composeOptions {
        kotlinCompilerExtensionVersion compose_version
        kotlinCompilerVersion '1.4.0'
    }
}

dependencies {
    ...
     implementation "androidx.compose.ui:ui:$compose_version"
    implementation "androidx.compose.material:material:$compose_version"
    implementation "androidx.ui:ui-tooling:$compose_version"
    ...
}

To update to the latest Compose version, open the root build.gradle file and change compose_version to 1.0.0-alpha04:

buildscript {
    ext {
        compose_version = '1.0.0-alpha04'
    }
    ...
}

Sync the project after doing that.

Solution to the codelab

You can get the code for the solution of this codelab from GitHub:

$ git clone https://github.com/googlecodelabs/android-compose-codelabs

Alternatively you can download the repository as a Zip file:

Download Zip

You'll find the solution code in the LayoutsCodelab project. We recommend that you follow the codelab step-by-step at your own pace and check the solution if you consider it necessary. During the codelab, you'll be presented with snippets of code that you'll need to add to the project.

Modifiers allow you to decorate a composable. You can change its behavior, appearance, add information like accessibility labels, process user input or even add high-level interactions like making something clickable, scrollable, draggable or zoomable. Modifiers are regular Kotlin objects. You can assign them to variables and reuse them. You can also chain several modifiers one after the other to compose them.

Let's implement the profile layout seen in the introduction section:

Open MainActivity.kt and add the following:

@Composable
fun PhotographerCard() {
    Column {
        Text("Alfred Sisley", fontWeight = FontWeight.Bold)
        ProvideEmphasis(EmphasisAmbient.current.medium) {
            Text("3 minutes ago", style = MaterialTheme.typography.body2)
        }
    }
}

@Preview
@Composable
fun PhotographerCardPreview() {
    LayoutsCodelabTheme {
        PhotographerCard()
    }
}

With preview:

Next, while the picture is loading, you might want to show a placeholder. For that, you can use a Surface where we specify a circle shape and the placeholder color. To specify how big it should be, we can use the preferredSize modifier:

@Composable
fun PhotographerCard() {
    Row {
        Surface(
            modifier = Modifier.preferredSize(50.dp),
            shape = CircleShape,
            color = MaterialTheme.colors.onSurface.copy(alpha = 0.2f)
        ) {
            // Image goes here
        }
        Column {
            Text("Alfred Sisley", fontWeight = FontWeight.Bold)
            ProvideEmphasis(EmphasisAmbient.current.medium) {
                Text("3 minutes ago", style = MaterialTheme.typography.body2)
            }
        }
    }
}

There are a couple of improvements we would like to do here:

  1. We want some separation between the placeholder and the text.
  2. We would like the text to be centered vertically.

For #1, we can use Modifier.padding on the Column that contains the text to add some space at the start of the composable to separate the image and the text. For #2, some layouts offer modifiers that are only applicable to them and their layout characteristics. For example composables in a Row can access certain modifiers (from the RowScope receiver of Row's content) that make sense there such as weight or align. This scoping offers type-safety, so you cannot accidentally use a modifier which does not make sense in another layout, for example weight does not make sense in a Box, so this will be prevented as a compile-time error.

@Composable
fun PhotographerCard() {
    Row {
        Surface(
            modifier = Modifier.preferredSize(50.dp),
            shape = CircleShape,
            color = MaterialTheme.colors.onSurface.copy(alpha = 0.2f)
        ) {
            // Image goes here
        }
        Column(
            modifier = Modifier
                .padding(start = 8.dp)
                .align(Alignment.CenterVertically)
        ) {
            Text("Alfred Sisley", fontWeight = FontWeight.Bold)
            ProvideEmphasis(EmphasisAmbient.current.medium) {
                Text("3 minutes ago", style = MaterialTheme.typography.body2)
            }
        }
    }
}

With preview:

Most composables accept an optional modifier parameter to make them more flexible, enabling the caller to modify them. If you're creating your own composable, consider having a modifier as a parameter, default it to Modifier (i.e. empty modifier that doesn't do anything) and apply it to the root composable of your function. In this case:

@Composable
fun PhotographerCard(modifier: Modifier = Modifier) {
    Row(modifier) { ... }
}

Order of modifiers matter

In the code, notice how you can chain multiple modifiers one after the other by using the factory-extension functions (i.e. Modifier.padding(start = 8.dp).align(Alignment.CenterVertically)).

Be mindful when chaining modifiers as the order matters. As they're concatenated into a single argument, the order affects the final result.

If you wanted to make the Photographer profile both clickable and have some padding, you could do something like this:

@Composable
fun PhotographerCard(modifier: Modifier = Modifier) {
    Row(modifier
        .padding(16.dp)
        .clickable(onClick = { /* Ignoring onClick */ })
    ) {
        ...
    }
}

Using interactive preview or running in an emulator:

Notice how the whole area is not clickable! This is because padding was applied before the clickable modifier. If we apply the padding modifier after the clickable one then the padding is included in the clickable area:

@Composable
fun PhotographerProfile(modifier: Modifier = Modifier) {
    Row(modifier
        .clickable(onClick = { /* Ignoring onClick */ })
        .padding(16.dp)
    ) {
        ...
    }
}

Using interactive preview or running in an emulator:

Let your imagination fly! Modifiers let you modify your composable in a very flexible way. For example, if you wanted to add some outer spacing, change the background color of the composable, and round the corners of the Row, you could use the following code:

@Composable
fun PhotographerProfile(modifier: Modifier = Modifier) {
    Row(modifier
        .padding(8.dp)
        .clip(RoundedCornerShape(4.dp))
        .background(MaterialTheme.colors.surface)
        .clickable(onClick = { /* Ignoring onClick */ })
        .padding(16.dp)
    ) {
        ...
    }
}

Using interactive preview or running in an emulator:

We'll see more about how modifiers work under the hood later in the codelab.

Compose provides high-level Material Components composables that you can use to build your UI. As they're building blocks for creating UI, you still need to provide the information of what to show on the screen.

Slot APIs are a pattern Compose introduces to bring in a layer of customization on top of composables, in this use case, the available Material Components composables.

Let's see this with an example:

If you think about a Material Button, there is a set guideline on how a Button should look and what it should contain, which we could translate into a simple API to use:

Button(text = "Button")

However, often you want to customize components well beyond what we may expect. We can try and add a parameter for each individual element you could customize, but that quickly gets out of hand:

Button(
    text = "Button",
    icon: Icon? = myIcon,
    textStyle = TextStyle(...),
    spacingBetweenIconAndText = 4.dp,
    ...
)

Therefore, instead of adding multiple parameters to customize the component in a way we cannot expect, we added Slots. Slots leave an empty space in the UI for the developer to fill as they wish.

For example in the case of Button, we can leave the inside of the Button to be filled by you, who may wish to insert a row with an icon and a text:

Button(backgroundColor = Color.Yellow) {
    Row {
        MyImage()
        Spacer(4.dp)
        Text("Button")
    }
}

To enable this, we provide an API for Button that takes a children composable lambda ( text: @Composable() () -> Unit). This allows you to define your own composable to be emitted within the Button.

@Composable
fun Button(
    modifier: Modifier = Modifier.None,
    onClick: (() -> Unit)? = null,
    ...
    text: @Composable() () -> Unit
}

Notice that this lambda, which we've named text, is the last parameter. This allows us to use the trailing lambda syntax to insert content into the Button in a structured way.

Compose uses Slots heavily in more complex components such as the Top App Bar.

Here we can customize more things apart from the title:

Example of usage:

TopAppBar(
    title = {
        Text(text = "Page title", maxLines = 2)
    },
    navigationIcon = {
        Icon(myNavIcon)
    }
)

When building your own composables, you can use the Slots API pattern to make them more reusable.

In the next sections, we'll see the different Material Components composables available and how to use them when building an Android app.

Compose comes with built-in Material Component composables that you can use to create your app. The most high-level composable is Scaffold.

Scaffold

Scaffold allows you to implement a UI with the basic Material Design layout structure. It provides slots for the most common top-level Material components such as TopAppBar, BottomAppBar, FloatingActionButton and Drawer. With Scaffold, you make sure these components will be positioned and work together correctly.

Based on the generated Android Studio template, we're going to modify the sample code to use Scaffold. Open MainActivity.kt and feel free to remove the Greeting and GreetingPreview composables as they won't be used.

Create a new composable called LayoutsCodelab that we'll be modifying throughout the codelab:

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

@Composable
fun LayoutsCodelab() {
    Text(text = "Hi there!")
}

@Preview
@Composable
fun LayoutsCodelabPreview() {
    LayoutsCodelabTheme {
        LayoutsCodelab()
    }
}

If you see the Compose preview function that must be annotated with @Preview, you'll see LayoutsCodelab as this:

Let's add the Scaffold composable to our example so that we can have a typical Material Design structure. All parameters in the Scaffold API are optional except the body content that is of type @Composable() (InnerPadding) -> Unit: the lambda receives a padding as a parameter. That's the padding that should be applied to the content root composable to constrain the items appropriately on the screen. To start simple, let's add Scaffold without any other Material component:

@Composable
fun LayoutsCodelab() {
    Scaffold { innerPadding ->
        Text(text = "Hi there!", modifier = Modifier.padding(innerPadding))
    }
}

With preview:

If we wanted to have a Column with the main content of our screen, we should apply the modifier to Column:

@Composable
fun LayoutsCodelab() {
    Scaffold { innerPadding ->
        Column(modifier = Modifier.padding(innerPadding)) {
            Text(text = "Hi there!")
            Text(text = "Thanks for going through the Layouts codelab")
        }
    }
}

With preview:

To make our code more reusable and testable, we should structure it into small chunks. For that, let's create another composable function with the content of our screen.

@Composable
fun LayoutsCodelab() {
    Scaffold { innerPadding ->
        BodyContent(Modifier.padding(innerPadding))
    }
}

@Composable
fun BodyContent(modifier: Modifier = Modifier) {
    Column(modifier = modifier) {
        Text(text = "Hi there!")
        Text(text = "Thanks for going through the Layouts codelab")
    }
}

It's typical to see a top AppBar in Android apps with information about the current screen, navigation and actions. Let's add that to our example now.

TopAppBar

Scaffold has a slot for a top AppBar with the topBar parameter of type @Composable() (() -> Unit)?, meaning we can fill the slot with any composable we want. For example, if we just want it to contain a h3 style text, we could use Text in the provided slot as follows:

@Composable
fun LayoutsCodelab() {
    Scaffold(
        topBar = {
            Text(
                text = "LayoutsCodelab",
                style = MaterialTheme.typography.h3
            )
        }
    ) { innerPadding ->
        BodyContent(Modifier.padding(innerPadding))
    }
}

With preview:

However, as for most Material components, Compose comes with a TopAppBar composable that has slots for a title, navigation icon, and actions. Also, it comes with some default that adjusts to what Material specs recommend such as the color to use on each component.

Following the slots API pattern, we want the title slot of TopAppBar to contain a Text with the title of the screen:

@Composable
fun LayoutsCodelab() {
    Scaffold(
        topBar = {
            TopAppBar(
                title = {
                    Text(text = "LayoutsCodelab")
                }
            )
        }
    ) { innerPadding ->
        BodyContent(Modifier.padding(innerPadding))
    }
}

With preview:

Top AppBars usually have some action items. In our example, we're going to add a favorite button you can tap when you think you learnt something. Compose also comes with some predefined Material icons you can use, for example the close, favorite and menu icons.

The slot for action items in the top AppBar is the actions parameter that internally uses a Row, so multiple actions will be placed horizontally. In order to use one of the predefined icons, we can use the IconButton composable with an Icon inside it:

@Composable
fun LayoutsCodelab() {
    Scaffold(
        topBar = {
            TopAppBar(
                title = {
                    Text(text = "LayoutsCodelab")
                },
                actions = {
                    IconButton(onClick = { /* doSomething() */ }) {
                        Icon(Icons.Filled.Favorite)
                    }
                }
            )
        }
    ) { innerPadding ->
        BodyContent(Modifier.padding(innerPadding))
    }
}

With preview:

Normally, actions modify the state of your application somehow. For more information about state, you can learn the basics of state management in the Basics Compose codelab.

Placing modifiers

Whenever we create new composables, having a modifier parameter that defaults to Modifier is a good practice to make the composable more reusable. Our BodyContent composable already takes a modifier as a parameter. If we wanted to add some more extra padding to BodyContent, where should we place the padding modifier?

We have two possibilities:

  1. Apply the modifier to the only direct child inside the composable so that all calls to BodyContent apply the extra padding:
@Composable
fun BodyContent(modifier: Modifier = Modifier) {
    Column(modifier = modifier.padding(8.dp)) {
        Text(text = "Hi there!")
        Text(text = "Thanks for going through the Layouts codelab")
    }
}
  1. Apply the modifier when calling the composable that will add the extra padding just when needed:
@Composable
fun LayoutsCodelab() {
    Scaffold(...) { innerPadding ->
        BodyContent(Modifier.padding(innerPadding).padding(8.dp))
    }
}

Deciding where to do it totally depends on the type of composable and the use case. If the modifier is intrinsic to the composable, place it inside; if not, outside. In our case, we'd go for option 2 as padding is something we might not always force whenever we call BodyContent, it should be applied on a case-by-case basis.

Modifiers can be chained by calling each successive modifier function on the previous one. When there is no available chaining method, you can use .plus(). In our example, we start with modifier (lower case), meaning the chain builds on top of the chain passed in as a parameter.

More icons

Apart from the icons we listed before, you can use the full list of Material icons by adding a new dependency to the project. In case you want to experiment with those icons, open the app/build.gradle (or build.gradle (Module: app)) file and import the ui-material-icons-extended dependency:

dependencies {
  ...
  implementation "androidx.compose.material:material-icons-extended:$compose_version"
}

Go ahead and feel free to change the icons of the TopAppBar as much as you like.

Further work

Scaffold and TopAppBar are just some composables that can be used to have a Material looking application. The same can be done for other Material components such as BottomNavigation or Drawer. As an exercise, we invite you to try to fill the Scaffold slots with those APIs in the same way we've done until now.

Compose promotes reusability of composables as small chunks that can be enough for some custom layouts by combining built-in composables such as Column, Row, or Box.

However, you might need to build something unique to your app that requires measuring and laying out children manually. For that, you can use the Layout composable. In fact all higher level layouts like Column and Row are built with this.

Before diving into how to create custom layouts, we need to know more about the principles of Layouts in Compose.

Principles of layouts in Compose

Some composable functions emit a piece of UI when invoked that is added to a UI tree that will get rendered on the screen. Each emission (or element) has one parent and potentially many children. Also, it has a location within its parent: an (x, y) position, and a size: a width and height.

Elements are asked to measure themselves with Constraints that should be satisfied. Constraints restrict the minimum and maximum width and height of an element. If an element has child elements it may measure each of them to help determine its own size. Once an element reports its own size, it has an opportunity to place its child elements relative to itself. This will be further explained when creating the custom layout.

Compose UI does not permit multi-pass measurement. This means that a layout element may not measure any of its children more than once in order to try different measurement configurations. Single-pass measurement is good for performance, allowing Compose to handle efficiently deep UI trees. If a layout element measured its child twice and that child measured one of its children twice and so on, a single attempt to lay out a whole UI would have to do a lot of work, making it hard to keep your app performing well. However, there are times when you really need additional information on top of what a single child measurement would tell you - for these cases there are ways of doing this, we will talk about them later.

Using the layout modifier

Use the layout modifier to manually control how to measure and position an element. Usually, the common structure of a custom layout modifier is as follows:

fun Modifier.customLayoutModifier(...) = Modifier.layout { measurable, constraints ->
  ...
})

When using the layout modifier, you get two lambda parameters:

Let's say you want to display a Text on the screen and control the distance from the top to the baseline of the first line of texts. In order to do that, you'd need to manually place the composable on the screen using the layout modifier. See the desired behavior in the next picture where the distance from top to first baseline is 24.dp:

Let's create a firstBaselineToTop modifier first:

fun Modifier.firstBaselineToTop(
  firstBaselineToTop: Dp
) = Modifier.layout { measurable, constraints ->
  ...
}

The first thing to do is measure the composable. As we mentioned in the Principles of Layout in Compose section, you can only measure your children once.

Measure the composable by calling measurable.measure(constraints). When calling measure(constraints), you can pass in the given constraints of the composable available in the constraints lambda parameter or create your own. The result of a measure() call on a Measurable is a Placeable that can be positioned by calling placeRelative(x, y), as we will do later.

For this use case, don't constrain measurement further, just use the given constraints:

fun Modifier.firstBaselineToTop(
  firstBaselineToTop: Dp
) = Modifier.layout { measurable, constraints ->
  val placeable = measurable.measure(constraints)
  ...
}

Now that the composable has been measured, you need to calculate its size and specify it by calling the layout(width, height) method which also accepts a lambda used for placing the content.

In this case, the width of our composable will be the width of the measured composable and the height will be until the composable's height with the desired top-to-baseline height minus the first baseline:

fun Modifier.firstBaselineToTop(
  firstBaselineToTop: Dp
) = Modifier.layout { measurable, constraints ->
  val placeable = measurable.measure(constraints)

  // Check the composable has a first baseline
  check(placeable[FirstBaseline] != AlignmentLine.Unspecified)
  val firstBaseline = placeable[FirstBaseline]

  // Height of the composable with padding - first baseline
  val placeableY = firstBaselineToTop.toIntPx() - firstBaseline
  val height = placeable.height + placeableY
  layout(placeable.width, height) {
    ...
  }
}

Now, you can position the composable on the screen by calling placeable.placeRelative(x, y). If you don't call placeRelative, the composable won't be visible. placeRelative automatically adjusts the position of the placeable based on the current layoutDirection.

In this case, the y position of the text corresponds to the top padding minus the position of the first baseline:

fun Modifier.firstBaselineToTop(
  firstBaselineToTop: Dp
) = Modifier.layout { measurable, constraints ->
  ...
  val placeableY = firstBaselineToTop.toIntPx() - firstBaseline
  val height = placeable.height + placeableY
  layout(placeable.width, height) {
    // Where the composable gets placed
    placeable.placeRelative(0, placeableY)
  }
}

To verify this works as expected, you can use this modifier on a Text as you saw in the picture above:

@Preview
@Composable
fun TextWithPaddingToBaselinePreview() {
  LayoutsCodelabTheme {
    Text("Hi there!", Modifier.firstBaselineToTop(32.dp))
  }
}

@Preview
@Composable
fun TextWithNormalPaddingPreview() {
  LayoutsCodelabTheme {
    Text("Hi there!", Modifier.padding(top = 32.dp))
  }
}

With preview:

Using the Layout composable

Instead of controlling how a single composable gets measured and laid out on the screen, you might have the same necessity for a group of composables. For that, you can use the Layout composable to manually control how to measure and position the layout's children. Usually, the common structure of a composable that uses Layout is as follows:

@Composable
fun CustomLayout(
    modifier: Modifier = Modifier,
    // custom layout attributes 
    children: @Composable() () -> Unit
) {
    Layout(
        modifier = modifier,
        children = children
    ) { measurables, constraints ->
        // measure and position children given constraints logic here
    }
}

The minimum required parameters for a CustomLayout are a modifier and children; these parameters are then passed to Layout. In the trailing lambda of Layout (of type MeasureBlock), you get the same lambda parameters as you get with the layout modifier.

To show Layout in action, let's start implementing a very basic Column using Layout to understand the API. Later, we'll build something more complex to showcase flexibility of the Layout composable.

Implementing a basic Column

Our custom implementation of Column lays out items vertically. Also, for simplicity, our layout occupies as much space as it can in its parent.

Create a new composable called MyOwnColumn and add the common structure of a Layout composable:

@Composable
fun MyOwnColumn(
    modifier: Modifier = Modifier,
    children: @Composable() () -> Unit
) {
    Layout(
        modifier = modifier,
        children = children
    ) { measurables, constraints ->
        // measure and position children given constraints logic here
    }
}

As before, the first thing to do is measure our children that can only be measured once. Similarly to how the layout modifier works, in the measurables lambda parameter, you get all the children that you can measure by calling measurable.measure(constraints).

For this use case, you won't constrain our child views further. When measuring the children, you should also keep track of the width and the maximum height of each row to be able to place them correctly on the screen later:

@Composable
fun MyOwnColumn(
    modifier: Modifier = Modifier,
    children: @Composable() () -> Unit
) {
    Layout(
        modifier = modifier,
        children = children
    ) { measurables, constraints ->

        // Don't constrain child views further, measure them with given constraints
        // List of measured children
        val placeables = measurables.map { measurable ->
            // Measure each children
            measurable.measure(constraints)
        }
    }
}

Now that you have the list of measured children in our logic, before positioning them on the screen, you need to calculate the size of our version of Column. As you're making it as big as its parent, the size of it is the constraints passed in by the parent. Specify the size of our own Column by calling the layout(width, height) method, which also gives you the lambda used for placing the children:

@Composable
fun MyOwnColumn(
    modifier: Modifier = Modifier,
    children: @Composable() () -> Unit
) {
    Layout(
        modifier = modifier,
        children = children
    ) { measurables, constraints ->
        // Measure children - code in the previous code snippet
        ...

        // Set the size of the layout as big as it can
        layout(constraints.maxWidth, constraints.maxHeight) {
            // Place children
        }
    }
}

Lastly, we position our children on the screen by calling placeable.placeRelative(x, y). In order to place the children vertically, we keep track of the y coordinate we have placed children up to. The final code of MyOwnColumn looks like this:

@Composable
fun MyOwnColumn(
    modifier: Modifier = Modifier,
    children: @Composable() () -> Unit
) {
    Layout(
        modifier = modifier,
        children = children
    ) { measurables, constraints ->
        // Don't constrain child views further, measure them with given constraints
        // List of measured children
        val placeables = measurables.map { measurable ->
            // Measure each children
            measurable.measure(constraints)
        }

        // Track the y co-ord we have placed children up to
        var yPosition = 0

        // Set the size of the layout as big as it can
        layout(constraints.maxWidth, constraints.maxHeight) {
            // Place children in the parent layout
            placeables.forEach { placeable ->
                // Position item on the screen
                placeable.placeRelative(x = 0, y = yPosition)

                // Record the y co-ord placed up to
                yPosition += placeable.height
            }
        }
    }
}

MyOwnColumn in action

Let's see MyOwnColumn on the screen by using it in the BodyContent composable. Replace the content inside BodyContent with the following:

@Composable
fun BodyContent(modifier: Modifier = Modifier) {
    MyOwnColumn(modifier.padding(8.dp)) {
        Text("MyOwnColumn")
        Text("places items")
        Text("vertically.")
        Text("We've done it by hand!")
    }
}

With preview:

Once the basics of Layout are covered. Let's create a more complex example to showcase the flexibility of the API. We'll build the custom Material Study Owl's staggered grid that you can see in the middle of the following picture:

Owl's staggered grid lays out items vertically, filled out a column at a time given a n number of rows. Doing this with a Row of Columns is not possible since you wouldn't get the staggering of the layout. Having a Column of Rows could be possible if you prepare the data so that it displays vertically.

However, the custom layout also gives you the opportunity to constrain the height of all the items in the staggered grid. So to have more control over the layout and learn how to create a custom one, we'll measure and position the children on our own.

If we wanted to make the grid reusable on different orientations, we could take as a parameter the number of rows we want to have on the screen. Since that information should come when the layout is invoked, we pass it as a parameter:

@Composable
fun StaggeredGrid(
    modifier: Modifier = Modifier,
    rows: Int = 3,
    children: @Composable() () -> Unit
) {
    Layout(
        modifier = modifier,
        children = children
    ) { measurables, constraints ->
        // measure and position children given constraints logic here
    }
}

As before, the first thing to do is measure our children. Remember you can only measure your children once.

For our use case, we won't constrain our child views further. When measuring our children, we should also keep track of what's the width and the max height of each row:

Layout(
    children = children,
    modifier = modifier
) { measurables, constraints ->

    // Keep track of the width of each row
    val rowWidths = IntArray(rows) { 0 }

    // Keep track of the max height of each row
    val rowMaxHeights = IntArray(rows) { 0 }

    // Don't constrain child views further, measure them with given constraints
    // List of measured children
    val placeables = measurables.mapIndexed { index, measurable ->

        // Measure each child
        val placeable = measurable.measure(constraints)

        // Track the width and max height of each row
        val row = index % rows
        rowWidths[row] = rowWidths[row] + placeable.width.value
        rowMaxHeights[row] = kotlin.math.max(rowMaxHeights[row], placeable.height.value)

        placeable
    }
    ...
}

Now that we have the list of measured children in our logic, before positioning them on the screen, we need to calculate the size of our grid (full width and height) . Also, since we already know the maximum height of each row, we can calculate where we'll position the elements for each row in the Y position. We save the Y positions in the rowY variable:

Layout(
    children = children,
    modifier = modifier
) { measurables, constraints ->
    ... 

    // Grid's width is the widest row
    val width = rowWidths.maxOrNull()
        ?.coerceIn(constraints.minWidth.rangeTo(constraints.maxWidth)) ?: constraints.minWidth

    // Grid's height is the sum of the tallest element of each row
    // coerced to the height constraints 
    val height = rowMaxHeights.sumBy { it }
        .coerceIn(constraints.minHeight.rangeTo(constraints.maxHeight))

    // Y of each row, based on the height accumulation of previous rows
    val rowY = IntArray(rows) { 0 }
    for (i in 1 until rows) {
        rowY[i] = rowY[i-1] + rowMaxHeights[i-1]
    }

    ...
}

Lastly, we position our children on the screen by calling placeable.placeRelative(x, y). In our use case, we also keep track of the X coordinate for each row in the rowX variable:

Layout(
    children = children,
    modifier = modifier
) { measurables, constraints ->
    ... 

    // Set the size of the parent layout
    layout(width, height) {
        // x cord we have placed up to, per row
        val rowX = IntArray(rows) { 0 }

        placeables.forEachIndexed { index, placeable ->
            val row = index % rows
            placeable.placeRelative(
                x = rowX[row],
                y = rowY[row]
            )
            rowX[row] += placeable.width
        }
    }
}

Using the custom StaggeredGrid in an example

Now that we have our custom grid layout that knows how to measure and position children, let's use it in our app. To simulate Owl's chips in the grid, we can easily create a composable that does something similar:

@Composable
fun Chip(modifier: Modifier = Modifier, text: String) {
    Card(
        modifier = modifier,
        border = BorderStroke(color = Color.Black, width = Dp.Hairline),
        shape = RoundedCornerShape(8.dp)
    ) {
        Row(
            modifier = Modifier.padding(start = 8.dp, top = 4.dp, end = 8.dp, bottom = 4.dp), 
            verticalAlignment = Alignment.CenterVertically
        ) {
            Box(
                modifier = Modifier.preferredSize(16.dp, 16.dp)
                    .background(color = MaterialTheme.colors.secondary)
            )
            Spacer(Modifier.preferredWidth(4.dp))
            Text(text = text)
        }
    }
}

@Preview
@Composable
fun ChipPreview() {
    LayoutsCodelabTheme {
        Chip(text = "Hi there")
    }
}

With preview:

Now, let's create a list of topics that we can show in our BodyContent and display them in the StaggeredGrid:

val topics = listOf(
    "Arts & Crafts", "Beauty", "Books", "Business", "Comics", "Culinary",
    "Design", "Fashion", "Film", "History", "Maths", "Music", "People", "Philosophy",
    "Religion", "Social sciences", "Technology", "TV", "Writing"
)


@Composable
fun BodyContent(modifier: Modifier = Modifier) {
    StaggeredGrid(modifier = modifier) {
        for (topic in topics) {
            Chip(modifier = Modifier.padding(8.dp), text = topic)
        }
    }
}

@Preview
@Composable
fun LayoutsCodelabPreview() {
    LayoutsCodelabTheme {
        LayoutsCodelab()
    }
}

With preview:

Notice that we can change the number of rows of our grid and it still works as expected:

@Composable
fun BodyContent(modifier: Modifier = Modifier) {
    StaggeredGrid(modifier = modifier, rows = 5) {
        for (topic in topics) {
            Chip(modifier = Modifier.padding(8.dp), text = topic)
        }
    }
}

With preview:

As depending on the number of rows, our topics can go off the screen, we can make our BodyContent scrollable by just wrapping the StaggeredGrid in a ScrollableRow and passing the modifier to it instead of StaggeredGrid.

@Composable
fun BodyContent(modifier: Modifier = Modifier) {
    ScrollableRow(modifier = modifier) {
        StaggeredGrid {
            for (topic in topics) {
                Chip(modifier = Modifier.padding(8.dp), text = topic)
            }
        }
    }
}

If you use the Interactive Preview button or run the app on the device by tapping on the Android Studio run button, you'll see how you can scroll the content horizontally.

Now that we know the basics of modifiers, how to create custom composables and measure and position children manually, we'll better understand how modifiers work under the hood.

As a recap, modifiers allow you to customize the behavior of a composable. You can combine multiple modifiers by chaining them together. There are multiple types of modifiers, but in this section, we'll focus on LayoutModifiers since they can change the way a UI component is measured and laid out.

Composables are responsible for their own content and that content may not be inspected or manipulated by a parent unless that composables's author exposes an explicit API to do so. Similarly, modifiers of a composable decorate what they modify in the same opaque way: modifiers are encapsulated.

Analysing a modifier

Since Modifier and LayoutModifier are public interfaces, you can create your own modifiers. As we used Modifier.padding before, let's analyse its implementation to understand modifiers better.

padding is a function that is backed up by a data class that implements the LayoutModifier interface and it's going to override the measure method. As it's a data class, it implements equals() so the modifier can be compared across recomponsitions.

As an example, here's the source code of how padding modifies the size and constraints of the element is applied on:

// How to create a modifier
@Stable
fun Modifier.padding(all: Dp) =
    this + PaddingModifier(start = all, top = all, end = all, bottom = all, rtlAware = true)

// Implementation detail
private data class LayoutPadding(
    start: Dp = 0.dp,
    top: Dp = 0.dp,
    end: Dp = 0.dp,
    bottom: Dp = 0.dp
) : LayoutModifier {
    override fun MeasureScope.measure(
        measurable: Measurable,
        constraints: Constraints,
        layoutDirection: LayoutDirection
    ): MeasureScope.MeasureResult {
        val horizontal = start.toIntPx() + end.toIntPx()
        val vertical = top.toIntPx() + bottom.toIntPx()

        val placeable = measurable.measure(constraints.offset(-horizontal, -vertical))

        val width = constraints.constrainWidth(placeable.width + horizontal)
        val height = constraints.constrainHeight(placeable.height + vertical)
        return layout(width, height) {
            if (rtlAware) {
                placeable.placeRelative(start.toIntPx(), top.toIntPx())
            } else {
                placeable.place(start.toIntPx(), top.toIntPx())
            }
        }
    }
}

The new width of the element will be the width of the child + the start and end padding values coerced to the element's width constraints. The height will be the height of the child + the top and bottom padding values coerced to the element's height constraints.

Order matters

As you saw in the first section, order matters when chaining modifiers as they're applied to the composable they modify from left to right (or from top to bottom in the code snippet), meaning that the measurement and layout of the modifiers on the left will affect the modifier on the right. The final size of the composable depends on all modifiers passed as a parameter.

First, modifiers will update the constraints from left to right, and then, they return back the size from right to left. Let's see this better with an example:

@Composable
fun BodyContent(modifier: Modifier = Modifier) {
    ScrollableRow(
        modifier = modifier
            .background(color = Color.LightGray)
            .size(200.dp)
            .padding(16.dp)
    ) {
        StaggeredGrid {
            for (topic in topics) {
                Chip(modifier = Modifier.padding(8.dp), text = topic)
            }
        }
    }
}

The modifiers applied in this way produce this preview:

First, we change the background to see how modifiers affect the UI, then we restrict the size to have 200.dp width and height, and lastly, we apply padding to add some space between the text and the surroundings.

Because the constraints are propagated through the chain from left to right, the constraints with which the content of the ScrollableRow to be measured are (200-16-16)=168 dp for both minimum and maximum width and height. This means that the size of the StaggeredGrid will be exactly 168x168 dp. Therefore, the final size of the ScrollableRow, after the modifySize chain is run from right to left, will be 200x200 dp.

If we change the order of the modifiers, to apply first the padding and then the size, we get a different UI:

@Composable
fun BodyContent(modifier: Modifier = Modifier) {
    ScrollableRow(
        modifier = modifier
            .background(color = Color.LightGray, shape = RectangleShape)
            .padding(16.dp)
            .size(200.dp)
    ) {
        StaggeredGrid {
            for (topic in topics) {
                Chip(modifier = Modifier.padding(8.dp), text = topic)
            }
        }
    }
}

With preview:

In this case, the constraints that ScrollableRow and padding had originally will be coerced to the size constraints to measure the children. Therefore, the StaggeredGrid will be constrained to 200 dp for both minimum and maximum width and height. The StaggeredGrid size is 200x200 dp and as the size is modified from right to left, the padding modifier will increment the size to (200+16+16)x(200+16+16)=232x232 that will be the final size of ScrollableRow as well.

Layout direction

You can change the layout direction of a composable using the LayoutDirection ambient.

If you're placing composables manually on the screen, the layoutDirection is part of the LayoutScope of the layout modifier or Layout composable. When using layoutDirection, place composables using place as unlike the placeRelative method, it won't automatically mirror the position in right-to-left context.

ConstraintLayout can help you place composables relative to others on the screen and is an alternative to using multiple Rows, Columns and Boxes. ConstraintLayout is useful when implementing larger layouts with more complicated alignment requirements.

ConstraintLayout in Compose works with a DSL:

Let's start with a simple example:

@Composable
fun ConstraintLayoutContent() {
    ConstraintLayout {

        // Create references for the composables to constrain
        val (button, text) = createRefs()

        Button(
            onClick = { /* Do something */ },
            // Assign reference "button" to the Button composable
            // and constrain it to the top of the ConstraintLayout
            modifier = Modifier.constrainAs(button) {
                top.linkTo(parent.top, margin = 16.dp)
            }
        ) {
            Text("Button")
        }

        // Assign reference "text" to the Text composable
        // and constrain it to the bottom of the Button composable
        Text("Text", Modifier.constrainAs(text) {
            top.linkTo(button.bottom, margin = 16.dp)
        })
    }
}

@Preview
@Composable
fun ConstraintLayoutContentPreview() {
    LayoutsCodelabTheme {
        ConstraintLayoutContent()
    }
}

This constrains the top of the Button to the parent with a margin of 16.dp and a Text to the bottom of the Button also with a margin of 16.dp.

If we wanted to center the text horizontally, we can use the centerHorizontallyTo function that sets both the start and end of the Text to the edges of the parent:

@Composable
fun ConstraintLayoutContent() {
    ConstraintLayout {
        ... // Same as before

        Text("Text", Modifier.constrainAs(text) {
            top.linkTo(button.bottom, margin = 16.dp)
            // Centers Text horizontally in the ConstraintLayout
            centerHorizontallyTo(parent)
        })
    }
}

With preview:

ConstraintLayout's size will be as small as possible to wrap its content. That's why it seems Text is centered around the Button instead of the parent. If other sizing behavior is desired, sizing modifiers (e.g. fillMaxSize, size) should be applied to the ConstraintLayout composable as with any other layout in Compose.

Helpers

The DSL also supports creating guidelines, barriers and chains. For example:

@Composable
fun ConstraintLayoutContent() {
    ConstraintLayout {
        // Creates references for the three composables
        // in the ConstraintLayout's body
        val (button1, button2, text) = createRefs()

        Button(
            onClick = { /* Do something */ },
            modifier = Modifier.constrainAs(button1) {
                top.linkTo(parent.top, margin = 16.dp)
            }
        ) { 
            Text("Button 1") 
        }

        Text("Text", Modifier.constrainAs(text) {
            top.linkTo(button1.bottom, margin = 16.dp)
            centerAround(button1.end)
        })

        val barrier = createEndBarrier(button1, text)
        Button(
            onClick = { /* Do something */ },
            modifier = Modifier.constrainAs(button2) {
                top.linkTo(parent.top, margin = 16.dp)
                start.linkTo(barrier)
            }
        ) { 
            Text("Button 2") 
        }
    }
}

With preview:

Note that

Customizing dimensions

By default, the children of ConstraintLayout will be allowed to choose the size they need to wrap their content. For example, this means that a Text is able to go outside the screen bounds when the text is too long:

@Composable
fun LargeConstraintLayout() {
    ConstraintLayout {
        val text = createRef()

        val guideline = createGuidelineFromStart(fraction = 0.5f)
        Text(
            "This is a very very very very very very very long text",
            Modifier.constrainAs(text) {
                linkTo(start = guideline, end = parent.end)
            }
        )
    }
}

@Preview
@Composable
fun LargeConstraintLayoutPreview() {
    LayoutsCodelabTheme {
        LargeConstraintLayout()
    }
}

Obviously, you'd like the text to line break in the available space. To achieve this, we can change the width behavior of the text:

@Composable
fun LargeConstraintLayout() {
    ConstraintLayout {
        val text = createRef()

        val guideline = createGuidelineFromStart(0.5f)
        Text(
            "This is a very very very very very very very long text",
            Modifier.constrainAs(text) {
                linkTo(guideline, parent.end)
                width = Dimension.preferredWrapContent
            }
        )
    }
}

With preview:

Available Dimension behaviors are:

Also, certain Dimensions can be coerced:

width = Dimension.preferredWrapContent.atLeast(100.dp)

Decoupled API

So far, in the examples, constraints have been specified inline, with a modifier in the composable they're applied to. However, there are cases when keeping the constraints decoupled from the layouts they apply to is valuable: the common example is for easily changing the constraints based on the screen configuration or animating between 2 constraint sets.

For these cases, you can use ConstraintLayout in a different way:

  1. Pass in a ConstraintSet as a parameter to ConstraintLayout.
  2. Assign references created in the ConstraintSet to composables using the tag modifier.

This API shape applied to the first ConstraintLayout example shown above optimized for the width of the screen looks like this:

@Composable
fun DecoupledConstraintLayout() {
    WithConstraints {
        val constraints = if (minWidth < 600.dp) {
            decoupledConstraints(margin = 16.dp) // Portrait constraints
        } else {
            decoupledConstraints(margin = 32.dp) // Landscape constraints
        }

        ConstraintLayout(constraints) {
            Button(
                onClick = { /* Do something */ },
                modifier = Modifier.layoutId("button")
            ) {
                Text("Button")
            }

            Text("Text", Modifier.layoutId("text"))
        }
    }
}

private fun decoupledConstraints(margin: Dp): ConstraintSet {
    return ConstraintSet {
        val button = createRefFor("button")
        val text = createRefFor("text")

        constrain(button) {
            top.linkTo(parent.top, margin= margin)
        }
        constrain(text) {
            top.linkTo(button.bottom, margin)
        }
    }
}

One of the rules of Compose is that you should only measure your children once; measuring children twice throws a runtime exception. However, there are times when you need some information about your children before measuring them.

Intrinsics lets you query children before they're actually measured.

To a composable, you can ask for its intrinsicWidth or intrinsicHeight:

For example, if you ask the minIntrinsicHeight of a Text with infinite width, it'll return the height of the Text as if the text was drawn in a single line.

Intrinsics in action

Imagine that we want to create a composable that displays two texts on the screen separated by a divider like this:

How can we do this? We can have a Row with two Texts inside that expands as much as they can and a Divider in the middle. We want the Divider to be as tall as the tallest Text and thin (width = 1.dp).

@Composable
fun TwoTexts(modifier: Modifier = Modifier, text1: String, text2: String) {
    Row(modifier = modifier) {
        Text(modifier = Modifier.weight(1f).padding(start = 4.dp), text = text1)
        Divider(color = Color.Black, modifier = Modifier.fillMaxHeight().preferredWidth(1.dp))
        Text(
            modifier = Modifier.weight(1f)
                    .padding(end = 4.dp)
                    .wrapContentWidth(Alignment.End),

            text = text2
        )
    }
}

@Preview
@Composable
fun TwoTextsPreview() {
    LayoutsCodelabTheme {
        Surface {
            TwoTexts(text1 = "Hi", text2 = "there")
        }
    }
}

If we preview this, we see that the Divider expands to the whole screen and that's not what we want:

This happens because Row measures each child individually and the height of Text cannot be used to constraint the Divider. We want the Divider to fill the available space with a given height. For that, we can use the preferredHeight(IntrinsicSize.Min) modifier .

preferredHeight(IntrinsicSize.Min) sizes its children being forced to be as tall as their minimum intrinsic height. As it's recursive, it'll query Row and its children minIntrinsicHeight.

Applying that to our code, it'll work as expected. As the usage of preferredHeight with intrinsics is still an experimental API, you need to annotate the composable and the preview with @ExperimentalLayout:

@ExperimentalLayout
@Composable
fun TwoTexts(modifier: Modifier = Modifier, text1: String, text2: String) {
    Row(modifier = modifier.preferredHeight(IntrinsicSize.Min)) {
        Text(modifier = Modifier.weight(1f).padding(start = 4.dp), text = text1)
        Divider(color = Color.Black, modifier = Modifier.fillMaxHeight().preferredWidth(1.dp))
        Text(
            modifier = Modifier.weight(1f)
                .padding(end = 4.dp)
                .wrapContentWidth(Alignment.End),
            text = text2
        )
    }
}

@ExperimentalLayout
@Preview
@Composable
fun TwoTextsPreview() {
    LayoutsCodelabTheme {
        Surface {
            TwoTexts(text1 = "Hi", text2 = "there")
        }
    }
}

With preview:

Row's minIntrinsicHeight will be the maximum minIntrinsicHeight of its children. Divider's minIntrinsicHeight is 0 as it doesn't occupy space if no constraints are given; Text's minIntrinsicHeight will be that of the text given a specific width. Therefore, Row's height constraint will be the max minIntrinsicHeight of the Texts. Divider will then expand its height to the height constraint given by Row.

DIY

Whenever you are creating your custom layout, you can modify how intrinsics are calculated with the (min|max)Intrinsic(Width|Height)MeasureBlock parameters; however, the defaults should be enough most of the time.

Also, you can modify intrinsics with modifiers overriding the Density.(min|max)Intrinsic(Width|Height)Of methods of the Modifier interface which also have a good default.

Congratulations, you've successfully completed this codelab!

Solution to the codelab

You can get the code for the solution of this codelab from GitHub:

$ git clone https://github.com/googlecodelabs/android-compose-codelabs

Alternatively you can download the repository as a Zip file:

Download Zip

What's next?

Check out the other codelabs on the Compose pathway:

Further reading

Sample apps