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:
To better visualize what I mean by equal spacing, I've highlighted these spaces:
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)
)
}
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)
)
}
Now, try to click on first item:
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)
)
}
And the result (with first item clicked) looks like the following:
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)
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")
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))
}
}
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)
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))
Top comments (0)