Skip to content
Application Development II
GitLabGitHub

Layouts in Compose

The basics of designing and building a User Interface layout in Compose
Overview

These notes have been adapted from Compose layout basics.

It may seem counter-intuitive to create a user interface out of functions, rather than out of tags like you are used to with markup languages like HTML. Where are the components rendered on a screen? How can we control the “flow” of the interface?

This lecture focuses on the layout of @Composable functions, explaining some of the building blocks Compose provides to help you lay out your UI elements.

Review from last lecture: @Composable functions are the basic building block of Compose. A composable function is a function emitting Unit that describes some part of your UI. The function takes some input and generates what’s shown on the screen.

A single @Composable function can emit several UI elements. In the ArtistCard function below, two Text elements are generated:

@Composable
fun ArtistCard() {
Text("Alfred Sisley")
Text("3 minutes ago")
}
@Composable
fun ArtistCard() {
Text("Alfred Sisley")
Text("3 minutes ago")
}

Compose does not handle layout automatically, and may arrange the elements in a way you don’t like. In the above example, Compose stacks the text elements on top of eachother, making them unreadable.

The reason this happens is that the ArtistCard() function has the instruction to draw two Text elements, but does NOT have instructions about the location those elements should be drawn.

Compose comes built-in with a variety of layout components that we can use. We can then compose these elements to define our own layouts.

Application user interfaces are (mostly) two dimensional, and made up of “boxes”. The easiest way to fix the ArtistCard(), then, is to arrange these “boxes” in either a Column or a Row, which are happily built-in for us:

@Composable
fun ArtistCardColumn() {
Column {
Text("Alfred Sisley")
Text("3 minutes ago")
}
}
@Composable
fun ArtistCardColumn() {
Column {
Text("Alfred Sisley")
Text("3 minutes ago")
}
}

While using the Row element alone would unravel the stack, it’s not as interesting to place two text elements side-by-side. Instead, we can nest @Composable layout elements to produce more complicated layouts, such as a Column within a Row:

@Composable
fun ArtistCardRow(artist: Artist) {
Row(verticalAlignment = Alignment.CenterVertically) {
Image(bitmap = artist.image, contentDescription = "Artist image")
Column {
Text(artist.name)
Text(artist.lastSeenOnline)
}
}
}
@Composable
fun ArtistCardRow(artist: Artist) {
Row(verticalAlignment = Alignment.CenterVertically) {
Image(bitmap = artist.image, contentDescription = "Artist image")
Column {
Text(artist.name)
Text(artist.lastSeenOnline)
}
}
}

Finally, the Box element allows you to control how elements are stacked upon eachother, allowing you to create stacks of elements where overlap is desired:

@Composable
fun ArtistAvatar(artist: Artist) {
Box {
Image(bitmap = artist.image, contentDescription = "Artist image")
Icon(Icons.Filled.Check, contentDescription = "Check mark")
}
}
@Composable
fun ArtistAvatar(artist: Artist) {
Box {
Image(bitmap = artist.image, contentDescription = "Artist image")
Icon(Icons.Filled.Check, contentDescription = "Check mark")
}
}

The primary layout components Column, Row, and Box are expressive and powerful in combination, and are often all you need to create complex layouts. You can write your own composable function to combine these layouts into a more elaborate layout that suits your app.

So far, it appears as though Jetpack Compose centers elements horizontally and vertically within their containers. This is pretty nice default behavior, but it would be nicer if we could control it — so of course, we can. We can see some useful parameters for this by taking a look at the definitions for each of Row, Column, and Box:

Kotlin
@Composable
inline fun Row(
modifier: Modifier = Modifier,
horizontalArrangement: Arrangement.Horizontal = Arrangement.Start,
verticalAlignment: Alignment.Vertical = Alignment.Top,
content: @Composable RowScope.() -> Unit
): Unit
Kotlin
@Composable
inline fun Row(
modifier: Modifier = Modifier,
horizontalArrangement: Arrangement.Horizontal = Arrangement.Start,
verticalAlignment: Alignment.Vertical = Alignment.Top,
content: @Composable RowScope.() -> Unit
): Unit

To set children’s position within a Row, set the horizontalArrangement and verticalAlignment arguments. For a Column, set the verticalArrangement and horizontalAlignment arguments.

