Fundamentals of Compose layouts and modifiers

Episode 1 of MAD Skills: Compose Layouts and Modifiers

Simona Milanović
Android Developers

--

Welcome to the MAD Skills series on Jetpack Compose layouts and modifiers! In this first post, we’re going to start our journey by explaining the fundamentals of layouts and modifiers. We’ll go over how they work together, what out-of-the-box APIs Compose offers, and how to beautifully style your UI — all this while building a screen for a mini pixelated game called Android Aliens!

Greetings, humans

If you haven’t checked out the previous MAD Skills series on Compose basics, we suggest you do so before continuing, as it will provide you with a good basis to build on top of.

You can also watch this post as a MAD Skills video:

Layouts — because almost everything in Compose is a layout

Layouts are the core components of Compose UI which enable you to make stunning apps with a variety of provided, ready-to-use APIs, as well as build your own custom solutions. In Compose, you use composable functions to emit portions of your UI, but layouts are the ones that specify the precise arrangement and alignment of those elements.

Therefore, layouts, whether you use the ones Compose provides for free or build your own custom ones, are a vital part of the Compose world. You can look at them as Compose coordinators — dictating the structure of other composables nested within. In fact, we’ll see later in this series that almost everything in Compose UI is a layout! But we’ll get to that later on.

Modifiers — chaining it all together!

Now, if you’ve used Compose before (⭐ for you if you have!), you must have noticed how important and crucial modifiers are to this framework. They allow you to decorate and augment composables, moulding them the way you need them to be and enabling them to do what you need them to do. By chaining as many modifiers as you like, you can:

Now that we’ve covered the main points on layouts and modifiers, let’s see them in action!

Start Game

Let’s roll up our sleeves and build a fun game screen to understand how layouts and modifiers work together. We’ll start off by using these two Android Alien ships and gradually build up to a full screenshot of the game:

So we need two ships! The reusable AndroidAlien composable looks like this:

/* Copyright 2023 Google LLC. 
SPDX-License-Identifier: Apache-2.0 */

@Composable
fun AndroidAlien(
color: Color,
modifier: Modifier = Modifier,
) {
Image(
modifier = modifier,
painter = painterResource(R.drawable.android_alien),
colorFilter = ColorFilter.tint(color = color),
contentDescription = null
)
}

Then, calling this composable twice, we add a green and a red alien to the parent AndroidAliens:

/* Copyright 2023 Google LLC. 
SPDX-License-Identifier: Apache-2.0 */

@Composable
fun AndroidAliens() {
AndroidAlien(
color = Color.Red,
modifier = Modifier
.size(70.dp)
.padding(4.dp)
)
AndroidAlien(
color = Color.Green,
modifier = Modifier
.size(70.dp)
.padding(4.dp)
)
}

Two things to explain here on the modifier usage when building simple composables like these:

  1. In the first snippet, we set a modifier parameter with a default argument, as per Compose API best practices
  2. In the second snippet, we pass a modifier chain consisting of two very common modifiers to set the size and padding of the image

Just reading the last code sample, we might want (and expect) Compose to lay these child composables out in a specific order. However, when we run it, we just see one element on the screen:

A single, lonely Android Alien

The red ship seems to be missing. To debug this, let’s increase the size of the red ship:

/* Copyright 2023 Google LLC. 
SPDX-License-Identifier: Apache-2.0 */

@Composable
fun AndroidAliens() {
AndroidAlien(
color = Color.Red,
modifier = Modifier.size(100.dp)
)
// …
}

The red ship seems a bit shy and is hiding behind the green one 🤔. Meaning, these two composables are in fact overlapping each other, as they’re missing specific instructions on how to be laid out. Compose can do a lot of things, but it cannot read your mind! This tells us that our ships need some structure and formations, and that is precisely what Compose layouts do.

Laying out elements vertically and horizontally

Rather than having our alien ships completely overlapping each other, we want them to be laid out one next to each other — a very common use case for UI elements in Android apps in general. We’d like our ships to be positioned side by side, as well as one above the other:

To do this, Compose offers Column and Row layout composables, for laying out elements vertically and horizontally, respectively.

Let’s first move our two AlienShips inside a Row to lay them out in a horizontal formation:

/* Copyright 2023 Google LLC. 
SPDX-License-Identifier: Apache-2.0 */

@Composable
fun AndroidAliensRow() {
Row {
AndroidAlien(…)
AndroidAlien(…)
}
}

Conversely, to arrange items vertically, wrap your ships in a Column composable:

/* Copyright 2023 Google LLC. 
SPDX-License-Identifier: Apache-2.0 */

