Chapters

Hide chapters

Jetpack Compose by Tutorials

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

9. Using ConstraintSets in Composables
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 section, you’ll start making a new app called JetReddit using the advanced features of Jetpack Compose. JetReddit is a composable version of the Reddit app in raywenderlich.com style. :]

First, you’ll learn how ConstraintLayout works in Jetpack Compose and what you can do with it. Then you’ll implement some of the core layouts in the app using constraint sets. Let’s get on it!

To follow this chapter, you need to know how ConstraintLayout works.

ConstraintLayout is, as its name says, a layout. This means that you use it to contain elements called children and position them appropriately. As opposed to other layouts, like Boxes, which place elements in specific positions, ConstraintLayout arranges elements relative to one another.

If you think about it, Column and Row both do the same thing, positioning each element relative to the previous element. On the other hand, they both have the same issue, which is that they can position elements in only one direction: either one below another, vertically, or next to each other, horizontally.

Understanding ConstraintLayout

That positioning works great for the most part, but if you want to build a complex UI where you can position an element anywhere on the screen, ConstraintLayout is the way to go.

ConstraintLayout allows you to position one element relative to another from any side you choose. More specifically, you can use a constraint between two elements to determine the final position. It’s possible to make constraints from four different sides: top, bottom, left and right.

Note: It’s better to use start and end instead of left and right. This lets your elements switch sides when your users have a language that’s read from right to left, also known as RTL (right-to-left) support.

ConstraintLayout Example

To make constraints easier to understand, look at the image below:

Constraint Layout Example
Gurtwrauwm Coraut Abozbta

ConstraintLayout in Jetpack Compose

In Jetpack Compose, there’s a composable with the same name called ConstraintLayout. It offers almost the same features as the ConstraintLayout you’ve used so far.

@Composable
fun ConstraintLayout(
   modifier: Modifier = Modifier,
   optimizationLevel: Int = Optimizer.OPTIMIZATION_STANDARD,
   crossinline content: @Composable ConstraintLayoutScope.() -> Unit
)
val (passwordInput, eyeIcon) = createRefs()

Icon(
   imageVector = ImageVector.vectorResource(id = R.drawable.ic_eye),
   contentDescription = stringResource(id = R.string.eye),
   modifier = Modifier.constrainAs(eyeIcon) {
     top.linkTo(passwordInput.top)
     bottom.linkTo(passwordInput.bottom)
     end.linkTo(passwordInput.end)
   }.padding(end = 16.dp)
)

Implementing the app drawer layout

To follow along with the code examples, open this chapter’s starter project using Android Studio and select Open an existing project.

Project Hierarchy
Zvogiyr Ciufeqnct

implementation "androidx.constraintlayout:constraintlayout-compose:$current-version"
Starting Screen
Ffugcany Jqnaap

Reddit App Drawer
Nizker Ehw Bducot

@Composable
fun AppDrawer(
  modifier: Modifier = Modifier,
  closeDrawerAction: () -> Unit
) {
  Column(
    modifier = modifier
      .fillMaxSize()
      .background(color = MaterialTheme.colors.surface)
  ) {
    AppDrawerHeader()

    AppDrawerBody(closeDrawerAction)

    AppDrawerFooter(modifier)
  }
}

Creating the app drawer header

Examining the header section of the Reddit screenshot shows that you can break it down into smaller parts. First, you’ll need to add a profile icon with the user name below it. Then you’ll need to add some extra user profile information like the user’s karma and Reddit age. Finally, there’s a divider that separates the header from the body.

Implementing the user icon and name

You’ll implement the user icon and user name first. You’ll add them in a Column, because they need to be ordered vertically. Add the Column and the Image first:

@Composable
private fun AppDrawerHeader() {
  Column(
     modifier = Modifier.fillMaxWidth(),
     horizontalAlignment = Alignment.CenterHorizontally
  ) {
    Image(
       imageVector = Icons.Filled.AccountCircle,
       colorFilter = ColorFilter.tint(Color.LightGray),
       modifier = Modifier
           .padding(16.dp)
           .size(50.dp),
       contentScale = ContentScale.Fit,
       alignment = Alignment.Center,
       contentDescription = stringResource(id = R.string.account)
    )
  }
}
@Composable
private fun AppDrawerHeader() {
  Column(
     modifier = Modifier.fillMaxWidth(),
     horizontalAlignment = Alignment.CenterHorizontally
  ) {
	...
    Text(
      text = stringResource(R.string.default_username),
      color = MaterialTheme.colors.primaryVariant
    )
  } // end of Column
  
  Divider(
    color = MaterialTheme.colors.onSurface.copy(alpha = .2f),
    modifier = Modifier.padding(
      start = 16.dp,
      end = 16.dp,
      top = 16.dp
    )
  )
}
App Drawer Header Without Profile Info
Ihk Czudow Yaopon Zugfael Stafoho Unxe

Adding the profile info

To get a better understanding of what you need to implement, look at the following image:

Profile Info
Gbiyewu Uvsa

