Chapters

Hide chapters

Jetpack Compose by Tutorials

First Edition · Android 11 · Kotlin 1.4 · Android Studio Canary

Before You Begin

Section 0: 3 chapters
Show chapters Hide chapters

3. Building Layout Groups in Compose
Written by Tino Balint

In this chapter, you’ll learn about layouts in Jetpack Compose. Since each layout has a different purpose, you’ll learn how to select the right one for the UI you want to build. Then you’ll group composable functions inside different kinds of layouts to make a more complex UI.

In the previous chapter, you focused on displaying the elements onscreen; this time, you’ll focus on positioning those elements.

As always, it’s best to start with the basics. Read on to discover what the Jetpack Compose replacements for the basic layouts in Android are.

Using basic layouts in Jetpack Compose

In the previous chapter, you learned how to write basic composable functions. The next step is to build a more complex UI by positioning those elements in a specific way—arranging them.

When working with XML, you achieve that by using a layout, a class that extends ViewGroup. ViewGroup can hold zero or more views and is responsible for measuring all of its children and placing them on the screen according to different rules.

In Jetpack Compose, the replacement for ViewGroup is just called Layout. Look at the source code to understand how Layout() works:

@Composable inline fun Layout(
    content: @Composable () -> Unit,
    measureBlocks: LayoutNode.MeasureBlocks,
    modifier: Modifier = Modifier
)

There are two important parameters here:

  1. content: A composable function that holds children of the Layout.
  2. measureBlocks: Responsible for measuring and positioning the children.

Measuring and positioning the elements is a complex job. That’s why Jetpack Compose offers predifined layout types that handle this for you.

Every implementation of these predefined layouts has its own logic for positioning the children. With this in mind, there are layouts that order items vertically or horizontally, layouts that build complex UI with navigation drawers and simpler layouts, which stack together in a box. All of those layouts use measureBlocks() to position items in different ways, so you don’t have to do it yourself!

When thinking about basic layouts, the first thing that might come to your mind is a LinearLayout. Your next step is to learn about LinearLayout’s composable counterpart.

Linear layouts

To follow the code in this chapter, make sure to open this chapter’s starter project, within the chapter materials.

A LinearLayout characteristically positions its children in a linear flow. This flow is called an orientation and can be horizontal or vertical. In Jetpack Compose, there are two different composable functions that replace LinearLayout, one for each orientation. You’ll start with the horizontal version—a Row.

Using Rows

Open RowScreen.kt and look inside. You’ll see an empty composable function, MyRow(), where you’ll write your code. You’ll add a Row, a LinearLayout counterpart, when it comes to horizontal layouts.

Start by replacing MyRow() with the following code:

@Composable
fun MyRow() {
  Row(verticalAlignment = Alignment.CenterVertically,
    horizontalArrangement = Arrangement.SpaceEvenly,
    modifier = Modifier.fillMaxSize()) {

    THREE_ELEMENT_LIST.forEach { textResId ->
      Text(
        text = stringResource(id = textResId),
        fontSize = 18.sp
      )
    }
  }
}

Here, you added a Row() with several different parameters.

You used Alignment.CenterVertically to center the children vertically, Arrangement.SpaceEvenly for each child to have an equal amount of space and Modifier.fillMaxSize() to make the layout fill the entire screen.

That final step is important because otherwise, a Row would take up only the space it needs to draw its children, since none of its children have weight defined. Without filling the screen size, the arrangement and alignment wouldn’t matter and all items would be placed at the top-left of the screen, one after another. You’ll learn more about weights in a moment.

Inside the Row(), you placed three Texts using a predefined list that holds string resources. You also increased their font size for readability.

Build and run, then tap on the Row button from the navigation menu and take a look at the screen:

Row
Row

You can see three text fields that are centered vertically and arranged so there’s equal spacing on all sides.

Note: This is a good time to experiment with different arrangements and observe their result.