@Composable
fun AndroidAliensColumn() {
Column {
AlienShip(...)
AlienShip(...)
}
}

Now, this looks fine but we want to tweak their positioning a bit more. We want them to be aligned on our screen, like this:

We want the spaceships to be “glued” to a specific corner of our screen, like the bottom end. This means we need to align and arrange these ships accordingly, while still keeping them in Column and Row formations.

To specify such precise positioning of elements within your Column and Row parents, you can use Arrangements snd Alignments. These properties instruct layouts on how to position its children. Meaning, if you wanted your nested ships to be “glued” to the bottom end of the Column parent, you set the following:

/* Copyright 2023 Google LLC. 
SPDX-License-Identifier: Apache-2.0 */

@Composable
fun AndroidAliensRow() {
Row(
horizontalArrangement = Arrangement.Center,
verticalAlignment = Alignment.Top,
) {
AndroidAlien(…)
AndroidAlien(…)
}
}
/* Copyright 2023 Google LLC. 
SPDX-License-Identifier: Apache-2.0 */

@Composable
fun AndroidAliensColumn() {
Column(
verticalArrangement = Arrangement.Top,
horizontalAlignment = Alignment.CenterHorizontally,
) {
AndroidAlien(…)
AndroidAlien(…)
}
}

However, when we run the app, we can still see our alien ships as they were previously:

We can’t glue them to the corner because layouts like Column and Row wrap around the size of their children and don’t go beyond that, so we cannot see the effects of the set Arrangements and Alignments. To have the Column expand across the entire available size, we will use the .fillMaxSize() modifier, which has a very self-explanatory name:

/* Copyright 2023 Google LLC. 
SPDX-License-Identifier: Apache-2.0 */

@Composable
fun AndroidAliensRow() {
Row(
modifier = Modifier.fillMaxSize(),
horizontalArrangement = Arrangement.Center,
verticalAlignment = Alignment.Top,
) {
AndroidAlien(…)
AndroidAlien(…)
}
}
/* Copyright 2023 Google LLC. 
SPDX-License-Identifier: Apache-2.0 */

@Composable
fun AndroidAliensColumn() {
Column(
modifier = Modifier.fillMaxSize(),
verticalArrangement = Arrangement.Top,
horizontalAlignment = Alignment.CenterHorizontally,
) {
AndroidAlien(…)
AndroidAlien(…)
}
}

That does the trick! Notice how Column uses a vertical arrangement and horizontal alignment, whereas Row accepts the horizontal arrangement and vertical alignment. Why is that?! Let’s explain the difference between the two in more detail.

Arrangement refers to how the layout children are laid out on the main axis — vertical for Column and horizontal for Row. Alignment does the same, but on the cross axis — horizontal for Column and vertical for Row:

There are a number of properties to choose from and adjust your composables with. You can even space out your aliens in various ways so that they aren’t too cozy with each other. Take a look at the documentation and explore all options that might suit your design requirements.

Breaking away from the rules

Aligning and arranging components in layouts in such a way ensures that the same rules are applied to all children. But what happens when we want a third, rogue alien ship to break away from the imposed rules? Let’s break down the exact positioning — we want the green and blue aliens to follow the parent rule instructions, but we want the red alien to rebel and “escape”:

Rebel yell

To achieve this, Compose offers the .align() modifier to be used on the specific child composable that you wish to position individually, defying the rules enforced by the parent:

/* Copyright 2023 Google LLC. 
SPDX-License-Identifier: Apache-2.0 */

@Composable
fun AndroidAliensRow() {
Row(…) {
AndroidAlien(…)
AndroidAlien(…)
AndroidAlien(…)
AndroidAlien(
color = Color.Red,
modifier = Modifier.align(Arrangement.CenterVertically)
) // Rogue Rebellion ship
}
}

Let’s continue with our game build up and see what happens when a big alien mothership enters the arena!

As the name hints, we want the mothership to be larger than the rest of its alien subjects. In this alien row formation, the regular ships should occupy the minimal width they need to render correctly and the red mothership should occupy the remaining width of the row:

This is a common use case where you want only some of your child UI elements to have a bigger or smaller size than the rest. You could manually change the size of just that one element, but that might be cumbersome and you might want the size to be relative to the rest of the elements, rather than a static, fixed value. For this, Column and Row composables offer the .weight() modifier:

/* Copyright 2023 Google LLC. 
SPDX-License-Identifier: Apache-2.0 */