Below follow some of the possible arguments for each, and their effects. You can see the range of possible Alignments and Arrangments by clicking on these corresponding links.

The row arrangement arguments and their effects

These notes have been adapted from Custom layouts in Compose.

As we have seen, UI elements in Compose are represented by the @Composable functions that emit a piece of UI when invoked. Since composables are functions, each composable can call other composable functions within their function body, allowing us to define nested UI structres. Behind the scenes, these composable UI elements are added to a UI tree that gets rendered on the screen. How is this “tree” modeled?

Each UI element has one parent and zero to many children. Each element is also located within its parent, specified as an (x, y) position, and a size, specified as a width and a height.

That is, parent elements define the constraints for their child elements, while child elements take up the space made available to them by their parent.

Constraints restrict the minimum and maximum width and height of an element. If an element has child elements, it may measure each child to help determine its own size (and may even expand to make more room, if that parent has room to expand). Once an element determines and reports its own size, it defines how to place its child elements relative to itself.

Laying out each node in the UI tree is therefore a three step process. Each node must:

  • Measure any children
  • Decide its own size
  • Place its children

Since UI is a tree structure, an initial render of the UI recursively performs this three-step process on all nodes. Afterwards, nodes are lazily recomposed whenever there are UI changes, ensuring that only nodes which require updates are rerendered.

We’ll see in the Layout modifiers section how you can use the layout model to customize these behaviors in Compose.

These notes have been adapted from Compose modifiers and API Guidelines for Jetpack Compose.

Modifiers allow you to decorate or augment a composable. Modifiers let you do these sorts of things:

  • Change the composable’s size, layout, behavior, and appearance
  • Add information, like accessibility labels
  • Process user input
  • Add high-level interactions, like making an element clickable, scrollable, draggable, or zoomable

Modifiers are standard Kotlin objects. Create a modifier by calling one of the Modifier class functions:

Kotlin
@Composable
private fun Greeting(name: String) {
Column(modifier = Modifier.padding(24.dp)) {
Text(text = "Hello,")
Text(text = name)
}
}
Kotlin
@Composable
private fun Greeting(name: String) {
Column(modifier = Modifier.padding(24.dp)) {
Text(text = "Hello,")
Text(text = name)
}
}

Modifier functions are factory extension functions on Modifier, (that is, each of these extention functions returns a modified Modifer) allowing us to chain Modifier function class for combined effects:

@Composable
private fun Greeting(name: String) {
Column(
modifier = Modifier
.padding(24.dp),
.fillMaxWidth()
) {
Text(text = "Hello,")
Text(text = name)
}
}
@Composable
private fun Greeting(name: String) {
Column(
modifier = Modifier
.padding(24.dp),
.fillMaxWidth()
) {
Text(text = "Hello,")
Text(text = name)
}
}

In the code above, notice different modifier functions used together.

  • padding puts space around an element.
  • fillMaxWidth makes the composable fill the maximum width given to it from its parent.

All the @Composables your write should accept an optional modifier parameter, and pass that modifier to its first child that emits UI. Doing so makes your code more reusable and makes its behavior more predictable and intuitive:

(Kotlin)
@Composable
fun FancyButton(
text: String,
onClick: () -> Unit,
modifier: Modifier = Modifier // Optional parameter (has a default argument)
) = Text(
text = text,
modifier = modifier.surface(elevation = 4.dp) // modifier passed to child (with customizations)
.clickable(onClick)
.padding(horizontal = 32.dp, vertical = 16.dp)
)
(Kotlin)
@Composable
fun FancyButton(
text: String,
onClick: () -> Unit,
modifier: Modifier = Modifier // Optional parameter (has a default argument)
) = Text(
text = text,
modifier = modifier.surface(elevation = 4.dp) // modifier passed to child (with customizations)
.clickable(onClick)
.padding(horizontal = 32.dp, vertical = 16.dp)
)

Modifiers are the standard means of adding external behavior and customizations to an element in Compose. Through the Modifier parameter, we don’t have to worry about defining common behavior/customizations for each @Composable we create. This allows element APIs to be smaller and more focused, as Modifiers are used to decorate those elements with standard behavior.

A best practise is for the Modifier to occupy the first optional parameter slot in a @Composable. This sets a consistent expectation for developers: they can always provide a modifier as the final positional parameter to an element call for any given element’s common case.

