Android Compose

Jetpack Compose ConstraintLayout

Learn how to work with ConstraintLayout in Compose.

Everything listed in this post is part of Jetpack Compose version 0.1.0-dev13, (Compose Developer Preview 2), so it’s quite prone to change over time while the team keeps improving the api surface.

🧠 Concern separation

When ConstraintLayout was coded for the Android View system, logics for rendering (constraints) were decoupled from the ViewGroup with the purpose of making it also work for the layout editor. That decision turned out to be pretty convenient so it could also be ported into Jetpack Compose now.

🤷🏼‍♀️ How to use it

You can find it in implementation 'androidx.ui:ui-layout:0.1.0-dev13' and import it like import androidx.ui.layout.ConstraintLayout.

Here you have a fully working example I prepared and I’ll be iterating over in other articles to come.

👀 An example

There is an awesome sample project on ConstraintLayout created by @riggaroo that showcases how to build a Google Play Movies detail like layout for the Android View System. In this post I’ll do a try to replicate it for Jetpack Compose as possible. Here is the final result:

Aladdin sample Aladdin gif

The only goal is to showcase how to use ConstraintLayout composable by yourself, so don’t expect much magic on this first post but mostly the basics.

For loading images from a remote URL seamlessly I’ll be using Coil via the Accompanist library, by @chrisbanes. This library provides some utilities for projects in the process of migration to Jetpack Compose, like a theme builder to load theme colors from the closest Context, so you can keep using your XML themes and styles while doing the migration. Pretty interesting in my opinion.

Let’s start by coding those rounded icon buttons on the image.

I decided to code those as a separate component for reusability. The component has an icon and a text inside. The name is RoundedIconButton, which gets a series of arguments: The tag, that will be used to reference composables within our layout declaration and that we’ll look into later, the image vector asset, a text to put below it and an optional background color.

@Composable
fun RoundedIconButton(
    tag: Tag,
    asset: VectorAsset,
    text: String,
    background: Color = lightThemeColors.primary
) {
    Column(
        modifier = Modifier.tag(tag) + Modifier.padding(16.dp),
        horizontalGravity = Alignment.CenterHorizontally
    ) {
        Image(
            modifier = Modifier.drawBackground(
                background,
                CircleShape
            ) + Modifier.padding(20.dp),
            asset = asset
        )
        Text(
            modifier = Modifier.padding(top = 8.dp),
            style = MaterialTheme.typography.body2,
            color = Color.Gray,
            text = text
        )
    }
}

By adding a Column I align the image and the text below it vertically, Then I impose center horizontal aligment for both things inside the Column with a modifier.

From the outside, we’ll be able to call it like:

RoundedIconButton(
    tag = FamilyCircleTag,
    asset = vectorResource(R.drawable.ic_family),
    text = "Family"
)

Where FamilyCircleTag is a simple: object FamilyCircleTag. This would be the visual result 👍

Aladdin gif

Let’s move on now into the actual ConstraintLayout code.

I will provide a new component representing my screen for this. Content will be into a VerticalScroller since I’ll want it to be scrollable in the vertical direction:

