Pinterest-Inspired Android UI Development With Jetpack Compose

Weight, Align & Grid to dynamically position, stretch & fill space

DuAa AwAn
7 min readApr 28, 2024

Pinterest arranges elements in rows and columns that adjust dynamically to fill any gaps in the design. This flexible layout allows for a visually appealing and organized display of content, making it easy for users to navigate and explore.

Pinterest-style masonry grid is a common feature in web design, but examples of implementing it in Jetpack Compose are scarce. After researching the Jetpack Compose documentation, I found that there are two methods to create a grid layout in Jetpack Compose.

Either by custom aligning & arranging rows and columns or utilizing Jetpack Compose lazy staggered grid.

Let’s explore how to do it both ways.

Custom Layout — Using Box Weight

We can create a flexible and responsive layout by simply using multiple columns inside a row and arranging and aligning rows and columns. In a way that allows the columns to dynamically stretch items, and adjust to fill the available space.

This is the simplest and most efficient solution.

The only drawback is lots of container columns and rows making code a little difficult to read.

Yet it is most easy to implement.

We need a row inside which we have our box with 1f weight and max-width.

Row() {
Box(modifier = Modifier
.fillMaxWidth()
.weight(1f)
) {
// Your Image here
}
Box(modifier = Modifier
.fillMaxWidth()
.weight(1f)
) {
// Your Image here
}
}

It will result in two columns with images in a row.

Now you wish to add more images to your boxes but what happens if you put more than one element in a box they will overlap. And the second image won’t be pushed below the first image layout.

Row() {
Box(modifier = Modifier
.fillMaxWidth()
.weight(1f)
) {
// Your Image here
}
Box(modifier = Modifier
.fillMaxWidth()
.weight(1f)
) {
// First Image here
// Second Image here
}
}

As you can see the the second image came on top of the first image instead of being pushed downwards expanding the box.

So, Instead of placing images directly inside the box, we will place them inside a column.

Row() {
Box(modifier = Modifier
.fillMaxWidth()
.weight(1f)
) {
Column {
// First Image here
// Second Image here
}
}
Box(modifier = Modifier
.fillMaxWidth()
.weight(1f)
) {
Column {
// First Image here
// Second Image here
}
}
}

As you can see, our layout looks exactly like the Pinterest fluid layout by using Jetpack Compose basic composable layouts like Row, Box, Column, and the 1f weight modifier.

Using Lazy Staggered Grid Layout

To implement a masonry layout, with a lazy vertical staggered grid, we need our images as a list so it can loop through it.

And a fixed number of cells for the number of columns to show.

Once we are done setting up the images list and stating the number of columns we can move on to looping through our list data.

And display all our images one by one.

   //We can store image IDs in our list
val itemsList = listOf(R.drawable.artwork2,R.drawable.artwork3,R.drawable.artwork3,R.drawable.artwork2)

LazyVerticalStaggeredGrid(
columns = StaggeredGridCells
.Fixed(2) // number of columns
) {
items(itemsList) {item->

Image(
painterResource(id = item), modifier = Modifier
.padding(5.dp), contentDescription = ""
)
}
}

This was simple too.

We need to have a data list and the number of columns.

So, we can choose either for our specific use case.

This was a basic fluid masonry layout example now cherry on top if we wish to go further to implement the nitty grid of a looker Pinterest-like layout.

We need more than an image to make the image look more like a card. We need a Pin Card and Pin Details to be aligned below the image card.

So, let’s start with adding more details.

Adding More Details To Pin Card

To refine our layout, I will create a Pin Composable to separate our pin card logic. Below is the code for our Pin Card Design.

@Composable
fun PinCard(
name: String,
author: String,
imageId: Int,
modifier: Modifier = Modifier) {
Column(
modifier = modifier
.fillMaxWidth()
.padding(9.dp)
)
{
// our pin content here
}
}

I have the pin name, author name, and image ID as parameters for PinCard Composable.

Next, we will use a painterResource to place the image ID inside an image layout and place the image layout inside a ElevatedCard Layout to give image elevation and rounded corners.

We can change elevation by giving dp value to defaultElevation.

   val painter = painterResource(id = imageId)
ElevatedCard(
elevation = CardDefaults.cardElevation(
defaultElevation = 8.dp
),
modifier = Modifier)
{
Image(
painter,
modifier = Modifier.fillMaxWidth(),
contentDescription = ""
)
}

Our Elevated Card Image looks like the above.

Below our Elevated Card, we will have another row, with two columns, for the pin name and more options icon.

Row(modifier = Modifier.fillMaxWidth()) {
Column {
Text(
text = name,
fontSize = 12.sp,
fontWeight = FontWeight.SemiBold,
color = Color.Black,
textAlign = TextAlign.Start,
modifier = modifier
.padding(8.dp)
)
}
Column(modifier = Modifier.fillMaxWidth()) {

Icon(
imageVector = Icons.Default.MoreVert,
contentDescription = null,
modifier = Modifier
.padding(4.dp)
.size(18.dp)
.align(Alignment.End)
.graphicsLayer {
rotationZ = 90f
},
)
}

}

