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

Heads up... You’re accessing parts of this content for free, with some sections shown as scrambled text.

Heads up... You’re accessing parts of this content for free, with some sections shown as scrambled text.

Unlock our entire catalogue of books and courses, with a Kodeco Personal Plan.

Unlock now

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.

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.

@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
      )
    }
  }
}
Row
Kuh

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
)
@Composable
fun MyRow() {
 Row(...) {
   Text(
     modifier = with(RowScope) { Modifier.weight(1 / 3f) }, // here
     ...
   )
 }   
}

Using Columns

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

@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
      )
    }
  }
}
Column
Tipisw

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
)

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.

@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)
    )
  }
}
Box
Rak

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
)

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.

@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
}
@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
  }
}
Surface
Rabkono

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
) 

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	
}
@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() }
  )
}
Scaffold
Fsofridr

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)
  )
}

@Composable
fun MyBottomAppBar() {
  BottomAppBar(
  	content = {},
  	backgroundColor = colorResource(id = R.color.colorPrimary))
}
Scaffold With App Bars
Wsatmeqf Yuxg Edn Cinp

Drawer
Hjuzaz

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

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.

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.
© 2024 Kodeco Inc.

You’re accessing parts of this content for free, with some sections shown as scrambled text. Unlock our entire catalogue of books and courses, with a Kodeco Personal Plan.

Unlock now