@Composable
fun GooglePlayScreen(movie: MovieViewState) {
  VerticalScroller {
    // My screen composable tree will live in here
  }

Time to to add our ConstraintLayout to the mix.

@Composable
fun GooglePlayScreen(movie: MovieViewState) {
    VerticalScroller {
	ConstraintLayout(
	  modifier = Modifier.drawBackground(color = Color.White) +
	  	Modifier.fillMaxWidth() +
	  	Modifier.fillMaxHeight(),
	  constraintSet = ConstraintSet {
	    // We'll add our initial constraints here!
	  }) {
	    // Our composables will be the composable children declared here.
	    // They will be constrained by the constraints provided above.
	  }
}

I’m imposing full width and height by modifiers, since I want this composable to take the whole screen.

Since Jetpack Compose composables don’t rely on ids for reference like in the View system, we need to create tags to refer to those within our constraints.

Let’s create a tag for our header image, and add some constraints to it:

private object HeaderImageTag

@Composable
fun GooglePlayScreen(movie: MovieViewState) {
    VerticalScroller {
	ConstraintLayout(
	  modifier = Modifier.drawBackground(color = Color.White) +
	  	Modifier.fillMaxWidth() +
	  	Modifier.fillMaxHeight(),
	  constraintSet = ConstraintSet {
	    val headerImage = tag(HeaderImageTag).apply {
	      top constrainTo parent.top
	      left constrainTo parent.left
	      right constrainTo parent.right
	    }
	  }) {
	    CoilImage(
                data = movie.headerImageUrl,
                contentScale = ContentScale.Crop,
                modifier = Modifier.fillMaxWidth() +
                  Modifier.preferredHeight(240.dp) +
                  Modifier.tag(HeaderImageTag)
            )
	  }
}

Note that we just need add the composable as a children of our ConstraintLayout, like I’m doing with my CoilImage. Remember to tag the view with the the same tag used for its constraints ⚠️, using a tag modifier.

You can see how simple is it to constrain our views:

headerImage.top constrainTo parent.top
headerImage.left constrainTo parent.left
headerImage.right constrainTo parent.right

Now let’s add our movie cover image as a portrait:

sealed class Tag
private object HeaderImageTag : Tag()
private object PortraitImageTag : Tag()

ConstraintLayout(
  modifier = Modifier.drawBackground(color = Color.White) +
    Modifier.fillMaxWidth() +
    Modifier.fillMaxHeight(),
  constraintSet = ConstraintSet {
    val headerImage = tag(HeaderImageTag).apply {
      top constrainTo parent.top
      left constrainTo parent.left
      right constrainTo parent.right
    }
    val portraitImage = tag(PortraitImageTag).apply {
      left constrainTo parent.left
      top constrainTo headerImage.bottom
      bottom constrainTo headerImage.bottom
    }
}) {
  CoilImage(
     data = movie.headerImageUrl,
     contentScale = ContentScale.Crop,
     modifier = Modifier.fillMaxWidth() +
     	  		Modifier.preferredHeight(240.dp) +
     	  		Modifier.tag(HeaderImageTag))

  CoilImage(
    data = movie.portraitUrl,
    modifier = Modifier.preferredSize(120.dp, 260.dp) +
     		Modifier.padding(8.dp) +
     		Modifier.tag(PortraitImageTag))
   }

We align its top and bottom to the bottom edge of the header image, so that way we center it vertically to that line.

Let’s add a title now:

sealed class Tag
private object HeaderImageTag : Tag()
private object PortraitImageTag : Tag()
private object TitleTag : Tag()

ConstraintLayout(
  modifier = Modifier.drawBackground(color = Color.White) +
    Modifier.fillMaxWidth() +
    Modifier.fillMaxHeight(),
  constraintSet = ConstraintSet {
    val headerImage = tag(HeaderImageTag).apply {
      top constrainTo parent.top
      left constrainTo parent.left
      right constrainTo parent.right
    }

    val portraitImage = tag(PortraitImageTag).apply {
      left constrainTo parent.left
      top constrainTo headerImage.bottom
      bottom constrainTo headerImage.bottom
    }

    val title = tag(TitleTag).apply {
      top constrainTo headerImage.bottom
      left constrainTo portraitImage.right
      right constrainTo parent.right
      width = spread
    }
}) {
  CoilImage(
     data = movie.headerImageUrl,
     contentScale = ContentScale.Crop,
     modifier = Modifier.fillMaxWidth() +
     	  		Modifier.preferredHeight(240.dp) +
     	  		Modifier.tag(HeaderImageTag))

  CoilImage(
    data = movie.portraitUrl,
    modifier = Modifier.preferredSize(120.dp, 260.dp) +
     		Modifier.padding(8.dp) +
     		Modifier.tag(PortraitImageTag))
   }

   Text(
     movie.name,
     style = MaterialTheme.typography.h4,
     modifier = Modifier.padding(top = 16.dp) + Modifier.tag(TitleTag))

Note how we align the title:

title.top constrainTo headerImage.bottom
title.left constrainTo portraitImage.right
title.right constrainTo parent.right
title.width = spread

This will make it position below the header image, and to the right of the portrait image.

The spread width has the same effect than for the View system ConstraintLayout; Since our text is smaller than the available width space, we want it to take all the available space instead, so its content (the actual text rendered within the composable) gets aligned to the left per its gravity. Here’s the visual result with the text background in Red so you can see what I’m talking about:

Aladdin sample Aladdin gif

And by following this pattern you can add the rest of the views. There are not any gotchas in there, so I’ll prompt you to have a look to the complete screen code here.

I plan on iterating a bit over this sample to add some interesting animations to the screen in future posts, so stay tunned 👍

Also keep in mind ConstraintLayout is not being promoted yet by the Jetpack Compose team, precisely because it’s prone to change, as many other apis. Keep that in mind when you use it 🙏

Aladdin sample


Remember that you’ll need Android Studio 4.2 Canary 1 to use Jetpack Compose.

You might be interested in other posts I wrote about Jetpack Compose, like:

I share thoughts and ideas on Twitter quite regularly. You can also find me on Instagram. See you there!

Stay tunned for more Jetpack Compose posts 👋