Now that you know how to position elements horizontally inside a row, it’s time to explore the Row() signature, to see what else you can do.

Exploring Rows

Open the Row() signature, to look at what you can do with it:

@Composable
@OptIn(ExperimentalLayoutNodeApi::class, InternalLayoutApi::class)
inline fun Row(
    modifier: Modifier = Modifier,
    horizontalArrangement: Arrangement.Horizontal = Arrangement.Start,
    verticalGravity: Alignment.Vertical = Alignment.Top,
    content: @Composable RowScope.() -> Unit
)

As you see, there are two new parameters for you to work with: horizontalArrangement and verticalAlignment.

You use arrangements to position children relative to one another. The possible horizontal arrangements are:

  • SpaceBetween: The Row places each child with an equal amount of space, without calculating in spacing before the first child, or after the last child.
  • SpaceEvenly: Similar to SpaceBetween, the Row places the children with an equal amount of space, but this time it includes starting or ending spacing.
  • SpaceAround: The Row places children just like with SpaceEvenly, but reduces the space between consecutive children by half.
  • Center, Start, End: The Row places children at the center, start or end without space between them.

Using Alignment you position the children in a specific way within the parent. Specifically, verticalAlignment aligns the children vertically in three different ways:

  • Top: Aligns the children to the top of the parent.
  • CenterVertically: Aligns the children in the center of the parent, vertically.
  • Bottom: Aligns the children to the bottom of the parent.

The final way to position children inside a Row is by using weights. To add weights, you need to use a special way to access the weight() modifier from Compose. In the above example with three Text elements, you could use it like so:

@Composable
fun MyRow() {
 Row(...) {
   Text(
     modifier = with(RowScope) { Modifier.weight(1 / 3f) }, // here
     ...
   )
 }   
}

By using with() you wrap the RowScope receiver, to enable you to use the weight() modifier. This seems weird, but weight() is hidden within the RowScope and some other Scope types, so it’s a way to ensure safety when it comes to building your UI. Within weight() you define how big of a fraction of the parent the child will take up. In this case, you gave each child a third of the parent! :]

If a child doesn’t have a weight, the Row will calculate its width using the preferred width first, e.g. using the size() modifier. It will then calculate the sizes of the children with weights, proportionally to their weight, based on the remaining available space. This means that if there is an element taking up 200dp in width, and you use weights, the weighted children will take up the screen width, minus the 200dp that’s already taken. If none of the children have weight, the Row will be as small as possible to fit all its children without spacing.

Rows represent horizontal arrangement of items, and such is its name. Following this logic, a vertical arragnement of items is called a Column. Let’s see how to use it!

Using Columns

The Compose counterpart for a vertically-oriented LinearLayout is a Column.

Open ColumnScreen.kt and you’ll see a similar situation as before—an empty MyColumn(), which you’ll implement.

Fill in the function, so it looks like this:

@Composable
fun MyColumn() {
  Column(
    horizontalAlignment = Alignment.CenterHorizontally,
    verticalArrangement = Arrangement.SpaceEvenly,
    modifier = Modifier.fillMaxSize()
  ) {

    THREE_ELEMENT_LIST.forEach { textResId ->
      Text(
        text = stringResource(id = textResId),
        fontSize = 22.sp
      )
    }
  }
}

The Column implementation is the same as the Row, except you swapped the arrangements and alignments from vertical to horizontal. This is because the items are already placed vertically in a Column, and you need to define how they behave horizontally and how to space them vertically. With Rows, the situation is the opposite—items are placed horizontally, and the Row needs to know how they behave vertically and how to space them horizontally.

Build and run and select the Column button from the navigation menu.

Column
Column

This time, you see that the items are arranged vertically instead of horizontally, with the same spacing.

Now, you’ve seen how Column and Row are similar to LinearLayouts. They’re more powerful, however, because you can arrange the children in several different ways—which the LinearLayout doesn’t allow.

Exploring Columns