Extracting reusable components

Because these components require relative constraints, you’ll use a ConstraintLayout. Add the following code to ProfileInfoItem():

@Composable
private fun ProfileInfoItem(
...
) {
  val colors = MaterialTheme.colors

  ConstraintLayout(modifier = modifier) {
    val (iconRef, amountRef, titleRef) = createRefs() // references
    val itemModifier = Modifier

    Icon(
      contentDescription = stringResource(id = textResourceId),
      imageVector = iconAsset,
      tint = Color.Blue,
      modifier = itemModifier
        .constrainAs(iconRef) {
          centerVerticallyTo(parent)
          start.linkTo(parent.start)
        }.padding(start = 16.dp)
    )
  }
}
@Composable
private fun ProfileInfoItem(
...
) {
  val colors = MaterialTheme.colors

  ConstraintLayout(modifier = modifier) {
	...
    Text(
      text = stringResource(amountResourceId),
      color = colors.primaryVariant,
      fontSize = 10.sp,
      modifier = itemModifier
        .padding(start = 8.dp)
        .constrainAs(amountRef) {
          top.linkTo(iconRef.top)
          start.linkTo(iconRef.end)
          bottom.linkTo(titleRef.top)
        }
    )
  }
}
@Composable
private fun ProfileInfoItem(
...
) {
  val colors = MaterialTheme.colors

  ConstraintLayout(modifier = modifier) {
	...
    Text(
      text = stringResource(textResourceId),
      color = Color.Gray,
      fontSize = 10.sp,
      modifier = itemModifier
        .padding(start = 8.dp)
        .constrainAs(titleRef) {
          top.linkTo(amountRef.bottom)
          start.linkTo(iconRef.end)
          bottom.linkTo(iconRef.bottom)
        }
    )
  }
}
Profile Info Item Preview
Zdopeso Itfo Unec Bwopoek

Completing ProfileInfo

Now, you’ll use your freshly made composable to complete ProfileInfo(). Replace the code of ProfileInfo() with the following implementation:

@Composable
fun ProfileInfo(modifier: Modifier = Modifier) {
  ConstraintLayout(
      modifier = modifier
          .fillMaxWidth()
          .padding(top = 16.dp)
  ) {
    val (karmaItem, divider, ageItem) = createRefs()
    val colors = MaterialTheme.colors

    ProfileInfoItem(
        Icons.Filled.Star,
        R.string.default_karma_amount,
        R.string.karma,
        modifier = modifier.constrainAs(karmaItem) {
          centerVerticallyTo(parent)
          start.linkTo(parent.start)
        }
    )

    Divider(
        modifier = modifier
            .width(1.dp)
            .constrainAs(divider) {
              centerVerticallyTo(karmaItem)
              centerHorizontallyTo(parent)
              height = Dimension.fillToConstraints
            },
        color = colors.onSurface.copy(alpha = .2f)
    )

    ProfileInfoItem(
        Icons.Filled.ShoppingCart,
        R.string.default_reddit_age_amount,
        R.string.reddit_age,
        modifier = modifier.constrainAs(ageItem) {
          start.linkTo(divider.end)
          centerVerticallyTo(parent)
        }
    )
  }
}
@Composable
private fun AppDrawerHeader() {
  Column(
     modifier = Modifier.fillMaxWidth(),
     horizontalAlignment = Alignment.CenterHorizontally
  ) {
    Image(
       ...
    )

    Text(
       ...
    )
    ProfileInfo() // Add this
  }
 
  Divider(
      ...
  )
}
App Drawer Header With Profile Info
Alk Gramej Gietac Gegv Yledipi Eklo

Implementing the app drawer’s body

The body of the app drawer is probably the easiest part to implement, since you don’t need to use a ConstraintLayout.

@Composable
private fun AppDrawerBody(closeDrawerAction: () -> Unit) {
  Column {
    ScreenNavigationButton(
        icon = Icons.Filled.AccountBox,
        label = stringResource(R.string.my_profile),
        onClickAction = {
          closeDrawerAction()
        }
    )

    ScreenNavigationButton(
        icon = Icons.Filled.Home,
        label = stringResource(R.string.saved),
        onClickAction = {
          closeDrawerAction()
        }
    )
  }
}
fun AppDrawer(closeDrawerAction: () -> Unit, modifier: Modifier = Modifier) {
   ...
   AppDrawerBody(closeDrawerAction)
   ...
}
App Drawer Body
Elm Npeyaq Fadv

Implementing the app drawer footer

Once again, check the Reddit screenshot, but this time, pay closer attention to the bottom of the screen. For this section, you need to add two new buttons, one for settings and another to change the theme.