@Composable
fun AndroidAliensRow() {
Row(…) {
AndroidAlien(modifier = Modifier.size(70.dp)) // Takes exactly 70 DP
AndroidAlien(modifier = Modifier.weight(1F)) // Gets the rest
AndroidAlien(modifier = Modifier.size(70.dp)) // Takes exactly 70 DP
}
}

The mothership has completely hogged the game space! Not assigning weights to the other elements means that they would only occupy the width (in Row) or height (in Column) as they require, and the remaining width/height is equally distributed to the elements which have assigned weights.

But what if we want the other, regular ships to occupy precisely 1/4 of the width and the mothership 2/4? Then, we can assign weights in the following manner:

/* Copyright 2023 Google LLC. 
SPDX-License-Identifier: Apache-2.0 */

@Composable
fun AndroidAliensRow() {
Row(…) {
AndroidAlien(modifier = Modifier.weight(1F))
AndroidAlien(modifier = Modifier.weight(2F))
AndroidAlien(modifier = Modifier.weight(1F))
}
}

You might notice the regular alien ships are now larger as well. The default behaviour of assigning weights is to resize the item as well, to fit the newly assigned width/height. However, if you wish to keep an element in its original size, you can pass false to the fill parameter of this modifier:

/* Copyright 2023 Google LLC. 
SPDX-License-Identifier: Apache-2.0 */

AndroidAlien(
modifier = Modifier
.size(70.dp)
.weight(1F, fill = false)
)

Now that we’ve covered how to lay out elements vertically and horizontally with Compose’s simple, but powerful Columns and Rows, as well as some handy modifier to make your UI super neat, let’s move on!

Stacking elements

An important feature of our game is to let you know when it’s GAME OVER 😢. Let’s look at the design where you’d need to display this overlay on top of the alien formation:

You can see this requires stacking the text on top of the aliens, to make it super clear to you that the game is over. In Compose, you can use the Box layout as a quick way of putting elements on top of each other or overlapping them, following the order the composables are executed in:

/* Copyright 2023 Google LLC. 
SPDX-License-Identifier: Apache-2.0 */

@Composable
fun AndroidAliensGameOverBox() {
Box {
AndroidAliensRow(…)
Text(
text = “GAME OVER”
// …
)
}
}

Similarly to our previous layouts, the Box composable also offers a way of instructing it how to lay out its nested children. The difference here is that there’s no differentiation between horizontal and vertical — instead, they’re merged into one contentAlignment:

/* Copyright 2023 Google LLC. 
SPDX-License-Identifier: Apache-2.0 */

@Composable
fun AndroidAliensGameOverBox() {
Box(
modifier = Modifier.fillMaxSize(),
contentAlignment = Alignment.Center
) {
AndroidAliensRow(…)
Text(
text = “GAME OVER”
// …
)
}
}

In the previous code snippet, we used contentAlignment = Alignment.Center to align child elements in the center, however you could also use any of the existing 2D alignments. This makes Box layout very useful when you wish to position the nested children, well, pretty much wherever you want within it!

If the game is indeed over, your main game screen might look nicer with an overlaid transparent background to make it even more apparent that you simply cannot play anymore:

If only there was a way to set a gray, transparent background behind the “GAME OVER” Text and expand it to cover the available size 🤔… And there is!

Box layout provides a handy .matchParentSize() modifier that allows a child element to match the size of the containing Box:

/* Copyright 2023 Google LLC. 
SPDX-License-Identifier: Apache-2.0 */

@Composable
fun AndroidAliensGameOverBox() {
Box(…) {
AndroidAliensRow(…)
Spacer(
modifier = Modifier
.matchParentSize()
.background(color = Color.Gray.copy(alpha = .7f))
)
Text(…)
}
}

Keep in mind however, that this child element does not take part in defining the final size of the Box parent. Instead, it matches the size of the Box after all other children (not using matchParentSize() modifier) have been measured first to obtain the full Box’s size. In contrast, the .fillMaxSize() modifier, which makes an element occupy all available space, will take part in defining the size of the Box.

🚨 You might notice that matchParentSize is only available within a Box composable. Why and how? This leads us to a very important modifier concept in Compose: scope safety.

Modifier scope safety

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. That is how matchParentSize is only available in BoxScope and weight in ColumnScope and RowScope. This prevents you from adding modifiers that simply won’t work in other places and saves you time from trial and error.

Slotted components

Our mini game could definitely benefit from some information on the game progress while we play — like a header with the current score and a button at the bottom to start or pause the game, which wrap the rest of the game content in between:

This might remind you of having something like a top and bottom bar, where our header and button would be placed in. To implement this, Compose offers a very handy set of out-of-the-box composables to use. And so we come to Material components!