Now that you’ve learned how to use Columns, check how they differ from a Row, by opening the Column() signature:

@Composable
@OptIn(ExperimentalLayoutNodeApi::class, InternalLayoutApi::class)
inline fun Column(
    modifier: Modifier = Modifier,
    verticalArrangement: Arrangement.Vertical = Arrangement.Top,
    horizontalGravity: Alignment.Horizontal = Alignment.Start,
    content: @Composable ColumnScope.() -> Unit
)

As you learned before, the parameters are almost the same, but take a closer look and you’ll see that the layout swaps the arrangements and alignments. This means you can do all the same things inside the Column as in a Row, but with different orientations.

Next, you’ll learn about a composable counterpart for a FrameLayout, called a Box.

Using Boxes

The composable counterpart for a FrameLayout is called a Box. Just like FrameLayout, it’s used to display children relative to their parent’s edges, and allows you to stack children. This is useful when you have elements that need to be displayed in those specific places or when you want to display elements that overlap.

Now, open BoxScreen.kt and you’ll find the usual empty function,MyBox(). Add the following code to complete it:

@Composable
fun MyBox(
  modifier: Modifier = Modifier,
  contentModifier: Modifier = Modifier
) {
  Box(modifier = modifier.fillMaxSize()) {
    Text(
      text = stringResource(id = R.string.first),
      fontSize = 22.sp,
      modifier = contentModifier.align(Alignment.TopStart)
    )

    Text(
      text = stringResource(id = R.string.second),
      fontSize = 22.sp,
      modifier = contentModifier.align(Alignment.Center)
    )
    Text(
      text = stringResource(id = R.string.third),
      fontSize = 22.sp,
      modifier = contentModifier.align(Alignment.BottomEnd)
    )
  }
}

This time, the function has two parameters—a modifier and a contentModifier, with default arguments of Modifier, the empty modifier implementation. This way, you can pass in custom modifiers that will change how the parent Box or each piece of content behaves. After which each element can chain more modifier function calls, to apply additional customization.

This is a good practice, as you can pass in a custom modifier that applies padding or styling to the parent modifier, and then reuse custom styles throughout your app, while the end component adds a bit more customization, based on the component. You can do the same for content based modifiers.

Here, the Box() has three text fields, as in previous examples, and uses thealign modifier to position those text fields in three different places.

Build and run, then select the Box option from the navigation menu to see the result:

Box
Box

The text fields appear diagonally across the screen, with the first one at the top-left corner, the second one in the center and the last one at the bottom-right corner.

Using a Box is really useful in specific situations, and they make positioning elements incredibly easy.

Exploring Boxes

When you have multiple children inside a Box, they’re rendered in the same order as you placed them inside the Box. Here’s the implementation:

@Composable
fun Box(
    modifier: Modifier = Modifier,
    contentAlignment: Alignment = Alignment.TopStart,
    content: @Composable BoxScope.() -> Unit
)

ContentAlignment allows you to set the default Alignment to its children. If you want to have different Alignments between each child, you need to set Alignment by using a modifier on a child.

You can set the Alignment to any edge of the screen as well as in relation to the center, using any of the following types of alignment:

  • TopStart
  • TopCenter
  • TopEnd
  • CenterStart
  • Center
  • CenterEnd
  • BottomStart
  • BottomCenter
  • BottomEnd

Where each of the alignments refers to which part of the screen the Box will attach an item to.

Next, you’ll learn about one of the first layouts introduced in Jetpack Compose: the Surface.

Using Surfaces

Surface is a new layout that serves as a central metaphor in Material Design. What’s unique about Surface is it can only hold one child at a time, but it provides many styling options for the content of its children, the elevation, border and much more.

It’s time to see the Surface in action. Open SurfaceScreen.kt and look at the contents:

@Composable
fun SurfaceScreen(modifier: Modifier = Modifier) {

  Box(modifier = modifier.fillMaxSize()) {
    MySurface(modifier = modifier.align(Alignment.Center))
  }

  BackButtonHandler {
    JetFundamentalsRouter.navigateTo(Screen.Navigation)
  }
}

@Composable
fun MySurface(modifier: Modifier) {
  //TODO write your code here
}

To show all that the Surface can do, the example is set inside a full-screen Box() and an Alignment.Center. All that’s left is to implement the empty MySurface(). To do this, add the following code to finish it:

@Composable
fun MySurface(modifier: Modifier) {
  Surface(
      modifier = modifier.size(100.dp), // 1
      color = Color.LightGray, // 2
      contentColor = colorResource(id = R.color.colorPrimary), // 2
      elevation = 1.dp, // 3
      border = BorderStroke(1.dp, Color.Black) // 4
  ) {
    MyColumn() // 5
  }
}

There are many small steps in this code, so go over them one by one:

  1. You first set the size of the surface to 100dp in both height and width using Modifier.size().
  2. Then you set the color of the surface to Color.LightGray and the color of its content to colorPrimary. The surface will be gray, and Surface will set the contentColor to all the elements it applies to—such as Text elements.
  3. You add an elevation of 1dp to raise the Surface above other elements.
  4. You also add a black border to outline the Surface.
  5. Finally, you set the child to the Surface to be the MyColumn() you defined earlier.

This is a perfect example of the power of Jetpack Compose. You can reuse each of the screens and composable functions you implemented before. This time, you reused the MyColumn(), with three vertical Text elements.

Build and run and select Surface from the navigation menu.

Surface
Surface

At the center of the screen is the Surface in a light gray color with a black border. Within it is the previously-implemented custom Column.

Previously, all the Text elements used the default, black color, but using contentColor you changed the text color of Column’s children to use the green color of raywenderlich.com.

If you’re reading the grayscale version of the book, you might not notice the color change as easily, so make sure to build and run the app and preview the changes directly on your phone!

Now let’s see what else the Surface() allows you to do.

Exploring Surfaces

To see what else a Surface() has to offer, open its signature:

@Composable
fun Surface(
    modifier: Modifier = Modifier,
    shape: Shape = RectangleShape,
    color: Color = MaterialTheme.colors.surface,
    contentColor: Color = contentColorFor(color),
    border: BorderStroke? = null,
    elevation: Dp = 0.dp,
    content: @Composable () -> Unit
) 

These parameters define Surface’s purpose. There are five purposes in total:

  • Shape: Clips the children with the defined shape.
  • Color: Fills the shape with a color you define.
  • Border: Draws borders, if they’re set.
  • Elevation: Sets the elevation and draws an appropriate shadow.
  • Content: Sets the default color for its content with the defined contentColor.

The most common way to use a Surface is as the root layout of your components. Since it can hold only one child, that child is usually another layout that positions the rest of the elements. The Surface() doesn’t handle positioning—its child does.

Note: There’s a popular custom Surface implementation called Card. A Card has exactly the same five purposes and can only hold one child. The only difference between the Card and a Surface are its default parameters. A Card has a predefined elevation and uses a material theme shape with rounded corners.

Now that you’ve learned all the basic layouts available in Jetpack Compose, your next step is to learn about a more advanced layout that lets you create a fully-functional UI. That element is called a Scaffold.

Scaffold

The Scaffold is a new layout that Jetpack Compose introduced. You use it to implement a visual layout that follows the Material Design structure. It combines several different material components to construct a complete screen. Because the Scaffold() offers multiple ways to build your UI, it’s best to jump into the code, and play around with it!

Using Scaffold

Open ScaffoldScreen.kt and look inside. You’ll see three empty composable functions:

@Composable
fun MyScaffold() {
  //todo write your code here
}

@Composable
fun MyTopAppBar(scaffoldState: ScaffoldState) {
  //todo write your code here
}

@Composable
fun MyBottomAppBar() {
  //todo write your code here	
}

