Chapters

Hide chapters

Jetpack Compose by Tutorials

First Edition · Android 11 · Kotlin 1.4 · Android Studio Canary - Arctic Fox Release

3. Building Layout Groups in Compose
Written by Tino Balint

Heads up... You're reading this book for free, with parts of this chapter shown beyond this point as scrambled text.

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,
  modifier: Modifier = Modifier,
  measurePolicy: MeasurePolicy
)

There are two important parameters here:

  1. content: A composable function that holds children of the Layout.
  2. measurePolicy: Responsible for defining measuring and layout behavior.

Measuring and positioning the elements is a complex job. That’s why Jetpack Compose offers predefined 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 measurePolicy 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
Ruw

Exploring Rows

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

@Composable
inline fun Row(
    modifier: Modifier = Modifier,
    horizontalArrangement: Arrangement.Horizontal = Arrangement.Start,
    verticalAlignment: Alignment.Vertical = Alignment.Top,
    content: @Composable RowScope.() -> Unit
)
@Composable
fun RowScope.MyRow() { // This composable is called from inside the Row
   Text(
     modifier = 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
Guxocm

Exploring Columns

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

@Composable
inline fun Column(
  modifier: Modifier = Modifier,
  verticalArrangement: Arrangement.Vertical = Arrangement.Top,
  horizontalAlignment: 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, such as dialogs.

@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
Fen

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,
  propagateMinConstraints: Boolean = false,
  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, such as 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
Qiztedu

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()
  val scope: CoroutineScope = rememberCoroutineScope()

  Scaffold(
      scaffoldState = scaffoldState,
      contentColor = colorResource(id = R.color.colorPrimary),
      content = { MyRow() },
      topBar = { MyTopAppBar(scaffoldState = scaffoldState, scope = scope) },
      bottomBar = { MyBottomAppBar() },
      drawerContent = { MyColumn() }
  )
}
@Composable
fun MyTopAppBar(scaffoldState: ScaffoldState, scope: CoroutineScope) {}
Scaffold
Djezlevc

Completing the screen

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

@Composable
fun MyTopAppBar(scaffoldState: ScaffoldState, scope: CoroutineScope) {
  val drawerState = scaffoldState.drawerState

  TopAppBar(
    navigationIcon = {
      IconButton(
        content = {
          Icon(
            Icons.Default.Menu,
            tint = Color.White,
            contentDescription = stringResource(R.string.menu)
          )
        },
        onClick = {
          scope.launch { if (drawerState.isClosed) drawerState.open() else drawerState.close() }
        }
      )
    },
    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
Ccipkukn Kilt Uqc Higz

Drawer
Hgigeh

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: @Composable () -> Unit = {},
    bottomBar: @Composable () -> Unit = {},
    snackbarHost: @Composable (SnackbarHostState) -> Unit = { SnackbarHost(it) },
    floatingActionButton: @Composable () -> Unit = {},
    floatingActionButtonPosition: FabPosition = FabPosition.End,
    isFloatingActionButtonDocked: Boolean = false,
    drawerContent: @Composable (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 = DrawerDefaults.scrimColor,
    backgroundColor: Color = MaterialTheme.colors.background,
    contentColor: Color = contentColorFor(backgroundColor),
    content: @Composable (PaddingValues) -> Unit
)

Key points

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 reading for free, with parts of this chapter shown as scrambled text. Unlock this book, and our entire catalogue of books and videos, with a Kodeco Personal Plan.

Unlock now