To align text towards the start of the column use textAlign property with value TextAlign.Start.

textAlign = TextAlign.Start

To align the Icon to the end of the column using modifier align with value Alignment.End and its parent column modifier to fillMaxWidth so it will take the remaining space of the row.

Column(modifier = Modifier.fillMaxWidth()) {
Icon().align(Alignment.End)
}

For the icon image, I used the default icon of MoreVert and rotationZ in my graphicsLayer modifier.

I have set it to 90 degree because my icon was vertical and to make it more like Pinterest pin I have to turn it horizontal.

After the name row, I will add another row below containing the user Icon Image and user name.

       Row {
Image(
painter, modifier = Modifier
.size(width = 24.dp, height = 24.dp)
.clip(RoundedCornerShape(50)),
contentDescription = "",
contentScale = ContentScale.Crop
)
Column {
Text(
text = author,
fontSize = 12.sp,
fontWeight = FontWeight.Normal,
color = Color.Black,
textAlign = TextAlign.Start,
modifier = modifier
.padding(5.dp)
.align(Alignment.CenterHorizontally)
)
}
}

The pin author’s name and image next to it.

To create a rounded image I used the clip modifier to give a rounded corner shape and content scale to crop.

 Image(modifier = Modifier
.clip(RoundedCornerShape(50)),
contentScale = ContentScale.Crop
)

For the author name text I aligned it to TextAlign.Start which brought it next to the icon image.

The complete pin card code will look something like this:

@Composable
fun PinCard(name: String, author: String, imageId: Int, modifier: Modifier = Modifier) {
val painter = painterResource(id = imageId)
Column(
modifier = modifier
.fillMaxWidth()
.padding(9.dp)
)
{
// Pin Image
ElevatedCard(
elevation = CardDefaults.cardElevation(
defaultElevation = 8.dp
),
modifier = Modifier
) {
Image(
painter, modifier = Modifier
.fillMaxWidth(), contentDescription = ""
)
}
// Pin Name Row
Row(modifier = Modifier.fillMaxWidth()) {
Column {
Text(
text = name,
fontSize = 12.sp,
fontWeight = FontWeight.SemiBold,
color = Color.Black,
textAlign = TextAlign.Start,
modifier = modifier
.padding(8.dp)
.align(Alignment.CenterHorizontally)
)
}
Column(modifier = Modifier.fillMaxWidth()) {

Icon(
imageVector = Icons.Default.MoreVert,
contentDescription = null,
modifier = Modifier
.padding(6.dp)
.size(20.dp)
.align(Alignment.End)
.graphicsLayer {
rotationZ = 90f
},
)
}

}
// Pin Author Row
Row {
Image(
painter, modifier = Modifier
.size(width = 24.dp, height = 24.dp)
.clip(RoundedCornerShape(50)), contentDescription = "",
contentScale = ContentScale.Crop
)
Column {
Text(
text = author,
fontSize = 12.sp,
fontWeight = FontWeight.Normal,
color = Color.Black,
textAlign = TextAlign.Start,
modifier = modifier
.padding(5.dp)
)
}
}
}


}

Final Look

For our final look, let’s add PinCard Composables, to the Box-Weight Custom layout we created earlier.

@Composable
fun Pins(modifier: Modifier = Modifier) {

Row(modifier = modifier
.padding(20.dp, 15.dp, 20.dp, 20.dp)
.verticalScroll(rememberScrollState())
.fillMaxWidth()) {

//Column one
Box(modifier = modifier
.fillMaxWidth()
.weight(1f)
) {
Column {
PinCard(
name = "Rabbit Girl",
author = "KINSLEY",
R.drawable.artwork,
modifier = modifier
)
PinCard(
name = "Purple Flowers",
author = "ABBI",
R.drawable.artwork3,
modifier = modifier
)
}

}

// Column two
Box(modifier = modifier
.fillMaxWidth()
.weight(1f)
) {
Column {
PinCard(
name = "Anime Drawing",
author = "AURORA",
R.drawable.artwork2,
modifier = modifier
)
PinCard(
name = "Shinny Clouds",
author = "CLOUD",
R.drawable.artwork4,
modifier = modifier
)
PinCard(
name = "Anime Drawing",
author = "AURORA",
R.drawable.artwork2,
modifier = modifier
)
}
}
}
}

Also, I have added a vertical scroll option to our outermost row so our items can scroll.

Our Pinterest-inspired layout in Jetpack Compose with basic modifiers and properties is ready.

There is much more you can do with the above layout.

For the sake of this guide, I provided a basic idea and a starting template for Pinterest.

That’s all for today.

Stay tuned.

Until we meet again.

May your code be free of bugs and your coffee be strong!

If you have any questions, don’t hesitate to leave a comment down below.

Or reach me on GitHub 🤖 and LinkedIn 🤝!

--

--

DuAa AwAn

Software Engineer | Android | Tech Writer. Join me in exploring the endless possibilities of app development!