You’ll use these empty functions to implement your own Scaffold and to add top and bottom app bars. Start by entering the following code inside MyScaffold(). It should look like so:

@Composable
fun MyScaffold() {
  val scaffoldState: ScaffoldState = rememberScaffoldState()

  Scaffold(
      scaffoldState = scaffoldState,
      contentColor = colorResource(id = R.color.colorPrimary),
      bodyContent = { MyRow() },
      topBar = { MyTopAppBar(scaffoldState = scaffoldState) },
      bottomBar = { MyBottomAppBar() },
      drawerContent = { MyColumn() }
  )
}

First, you create the scaffold state by calling rememberScaffoldState(), then you assign it to the Scaffold(). You set contentColor to the primary color of the app and bodyContent to MyRow(), which you previously implemented.

Next, you set the top and bottom bar content to the composables you haven’t implemented yet.

Finally, you set MyColumn() as your drawer. There’s a lot going on here, but you’ll see how it connects together in a moment.

Build and run, then click the Scaffold option from the navigation menu.

Scaffold
Scaffold

You can see the MyRow() with the three Text elements on the screen, but the top and bottom app bars aren’t showing. That’s not surprising since you haven’t implemented them yet. :]

Additionally, you can open the navigation drawer by swiping from the left to the right side of the screen, to show the MyColumn from before.

Now let’s finish the screen by implementing the top and bottom bars.

Completing the screen

To complete the screen, implement the two remaining composables. Add the following code to complete MyTopAppBar():

@Composable
fun MyTopAppBar(scaffoldState: ScaffoldState) {
  TopAppBar(
    navigationIcon = {
      IconButton(
        content = {
          Icon(
            Icons.Default.Menu,
            tint = Color.White
          )
        },
        onClick = { scaffoldState.drawerState.open() }
      )
    },
    title = {
      Text(
        text = stringResource(id = R.string.app_name),
        color = Color.White
      )
    },
    backgroundColor = colorResource(id = R.color.colorPrimary)
  )
}

First, you add an existing predefined implementation of the TopAppBar. You add an IconButton with the Menu icon and White content color as the navigationIcon. For the click action of the IconButton, you initiate opening the drawer by changing the drawerState inside the scaffoldState. This will open the drawer whenever you click the menu icon. The navigationIcon is a predefined parameter you can use to define the first element in the TopAppBar, which usually represents a Home or Back button.

Then you define the title, to represent a simple Text element that shows the app name. Showing a title in the top bar is common behavior for most Android apps.

The TopAppbar is a pretty simple component that lets you define an elevation, a backgroundColor, title and navigationIcon separately, or combined in a single content function, and special actions that define another composable function for menu actions. Play around with other parameters to see how they style the TopAppBar.

Now, implement the MyBottomAppBar():

@Composable
fun MyBottomAppBar() {
  BottomAppBar(
  	content = {},
  	backgroundColor = colorResource(id = R.color.colorPrimary))
}

For MyBottomAppBar(), you add an existing implementation of the bottom app bar with empty content. You also define colorPrimary as the background color.

Build and run again and check the changes to the screen:

Scaffold With App Bars
Scaffold With App Bars

The screen now shows top and bottom bars as well. The top bar contains the Menu icon, which opens the drawer when the user clicks it.

Drawer
Drawer

The drawer shows the three text fields from MyColumn(). To dismiss the drawer, just click anywhere on the screen, or swipe it left.

You’ve now learned how to group your composables inside layouts, to position them and to give them common properties. You can achieve this by using either multiple basic layouts or one of the advanced layouts.

Aside from Scaffold, advanced layouts also include a ConstraintLayout. ConstraintLayout lets you make constraints between the elements, just like the version you find in XML.

You’ll dive deep and learn more about ConstraintLayout in Chapter 9, “Using ConstraintSets in Composables”. But before all that, let’s expore the Scaffold() signature.

