My approach to solve the problem of first and last item padding with Jetpack Compose

pchmielowski

Piotr Chmielowski

Posted on February 6, 2022

My approach to solve the problem of first and last item padding with Jetpack Compose

Edit: sorry for stretched screenshots. Please click on each to see it with correct ratio.

In this post I'll share my approach to solve the problem I encounter quite often: a list of elements with equal spacing between items themselves as well as parent boundaries.

Here is an example of the view we are going to build:

Items with equal spacing

To better visualize what I mean by equal spacing, I've highlighted these spaces:

Spaces highlighted


First I'd like to explain why the simplest approach will not work. Here is the code of something I'd consider the simplest approach:



@Composable
fun ItemList() {
    Column(modifier = Modifier.padding(vertical = 8.dp)) {
        Item("Item 1")
        Item("Item 2")
        Item("Item 3")
        Item("Item 4")
    }
}

@Composable
fun Item(text: String) {
    Text(
        text = text,
        modifier = Modifier
            .fillMaxWidth()
            .padding(horizontal = 16.dp, vertical = 8.dp)
    )
}



Enter fullscreen mode Exit fullscreen mode

As you can see, Item has horizontal padding of 16.dp. Vertical padding of Item and Column is 8.dp. Vertical paddings are added so, as an result we get 16.dp space everywhere.

This approach, however, has one big problem: doesn't look pretty when clickable. Let's modify Item function:



@Composable
fun Item(text: String) {
    Text(
        text = text,
        modifier = Modifier
            .fillMaxWidth()
            .clickable {  } // Add this line
            .padding(horizontal = 16.dp, vertical = 8.dp)
    )
}


Enter fullscreen mode Exit fullscreen mode

Now, try to click on first item:

First item clicked

As you can see, there is a white space above the clicked item. It is ugly. To fix it, we have to migrate from the padding of parent (Column) to the padding of child (Item).

This mean that first and last items should have different vertical padding than central ones. top padding of first item should be 16.dp, the same apply for bottom padding of the last item. Other paddings stay unchanged with the value of 8.dp.

There are several possible approaches. We can pass index as an argument to Item function. We can pass isFirst and isLast flags. We can pass padding value directly. All these approach are valid, however my personal sense of taste consider them not elegant.

Therefore I'd suggest another approach: pass the Modifier as an argument.

Here is the final solution:




@Composable
fun ItemList() {
    Column {
        Item("Item 1", modifier = Modifier.padding(top = 16.dp, bottom = 8.dp))
        Item("Item 2")
        Item("Item 3")
        Item("Item 4", modifier = Modifier.padding(top = 8.dp, bottom = 16.dp))
    }
}

@Composable
fun Item(text: String, modifier: Modifier = Modifier.padding(vertical = 8.dp)) {
    Text(
        text = text,
        modifier = Modifier
            .fillMaxWidth()
            .clickable {  }
            .padding(horizontal = 16.dp)
            .then(modifier)
    )
}


Enter fullscreen mode Exit fullscreen mode

And the result (with first item clicked) looks like the following:

First item clicked


What is important here, I've used .then(modifier) option. I encourage you to try out what happen if instead of using then, modifier is injected in this way:



modifier = modifier
    .fillMaxWidth()
    .clickable {  }
    .padding(horizontal = 16.dp)


Enter fullscreen mode Exit fullscreen mode

Spoiler: because vertical padding would be applied before applying clickable, the on-click highlighted area will not cover padding.
That is why the order of modifiers is so important and why then function is useful.

(Optional) further refactoring

I believe it is very important not to mix abstraction levels in the code. The following code is an example of mixing abstractions:



// Padding size is set in the parent function:
Item("Item 1", modifier = Modifier.padding(top = 16.dp, bottom = 8.dp))
// Padding size is set in the child function:
Item("Item 2")


Enter fullscreen mode Exit fullscreen mode

Therefore I suggest the following steps of refactoring:

First, pull the padding values to the parent:



@Composable
fun ItemList() {
    Column {
        Item("Item 1", modifier = Modifier.padding(top = 16.dp, bottom = 8.dp))
        Item("Item 2", modifier = Modifier.padding(vertical = 8.dp))
        Item("Item 3", modifier = Modifier.padding(vertical = 8.dp))
        Item("Item 4", modifier = Modifier.padding(top = 8.dp, bottom = 16.dp))
    }
}


Enter fullscreen mode Exit fullscreen mode

Then create helper functions to increase readability and remove code duplication:



@Stable
private fun Modifier.firstElementPadding(padding: Dp) =
    padding(top = padding, bottom = padding / 2)

@Stable
private fun Modifier.middleElementPadding(padding: Dp) =
    padding(vertical = padding / 2)

@Stable
private fun Modifier.lastElementPadding(padding: Dp) =
    padding(top = padding / 2, bottom = padding)


Enter fullscreen mode Exit fullscreen mode


Item("Item 1", modifier = Modifier.firstElementPadding(16.dp))
Item("Item 2", modifier = Modifier.middleElementPadding(16.dp))
Item("Item 3", modifier = Modifier.middleElementPadding(16.dp))
Item("Item 4", modifier = Modifier.lastElementPadding(16.dp))


Enter fullscreen mode Exit fullscreen mode
💖 💪 🙅 🚩
pchmielowski
Piotr Chmielowski

Posted on February 6, 2022

Join Our Newsletter. No Spam, Only the good stuff.

Sign up to receive the latest update from our blog.

Related