@Composable
private fun AppDrawerFooter(modifier: Modifier = Modifier) {
  ConstraintLayout(
    modifier = modifier
      .fillMaxSize()
      .padding(
        start = 16.dp,
        bottom = 16.dp,
        end = 16.dp
      )
  ) {

    val colors = MaterialTheme.colors
    val (settingsImage, settingsText, darkModeButton) = createRefs()
  }
}
@Composable
private fun AppDrawerFooter(modifier: Modifier = Modifier) {
  ConstraintLayout(
	...
  ) {
	...
    Icon(
      modifier = modifier.constrainAs(settingsImage) {
        start.linkTo(parent.start)
        bottom.linkTo(parent.bottom)
      },
      imageVector = Icons.Default.Settings,
      contentDescription = stringResource(id = R.string.settings),
      tint = colors.primaryVariant
    )

    Text(
      fontSize = 10.sp,
      text = stringResource(R.string.settings),
      style = MaterialTheme.typography.body2,
      color = colors.primaryVariant,
      modifier = modifier
        .padding(start = 16.dp)
        .constrainAs(settingsText) {
          start.linkTo(settingsImage.end)
          centerVerticallyTo(settingsImage)
        }
    )
  }
}
@Composable
private fun AppDrawerFooter(modifier: Modifier = Modifier) {
  ConstraintLayout(
	...
  ) {
	...
    Icon(
      imageVector = ImageVector.vectorResource(id = R.drawable.ic_moon),
      contentDescription = stringResource(id = R.string.change_theme),
      modifier = modifier
        .clickable(onClick = { changeTheme() })
        .constrainAs(darkModeButton) {
          end.linkTo(parent.end)
          bottom.linkTo(settingsImage.bottom)
        },
      tint = colors.primaryVariant
    )
  }
}
App Drawer Footer
Osq Phifux Wievix

Advanced features of ConstraintLayout

ConstraintLayout makes building UI much easier than before. However, there are still some cases that are almost impossible to solve without introducing unnecessary complexity.

Guidelines

A guideline is an invisible object you use as a helper tool when you work with ConstraintLayout. You can create a guideline from any side of the screen and use one of two different ways to give it an offset:

createGuidelineFromStart(0.5f)
createGuidelineFromEnd(0.5f)
val verticalGuideline = createGuidelineFromStart(0.5f)
Icon(
  imageVector = iconAsset,
  contentDescription = stringResource(id = R.string.some_string),
  modifier = Modifier
    .constrainAs(iconReference) {
       start.linkTo(verticalGuideline)
       top.linkTo(parent.top)
       bottom.linkTo(parent.bottom)
    }
)

Barriers

Now that you know how to position objects at specific places on the screen, it’s time to think about some other problems you can solve.

Barrier
Wacjuos

ConstraintLayout(modifier = Modifier.fillMaxSize()) {
  val (button, firstName, lastName) = createRefs()
  val startBarrier = createStartBarrier(firstName, lastName)

  Text(
    text = "long first name",
    modifier = Modifier.constrainAs(firstName) {
      end.linkTo(parent.end)
      top.linkTo(parent.top)
    }
  )

  Text(
    text = "last name",
    modifier = Modifier.constrainAs(lastName) {
      end.linkTo(parent.end)
      top.linkTo(firstName.bottom)
    }
  )
  
  Button(
    content = {},
    onClick = {},
    modifier = Modifier.constrainAs(button) {
      end.linkTo(startBarrier)
    }
  )
}

Chains

The final problem that you might face when using ConstraintLayout is when you have multiple elements that are constrained to each other. Here are the possible scenarios:

Chains
Vfoinn

val (firstElement, secondElement, thirdElement) = createRefs()

Button(
  modifier = Modifier
  .constrainAs(firstElement) {
    start.linkTo(parent.start)
    end.linkTo(secondElement.start)
    top.linkTo(parent.top)
    bottom.linkTo(parent.bottom)
  }
)

Button(
  modifier = Modifier
  .constrainAs(secondElement) {
    start.linkTo(firstElement.end)
    end.linkTo(thirdElement.start)
    top.linkTo(parent.top)
    bottom.linkTo(parent.bottom)
  }
)

Button(
  modifier = Modifier
  .constrainAs(thirdElement) {
    start.linkTo(secondElement.end)
    end.linkTo(parent.end)
    top.linkTo(parent.top)
    bottom.linkTo(parent.bottom)
  }
)

createHorizontalChain(
    firstElement,
    secondElement,
    thirdElement, 
    chainStyle = ChainStyle.SpreadInside
)

Key points

  • ConstraintLayout positions its children relative to each other.
  • Add implementation androidx.constraintlayout:constraintlayout-compose:$current-version in your module level build.gradle file to use ConstraintLayout.
  • To use ConstraintLayout modifiers in your referenced composables, pass ConstraintLayoutScope as a parameter.
  • It’s better to use start and end constraints, rather than left and right.
  • Use createRefs() to create constraint references for your composables.
  • Use a guideline if you need to position your composable relative to a specific place on the screen.
  • Set a guideline by passing a specific dp amount or a fraction of the screen size.
  • Use a barrier when you need to constraint multiple composables from the same side.
  • Use a chain when you need multiple elements constrained to each other.
  • Use ChainStyle to specify the kind of chain to use.
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