Exploring Scaffold

To learn more about all the parameters the Scaffold() lets you use, open its signature:

@Composable fun Scaffold(
    modifier: Modifier = Modifier, 
    scaffoldState: ScaffoldState = rememberScaffoldState(), 
    topBar: () -> Unit = emptyContent(), 
    bottomBar: () -> Unit = emptyContent(), 
    snackbarHost: (SnackbarHostState) -> Unit = { SnackbarHost(it) }, 
    floatingActionButton: () -> Unit = emptyContent(), 
    floatingActionButtonPosition: FabPosition = FabPosition.End, 
    isFloatingActionButtonDocked: Boolean = false, 
    drawerContent: ColumnScope.() -> Unit = null, 
    drawerGesturesEnabled: Boolean = true, 
    drawerShape: Shape = MaterialTheme.shapes.large, 
    drawerElevation: Dp = DrawerDefaults.Elevation, 
    drawerBackgroundColor: Color = MaterialTheme.colors.surface, 
    drawerContentColor: Color = contentColorFor(drawerBackgroundColor), 
    drawerScrimColor: Color = DrawerConstants.defaultScrimColor, 
    backgroundColor: Color = MaterialTheme.colors.background, 
    contentColor: Color = contentColorFor(backgroundColor), 
    bodyContent: (PaddingValues) -> Unit
): Unit

You can see that it has lots of features and components. This breakdown will give you a clear picture of what each does, and how you used them:

  • scaffoldState: The state of the layout. Unlike the basic layouts, Scaffold requires custom handling of its state. This is important because it can hold several different components that can change its visibility or content. A common example of handling state is showing whether a drawer displays or not.
  • topBar: A composable that renders the top app bar. While you could create a custom composable, Jetpack Compose offers you a predefined composable to save you the effort.
  • bottomBar: This composable renders the bottom app bar. As with the previous parameter, you can choose whether to use a custom composable or the predefined BottomAppBar.
  • snackbarHost: As the name implies, this component hosts a SnackBar. It handles the state when snackers should be shown.
  • floatingActionButton: Lets you set a composable for the main button on the screen. A FloatingActionButton is recommended.
  • drawerContent: Use this composable for drawers that require a custom implementation.
  • bodyContent: A composable shown inside Scaffold. This is where you put the main content of the screen.

There are also many other parameters which are less important, but you can explore them if you want to play around with your Scaffold.

Now you can move onto building more components in Compose and complex UI, with the knowledge you gained in the first few chapters! :]

Key points

  • Use Layouts to position your elements or give them shared properties.
  • Row lets you position elements horizontally on the screen.
  • Column lets you position elements vertically on the screen.
  • Use vertical or horizontal Arrangement to change the position of elements inside the Row or Column.
  • Use weights to change the proportion of the screen your elements will use.
  • Box allows you to position the elements in the corners of the screen or stack them on top of each other.
  • Using with(Scope) syntax, you gain access to hidden modifiers, for RowScope, ColumnScope, BoxScope and other scope types.
  • Group multiple basic layouts to create a more complex screen.
  • Use Surface to clip the elements inside it with an option to add the border and elevation.
  • Surface can hold only one child.
  • Add another layout inside Surface to position the elements.
  • Card is a just a Surface with default parameters.
  • Scaffold lets you build the entire screen by adding different material components.
  • Use ScaffoldState to handle states for the components inside the scaffold.
  • rememberScaffoldState will remember the state and preserve it during the recomposition.

Where to go from here?

You now know how to use multiple predefined composables to implement different features. You’ve also learned how to group and position them inside layouts to make a complete screen.

Next, you’ll learn about different ways of making lists, how to make adapters and how to get the same result as when you use a RecyclerView. Finally, you’ll learn how to implement custom grids.

See you in the next chapter!

Have a technical question? Want to report a bug? You can ask questions and report bugs to the book authors in our official book forum here.
© 2026 Kodeco Inc.