Lazy layouts contentPadding

May 22, 2022

TL;DR: When working with Lazy layouts in Compose, use contentPadding to avoid clipping before and after the last item in your list. The concept is similar to clipToPadding = true in RecyclerView.

Here is a LazyRow implementation that has this issue (notice that Modifier.padding was used):

LazyRow(
  horizontalArrangement = Arrangement.spacedBy(12.dp),
  modifier = Modifier
    .padding(
      start = 24.dp,
      end = 24.dp
    )
)

The problem is both start and end padding is always there for every item. What we really want is add add start and end padding for the first item and the last item. Items in between should not be clipped when scrolled.

It’s easy to make the same mistake with a LazyColumn:

LazyColumn example

Again, the top and bottom padding that’s always there for every item. Ideally, only the first and last items in our LazyColumn would have a padding.

Here is the full source code for the LazyRow example above:

class MainActivity : ComponentActivity() {
  override fun onCreate(savedInstanceState: Bundle?) {
    super.onCreate(savedInstanceState)
    setContent {
      LazyLayoutsDemoTheme {
        // A surface container using the 'background' color from the theme
        Surface(
          modifier = Modifier
            .fillMaxSize(),
          color = MaterialTheme.colors.background
        ) {
          Box(
            modifier = Modifier
              .fillMaxWidth()
              .wrapContentSize(Alignment.Center)
          ) {
            MyCardList()
          }
        }
      }
    }
  }
}

@Composable
fun MyCardList() {
  val cardData = remember { generateFakeCards() }

  LazyRow(
    horizontalArrangement = Arrangement.spacedBy(12.dp),
    modifier = Modifier
      .padding(
        start = 24.dp,
        end = 24.dp
      )
  ) {
    items(cardData) { item ->
      MyCard(
        title = item.first,
        subtitle = item.second
      )
    }
  }
}

@Composable
fun MyCard(
  title: String,
  subtitle: String
) {
  Card(
    shape = RoundedCornerShape(12.dp),
    modifier = Modifier
      .height(180.dp)
      .width(140.dp)
  ) {
    Image(
      painter = painterResource(id = R.drawable.green_card),
      contentDescription = null,
      contentScale = ContentScale.FillBounds
    )
    Column(
      verticalArrangement = Arrangement.SpaceBetween,
      horizontalAlignment = Alignment.CenterHorizontally,
      modifier = Modifier
        .padding(
          top = 48.dp,
          bottom = 16.dp
        )
    ) {
      Text(
        text = title,
        color = Color.White,
        style = MaterialTheme.typography.h6,
        textAlign = TextAlign.Center
      )
      Text(
        text = subtitle,
        color = Color.White,
        style = MaterialTheme.typography.subtitle1,
        textAlign = TextAlign.Center
      )
    }
  }
}

@Preview(showBackground = true)
@Composable
fun MyCardPreview() {
  LazyLayoutsDemoTheme {
    MyCard(
      title = "my title",
      subtitle = "my subtitle"
    )
  }
}

private fun generateFakeCards(): List<Pair<String, String>> {
  return MutableList(10) { index ->
    val cardNumber = index+1
    "Title $cardNumber" to "Subtitle $cardNumber"
  }
}

The issue is caused by using the modifier on the LazyRow:

  LazyRow(
    horizontalArrangement = Arrangement.spacedBy(12.dp),
    modifier = Modifier
       .padding(
           start = 24.dp,
           end = 24.dp
      )
  )

Using that modifier clips your content to remain within the bounds of your LazyRow padding.

Without that modifier, the layout does not look correct either. We do want the padding to be applied before the first and after the last item without clipping the content:

This is what contentPadding param is designed to address:

To do that, we removed the modifier and used contentPadding instead:

  LazyRow(
    horizontalArrangement = Arrangement.spacedBy(12.dp),
    contentPadding = PaddingValues(
      start = 24.dp,
      end = 24.dp
    )
  ) {
    items(cardData) { item ->
      MyCard(
        title = item.first,
        subtitle = item.second
      )
    }
  }

The contentPadding parameter is very well documented as well:

@param contentPadding a padding around the whole content. 
This will add padding for the content after it has been clipped,
which is not possible via [modifier] param. 
You can use it to add a padding before the first item 
or after the last one. If you want to add a spacing
between each item use [horizontalArrangement].

That was easy! The same fix applies to LazyColumn

  LazyColumn(
    verticalArrangement = Arrangement.spacedBy(12.dp),
    contentPadding = PaddingValues(
      top = 24.dp,
      bottom = 24.dp
    )
  ) {
    items(cardData) { item ->
      MyCard(
        title = item.first,
        subtitle = item.second
      )
    }
  }
Previous
Previous

Lazy Grid layouts in Compose

Next
Next

Composable functions and return types