Chapters

Hide chapters

Jetpack Compose by Tutorials

Second Edition · Android 13 · Kotlin 1.7 · Android Studio Dolphin

Section VI: Appendices

Section 6: 1 chapter
Show chapters Hide chapters

9. Using ConstraintSets in Composables
Written by Prateek Prasad

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 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 Kodeco 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. Compared to other layouts, like Box, Row or Column, which place their elements in a specific order, ConstraintLayout arranges elements relative to one another.

Understanding ConstraintLayout

While the layout or container composables you have worked with so far are simple to understand, they have inherent limitations when positioning children. They are constrained(pun intended) by their directionality. They can only position elements in the x, y, or z coordinate at a time. So often, while implementing complex designs, you end up with a very nested hierarchy of these containers in your code.

ConstraintLayout eliminates this restrictions by letting you position elements relative to one another. You can use a constraint between two elements to determine the final position. It’s possible to make constraints from four sides: top, bottom, left and right.

Note: It’s considered best practice 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 like arabic.

ConstraintLayout Example

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

Constraint Layout Example
Nafncpuiwg Sodeaq Ovinnqo

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
Vqudakq Naocithls

implementation "androidx.constraintlayout:constraintlayout-compose:$current-version"
Starting Screen
Lluzpafn Tgveov

Reddit App Drawer
Keytem Ojl Kfibiq

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

    AppDrawerBody(onScreenSelected)

    AppDrawerFooter(modifier)
  }
}

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 following code to the AppDrawerHeader method stub:

@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
  ) {
	... Image Composable here ...
    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
Imy Wkaput Jievus Kiwpioc Mrakidi Uvga

Adding the Profile Info

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

Profile Info
Traseqa Urpi

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
Gmozofo Upmu Uhew Kbosuij

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

  Divider(
      ...
  )
}
App Drawer Header With Profile Info
Ajv Vgitiy Vaicof Hidh Dzipavi Ikme

Implementing the App Drawer’s Body

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

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

    ScreenNavigationButton(
      icon = Icons.Filled.Home,
      label = stringResource(R.string.saved),
      onClickAction = {
        onScreenSelected.invoke(Screen.Subscriptions)
      }
    )
  }
}
fun AppDrawer(
  onScreenSelected: (Screen) -> Unit,
  modifier: Modifier = Modifier) {
   ...
   AppDrawerBody(onScreenSelected)
   ...
}
App Drawer Body
Odb Wgadez Kunc

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
Egg Dfabol Qoufed

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
Xigsuic

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
Jyeigx

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 to facilitate with internationalization.
  • 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 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