For more information, see the Compose API guidelines, Elements accept and respect a Modifier parameter.

The order of modifier functions in a chain of calls is significant. Since each function makes changes to the Modifier returned by the previous function, the sequence order affects the final result. Let’s see an example of this:

@Composable
fun ArtistCard(/*...*/) {
val padding = 16.dp
Column(
Modifier
.clickable(onClick = onClick)
.padding(padding)
.fillMaxWidth()
) {
// rest of the implementation
}
}
@Composable
fun ArtistCard(/*...*/) {
val padding = 16.dp
Column(
Modifier
.clickable(onClick = onClick)
.padding(padding)
.fillMaxWidth()
) {
// rest of the implementation
}
}

In the code above the whole area is clickable, including the surrounding padding, because the padding modifier has been applied after the clickable modifier. If the modifiers order is reversed, the space added by padding does not react to user input:

@Composable
fun ArtistCard(/*...*/) {
val padding = 16.dp
Column(
Modifier
.padding(padding)
.clickable(onClick = onClick)
.fillMaxWidth()
) {
// rest of the implementation
}
}
@Composable
fun ArtistCard(/*...*/) {
val padding = 16.dp
Column(
Modifier
.padding(padding)
.clickable(onClick = onClick)
.fillMaxWidth()
) {
// rest of the implementation
}
}

The explicit ordering of Modifier chains helps developers create predictable and controllable outcomes from a variety of Modifier combinations.

Jetpack Compose provides a list of built-in modifiers to help you decorate or augment a composable. Here are some common modifiers you’ll use to adjust your layouts.

By default, layouts provided in Compose wrap their children. However, you can set a size by using the size modifier:

Kotlin
@Composable
fun ArtistCard(/*...*/) {
Row(
modifier = Modifier.size(width = 400.dp, height = 100.dp)
) {
Image(/*...*/)
Column { /*...*/ }
}
}
Kotlin
@Composable
fun ArtistCard(/*...*/) {
Row(
modifier = Modifier.size(width = 400.dp, height = 100.dp)
) {
Image(/*...*/)
Column { /*...*/ }
}
}

