A Cleaner Way to Manage Spacing in RecyclerView
RecyclerView is one of Android’s most powerful tools for displaying large data sets efficiently in a scrollable list or grid format. However, customizing the spacing between items can be a challenge, particularly when you need fine-grained control over the layout. Developers often rely on padding or margins applied directly to item views, but this approach can become unwieldy and error-prone, especially in complex layouts or when dealing with multiple item types.
In this article, we’ll explore a cleaner and more flexible approach to adding spacing in a RecyclerView using custom ItemDecoration
classes. This method provides a maintainable and scalable solution for managing item spacing without the pitfalls of manual padding and margin adjustments. Whether you’re working with simple lists or complex multi-type layouts, decorators offer a more elegant way to handle spacing, minimizing redundant code and ensuring consistent UI layout
What are older version of spacing in RecyclerView?
Developers often use padding or margins to add spacing directly to item views in a RecyclerView. While this may work in some cases, it’s not the most efficient approach. The main issue is that spacing is applied to the item view itself, which requires manual calculations and adjustments, especially for items at the edges of the list. For example, in a vertical list, you don’t want to add bottom spacing to the last item, which means adding conditions to check if the item is the last one. Though this might seem like a simple if
check, it quickly leads to duplicated code across adapters.
<!-- Item layout for RecyclerView -->
<TextView
xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="RecyclerView Item"
android:textSize="18sp"
android:layout_marginBottom="16dp"
android:textColor="@android:color/black"/>
class SimpleAdapter(private val items: List<String>) : RecyclerView.Adapter<SimpleAdapter.SimpleViewHolder>() {
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): SimpleViewHolder {
val view = LayoutInflater.from(parent.context).inflate(R.layout.item_view, parent, false)
return SimpleViewHolder(view)
}
override fun onBindViewHolder(holder: SimpleViewHolder, position: Int) {
holder.bind(items[position])
// Example issue: the margin is applied to all items, including the last one
if (position == itemCount - 1) {
// Last item - still has bottom margin, causing unwanted space at the bottom
}
}
override fun getItemCount(): Int = items.size
class SimpleViewHolder(itemView: View) : RecyclerView.ViewHolder(itemView) {
private val textView: TextView = itemView.findViewById(R.id.textView)
fun bind(text: String) {
textView.text = text
}
}
}
Alternatively, if your list contains multiple item types, you’ll need to add spacing for each type individually, which can make the code more complex and harder to maintain.
What is a better way to add spacing in RecyclerView?
A better solution is to use a custom ItemDecoration
to manage spacing between items in a RecyclerView
. An ItemDecoration
allows you to add custom drawing and layout offsets to specific item views. Essentially, it provides a way to apply spacing to the top, bottom, left, and right of each item view, ensuring consistent spacing regardless of the view's definition.
open class SpacingDecorator : RecyclerView.ItemDecoration {
protected val top: Int
protected val start: Int
protected val end: Int
protected val bottom: Int
constructor(
top: Int,
start: Int,
end: Int,
bottom: Int
) : super() {
this.top = top
this.start = start
this.end = end
this.bottom = bottom
}
override fun getItemOffsets(
outRect: Rect,
view: View,
parent: RecyclerView,
state: RecyclerView.State
) {
with(outRect) {
top = this@SpacingDecorator.top
left = this@SpacingDecorator.start
right = this@SpacingDecorator.end
bottom = this@SpacingDecorator.bottom
}
}
override fun equals(other: Any?): Boolean {
if (other !is SpacingDecorator) return false
return other.top == top && other.start == start && other.end == end && other.bottom == bottom
}
fun top(spaceSize: Int) = SpacingDecorator(spaceSize, start, end, bottom)
fun start(spaceSize: Int) = SpacingDecorator(top, spaceSize, end, bottom)
fun end(spaceSize: Int) = SpacingDecorator(top, start, spaceSize, bottom)
fun bottom(spaceSize: Int) = SpacingDecorator(top, start, end, spaceSize)
fun vertical(spaceSize: Int) = SpacingDecorator(spaceSize, start, end, spaceSize)
fun horizontal(spaceSize: Int) = SpacingDecorator(top, spaceSize, spaceSize, bottom)
companion object {
fun top(spaceSize: Int) = SpacingDecorator(spaceSize, 0, 0, 0)
fun start(spaceSize: Int) = SpacingDecorator(0, spaceSize, 0, 0)
fun end(spaceSize: Int) = SpacingDecorator(0, 0, spaceSize, 0)
fun bottom(spaceSize: Int) = SpacingDecorator(0, 0, 0, spaceSize)
fun vertical(spaceSize: Int) = SpacingDecorator(spaceSize, 0, 0, spaceSize)
fun horizontal(spaceSize: Int) = SpacingDecorator(0, spaceSize, spaceSize, 0)
}
open class Builder {
var top: Int = 0
var start: Int = 0
var end: Int = 0
var bottom: Int = 0
fun top(value: Number) {
this.top = value.toInt()
}
fun start(value: Number) {
this.start = value.toInt()
}
fun end(value: Number) {
this.end = value.toInt()
}
fun bottom(value: Number) {
this.bottom = value.toInt()
}
open fun build() = SpacingDecorator(top, start, end, bottom)
}
}
Here’s a streamlined implementation of the SpacingDecorator
class. This class extends RecyclerView.ItemDecoration
and includes four properties: top, start, end, and bottom, which define the spacing applied to each side of an item view. The getItemOffsets
method is overridden to apply these spacings, making it the key function where the spacing is set.
Once you add this decorator to your RecyclerView
, you'll notice the desired spacing between items.
val recyclerView = binding.recyclerView
recyclerView.addItemDecoration(
SpacingDecoratorBuilder()
.top(/*16dp as int*/)
.build()
)
However, this approach alone isn’t enough. If applied multiple times to a RecyclerView, it will result in duplicate spacing between items. To resolve this, I developed the following solution:
fun RecyclerView.spacing(
builder: SpacingDecorator.Builder.() -> Unit = {}
) {
val spacing = SpacingDecorator.Builder()
.apply(builder)
.build()
val removeList = mutableListOf<Int>()
runCatching {
repeat(this.itemDecorationCount) {
val decoration = getItemDecorationAt(it)
if (decoration !is SpacingDecorator) return@repeat
val hasSetBefore = decoration == spacing
when {
hasSetBefore -> return
else -> removeList.add(it)
}
}
}
removeList.forEach(::removeItemDecorationAt)
addItemDecoration(spacing)
}
Here, we check if the decorator is already applied to the RecyclerView
. If it is, we remove it before adding the new one, preventing duplicate decorators.
You might be thinking, “But it still applies the same spacing to all sides of the item view.” You’re right. However, we can extend the SpacingDecorator
class to allow different spacings for each side of the item view. Here’s the extended version of the SpacingDecorator
class:
class VerticalSpacingDecorator : SpacingDecorator {
constructor(
top: Int,
start: Int,
end: Int,
bottom: Int
) : super(top, start, end, bottom)
override fun getItemOffsets(
outRect: Rect,
view: View,
parent: RecyclerView,
state: RecyclerView.State
) {
val position = parent.getChildAdapterPosition(view)
val itemCount = parent.adapter?.itemCount ?: 0
val isFirstItem = position == 0
val isLastItem = position == itemCount - 1
with(outRect) {
left = this@VerticalSpacingDecorator.start
right = this@VerticalSpacingDecorator.end
top = if (isFirstItem) 0 else this@VerticalSpacingDecorator.top
bottom = if (isLastItem) 0 else this@VerticalSpacingDecorator.bottom
}
}
companion object {
class Builder : SpacingDecorator.Builder() {
override fun build() = VerticalSpacingDecorator(top, start, end, bottom)
}
}
}
This is why the SpacingDecorator
class was designed as an open class—it allows us to easily extend it and apply different spacings to each side of the item view. Additionally, the Builder
class is extended and overridden to create a VerticalSpacingDecorator
.
With this setup, we can now introduce a new extension function as follows:
fun RecyclerView.linearSpacing(
builder: VerticalSpacingDecorator.Companion.Builder.() -> Unit = {}
) {
val spacing = VerticalSpacingDecorator.Companion.Builder()
.apply(builder)
.build()
val removeList = mutableListOf<Int>()
runCatching {
repeat(this.itemDecorationCount) {
val decoration = getItemDecorationAt(it)
if (decoration !is VerticalSpacingDecorator) return@repeat
val hasSetBefore = decoration == spacing
when {
hasSetBefore -> return
else -> removeList.add(it)
}
}
}
removeList.forEach(::removeItemDecorationAt)
addItemDecoration(spacing)
}
Now we can apply different spacing to different sides of the item view. For instance, we can apply top spacing to the first item and bottom spacing to the last item.
recyclerView.linearSpacing {
top = /*16dp as int*/
bottom = /*16dp as int*/
}
Bonus
This solution still works for multi-type items. we can still apply a dedicated spacing decorator for each item type.
For simplicity, I will use a new extension of Decorator class rather than using SpaceDecorator class.
class MultiTypeDecoration : RecyclerView.ItemDecoration() {
override fun getItemOffsets(
outRect: Rect,
view: View,
parent: RecyclerView,
state: RecyclerView.State
) {
if (parent.adapter !is MyAdapter) { // to limit other adapters to use this decorator
super.getItemOffsets(outRect, view, parent, state)
return
}
val index = parent.getChildAdapterPosition(view)
if (index == RecyclerView.NO_POSITION) {
super.getItemOffsets(outRect, view, parent, state)
return
}
val type = parent.adapter?.getItemViewType(index) ?: run {
super.getItemOffsets(outRect, view, parent, state)
return
}
when {
type == MyAdapter.TYPE_HEADER -> {
with(outRect) {
bottom = 10.dp
left = 10.dp
right = 10.dp
}
}
...
}
}
}
Here we simply check the adapter type and apply the spacing to the item view based on the item type.
recyclerView.addItemDecoration(MultiTypeDecoration())
Conclusion
And that’s it! We have successfully added spacing between items in a RecyclerView using decorators in Android. This approach is more flexible and maintainable than adding spacing directly to the item view. It allows us to apply custom spacing to different sides of the item view and prevents the duplication of spacing decorators. I hope you found this article helpful and that you can use this approach in your own projects. And as a homework, you can extend the SpacingDecorator class to apply different spacing to different sides of the item view. Happy coding!