Jetpack Compose offers an implementation of Material Design, a comprehensive design system for creating digital interfaces. Material components, such as buttons, cards, switches, as well as layouts like Scaffold, are available as composable functions. Together, they represent interactive building blocks for creating a user interface. There are many to choose from, so make sure to check out the Compose Material API reference for details.

In our case of adding a header as a top and a button as a bottom bar, Compose and Material provide the Scaffold layout, which contains slots for various components to be laid out into common screen patterns. So let’s add our start button and score header as top and bottom bars:

/* Copyright 2023 Google LLC. 
SPDX-License-Identifier: Apache-2.0 */

@Composable
fun AndroidAliensWithInfo() {
Scaffold(
topBar = {
InfoHeader(…)
},
bottomBar = {
Button(…) {
Text(
text = “PRESS START”
)
}
}
) {
AndroidAliens(…)
}
}

Other common use cases for the top and bottom bar are the evergreen toolbar and the bottom navigation bar. But since the Scaffold composable parameters accept generic composable lambdas, you can pass in any type of a composable you’d like:

/* Copyright 2023 Google LLC. 
SPDX-License-Identifier: Apache-2.0 */

@Composable
fun Scaffold(
// …
topBar: @Composable () -> Unit = {},
bottomBar: @Composable () -> Unit = {},
// …
)
/* Copyright 2023 Google LLC. 
SPDX-License-Identifier: Apache-2.0 */

@Composable
fun AndroidAliensWithInfo() {
Scaffold(
topBar = {
ComposeShipsRow(…)
},
bottomBar = {
AndroidAliensRow(…)
}
) {
AndroidAliens(…)
}
}

This open slotted concept is called Slot API and is heavily used across Compose. Scaffold also accepts floating action buttons, snackbars, and many other customization options.

Now, you might notice we’ve used a Button composable as the bottom bar. This is just one more of many out-of-the-box solutions that Material components offer. It enables a quick way of adding a button to your screen and providing any kind of content inside it — text, images and others, thus also using the Slot API concept. For learning more about the entire Material portfolio, check out the Material components documentation.

Fantastic! Our game is progressing really well. But as with our game, our Compose tutorial also increases the difficulty with each level. So let’s move onto the next challenge.

Adding content on demand

So far, we’ve had a very limited amount of alien ships on the screen in a simple SVG form. But what if we had hundreds or thousands even? And what if they’re animated?!

AlienShipStackOverflow

This little invasion could cause some serious jank in that case. Loading up all of your content at once from the backend, especially if it contains large data sets, heavy images or videos, can impact your app’s performance. What if instead you could load your content on demand, bit by bit when scrolling? Cutscene to my personal favorites, the Lazy components in Compose.

Lazy lists render a scrollable list of items as they become visible on the screen, rather than all at once. To build an impressively quick grid of green Android Aliens, we will use the LazyVerticalGrid composable, then set a fixed amount of columns on it and add 50 AndroidAlien items to it:

/* Copyright 2023 Google LLC. 
SPDX-License-Identifier: Apache-2.0 */

@Composable
fun AndroidAliensGrid() {
// Specify that the grid should have 5 columns precisely
LazyVerticalGrid(columns = GridCells.Fixed(5)) {
// Add 50 items or green Android aliens
items(50) {
AndroidAlien(…)
}
}
}

There is a number of additional Lazy components to choose from: LazyHorizontalGrid, LazyColumn, LazyRow and most recently, staggered grids — LazyVerticalStaggeredGrid and LazyHorizontalStaggeredGrid.

Lazy layouts are a massive Compose area and could easily merit an entire post for themselves, so to get the best and most exhaustive information on this API, check out the Lazy layouts in Compose video where we explain everything there is to know about Lazy layouts.

Game check point

We have covered A LOT today! From the very basics on Compose layouts and modifiers and their mutual collaboration, what out-of-the-box APIs are offered, Material components and Slot APIs, as well as how to add content on demand — all that while building a game screen of our own.

Stay tuned for the next post where we cover phases of Compose under the hood. And of course, don’t forget to subscribe for regular updates!

Additional resources:

Basic layouts in Compose codelab: https://developer.android.com/codelabs/jetpack-compose-layouts

Basic layouts in Compose codealong: https://www.youtube.com/watch?v=kyH01Lg4G1E

This blog post is part of a series:

Episode 1: Fundamentals of Compose layouts and modifiers
Episode 2: Compose phases
Episode 3: Constraints and modifier order
Episode 4: Advanced Layout concepts

--

--

Simona Milanović
Android Developers

Android Developer Relations Engineer @Google, working on Jetpack Compose