Note that the size you specified might not be respected if it does not satisfy the constraints coming from the layout’s parent (see the layout model. If you require the composable size to be fixed regardless of the incoming constraints, use the requiredSize modifier:

Kotlin
@Composable
fun ArtistCard(/*...*/) {
Row(
modifier = Modifier.size(width = 400.dp, height = 100.dp)
) {
Image(
/*...*/
modifier = Modifier.requiredSize(150.dp)
)
Column { /*...*/ }
}
}
Kotlin
@Composable
fun ArtistCard(/*...*/) {
Row(
modifier = Modifier.size(width = 400.dp, height = 100.dp)
) {
Image(
/*...*/
modifier = Modifier.requiredSize(150.dp)
)
Column { /*...*/ }
}
}

In this example, even with the parent height set to 100.dp, the height of the Image will be 150.dp, as the requiredSize modifier takes precedence.

If you want a child layout to fill all the available height allowed by the parent, add the fillMaxHeight modifier (Compose also provides fillMaxSize and fillMaxWidth):

Kotlin
@Composable
fun ArtistCard(/*...*/) {
Row(
modifier = Modifier.size(width = 400.dp, height = 100.dp)
) {
Image(
/*...*/
modifier = Modifier.fillMaxHeight()
)
Column { /*...*/ }
}
}
Kotlin
@Composable
fun ArtistCard(/*...*/) {
Row(
modifier = Modifier.size(width = 400.dp, height = 100.dp)
) {
Image(
/*...*/
modifier = Modifier.fillMaxHeight()
)
Column { /*...*/ }
}
}

To add padding all around an element, set a padding modifier.

If you want to add padding above a text baseline such that you achieve a specific distance from the top of the layout to the baseline, use the paddingFromBaseline modifier:

Kotlin
@Composable
fun ArtistCard(artist: Artist) {
Row(/*...*/) {
Column {
Text(
text = artist.name,
modifier = Modifier.paddingFromBaseline(top = 50.dp)
)
Text(artist.lastSeenOnline)
}
}
}
Kotlin
@Composable
fun ArtistCard(artist: Artist) {
Row(/*...*/) {
Column {
Text(
text = artist.name,
modifier = Modifier.paddingFromBaseline(top = 50.dp)
)
Text(artist.lastSeenOnline)
}
}
}

To position a layout relative to its original position, add the offset modifier and set the offset in the x and y axis. Offsets can be positive as well as non-positive. The difference between padding and offset is that adding an offset to a composable does not change its measurements:

Kotlin
@Composable
fun ArtistCard(artist: Artist) {
Row(/*...*/) {
Column {
Text(artist.name)
Text(
text = artist.lastSeenOnline,
modifier = Modifier.offset(x = 4.dp)
)
}
}
}
Kotlin
@Composable
fun ArtistCard(artist: Artist) {
Row(/*...*/) {
Column {
Text(artist.name)
Text(
text = artist.lastSeenOnline,
modifier = Modifier.offset(x = 4.dp)
)
}
}
}

The offset modifier is applied horizontally according to the layout direction — by default, shifting an element to the right; but shifting an element to the left in a right-to-left context (see link for more detail).

See “Use the layout modifier” on the Developer Android docs. (notes to be updated soon.)

In Compose, there are modifiers that can only be used when applied to children of certain composables. Compose enforces this by means of custom scopes.

For example, if you want to make a child as big as the parent Box without affecting the Box size, use the matchParentSize modifier. matchParentSize is only available in BoxScope. Therefore, it can only be used on a child within a Box parent.

As mentioned above, if you want a child layout to be the same size as a parent Box without affecting the Box size, use the matchParentSize modifier.

Note that matchParentSize is only available within a Box scope, meaning that it only applies to direct children of Box composables.

In the example below, the child Spacer takes its size from its parent Box, which in turn takes its size from the biggest children, ArtistCard in this case.

fun MatchParentSizeComposable() {
Box {
Spacer(
Modifier
.matchParentSize() // .fillMaxSize()
.background(Color.LightGray)
)
ArtistCard()
}
}
fun MatchParentSizeComposable() {
Box {
Spacer(
Modifier
.matchParentSize() // .fillMaxSize()
.background(Color.LightGray)
)
ArtistCard()
}
}

As you have seen in the previous section, by default, a composable size is defined by the content it is wrapping (i.e. by the size of its children). You can set a composable size to be flexible within its parent using the weight Modifier that is only available in RowScope and ColumnScope.

Let’s take a Row that contains two Box composables. The first box is given twice the weight of the second, so it’s given twice the width. Since the Row is 210.dp wide, the first Box is 140.dp wide, and the second is 70.dp:

@Composable
fun ArtistCard(/*...*/) {
Row(
modifier = Modifier.fillMaxWidth()
) {
Image(
/*...*/
modifier = Modifier.weight(2f)
)
Column(
modifier = Modifier.weight(1f)
) {
/*...*/
}
}
}
@Composable
fun ArtistCard(/*...*/) {
Row(
modifier = Modifier.fillMaxWidth()
) {
Image(
/*...*/
modifier = Modifier.weight(2f)
)
Column(
modifier = Modifier.weight(1f)
) {
/*...*/
}
}
}

Multiple modifiers can be chained together to decorate or augment a composable. This chain is created via the Modifier interface which represents an ordered, immutable list of single Modifier.Elements

Create your own Modifier chains and extract them to reuse them on multiple composable components. It is completely fine to just save a modifier, as they are data-like objects:

Kotlin
val reusableModifier = Modifier
.fillMaxWidth()
.background(Color.Red)
.padding(12.dp)
Kotlin
val reusableModifier = Modifier
.fillMaxWidth()
.background(Color.Red)
.padding(12.dp)

Each Modifier.Element represents an individual behavior, like layout, drawing and graphics behaviors, all gesture-related, focus and semantics behaviors, as well as device input events. Their ordering matters: modifier elements that are added first will be applied first.

Sometimes it can be beneficial to reuse the same modifier chain instances in multiple composables, by extracting them into variables and hoisting them into higher scopes. It can improve code readability or help improve your app’s performance for a few reasons:

  • The re-allocation of the modifiers won’t be repeated when recomposition occurs for composables that use them
  • Modifier chains could potentially be very long and complex, so reusing the same instance of a chain can alleviate the workload Compose runtime needs to do when comparing them
  • This extraction promotes code cleanliness, consistency and maintainability across the codebase

You can see a complete set of best practises for reusing modifiers by following the link.