6 Things You Might Not Know About RecyclerView
Tags: android, kotlin, recyclerview, tutorial
Published: 2020-10-09 15:15:17

The entire code base was uploaded to [GitHub Repo](https://github.com/bigaru/demo-recyclerview) so that you can better comprehend the various concepts. The chapters are built on each other. Nonetheless, if you are familiar with a certain concept, that one can be skipped. Furthermore, a separate git branch was created for each chapter. So you can easily jump back and forth between the chapters.

{% github bigaru/demo-recyclerview no-readme %}

```bash 
pixel@fedora:~/repo $ git branch
  basic-setup
  layoutManager
  clickHandling-interface
  clickHandling-functionType
  multiViewTypes
  loadMore
  dataBinding
  diffUtil
```


## Table of Contents

* [Basic Setup](#basic-setup)
* [Set LayoutManager declaratively](#set-layoutManager-declaratively)
* [Click Handling](#click-handling)
    * [Interface Approach](#interface-approach)
    * [Function Type Approach](#function-type-approach)
* [Multiple View Types](#multiple-view-types)
* [Load More / Infinite Scrolling](#load-more-/-infinite-scrolling)
* [Data Binding](#data-binding)
* [DiffUtil](#diffutil)
* [References](#references)

## Basic Setup
Add the recyclerview to your dependencies in `build.gradle (:app)`:
```groovy

dependencies {
    // ...
    implementation 'androidx.recyclerview:recyclerview:1.1.0'

}
```

Our model looks as follows:
```kotlin
data class Todo(
    val id: UUID,
    val description: String,
    val isDone: Boolean,
    val dueDate: Date
)

interface TodoRepository {
    fun getOne(id: UUID): Todo?
    fun getMany(beginAt: Int, count: Int): List<Todo>
    fun insert(item: Todo): Boolean
    fun remove(id: UUID): Boolean
    fun totalSize(): Int
}
```

Our RecyclerAdapter and the corresponding ViewHolder are set up as follows:
```kotlin
data class TodoVH(val root: View): RecyclerView.ViewHolder(root){
    val description: TextView = root.findViewById(R.id.row_description)
    val id: TextView = root.findViewById(R.id.row_id)
    val checkBox: CheckBox = root.findViewById(R.id.row_checkbox)

    fun bindData(item: Todo){
       description.text = item.description
       // display only the first 8 characters
       id.text = item.id.toString().take(8)
    }
}

class TodoAdapter(val data: List<Todo>): RecyclerView.Adapter<TodoVH>() {
    override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): TodoVH {
        val rootView = LayoutInflater.from(parent.context)
            .inflate(R.layout.row_default, parent, false)
        return TodoVH(rootView)
    }

    override fun onBindViewHolder(holder: TodoVH, position: Int) {
        holder.bindData(data[position])
    }

    override fun getItemCount(): Int = data.size
}
```

Lastly our Activity:
```kotlin
class MainActivity : AppCompatActivity() {

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_main)

        val todoRepo = DefaultTodoRepo()
        val initialData = todoRepo.getMany(0, 10)
        val todoAdapter = TodoAdapter(initialData)

        val recyclerView: RecyclerView = findViewById(R.id.recyclerview)
        recyclerView.apply {
            adapter = todoAdapter
            layoutManager = LinearLayoutManager(context)
            setHasFixedSize(true)
        }
    }
}
```

## Set LayoutManager declaratively

Instead of explicitly instantiating a LayoutManager in the Activity,
```kotlin
recyclerView.apply {
    adapter = todoAdapter
    layoutManager = LinearLayoutManager(context) //<-- Remove this line
    setHasFixedSize(true)
}
```

we could declaratively set a LayoutManager using the fully-qualified name in the XML layout. 
```xml
<androidx.recyclerview.widget.RecyclerView
    android:id="@+id/recyclerview"
    android:layout_width="0dp"
    android:layout_height="0dp"
    app:layoutManager="androidx.recyclerview.widget.LinearLayoutManager"
    ...
/>
```

## Click Handling
Since the `onBindViewHolder` method is used to populate the views while scrolling, do not set the `View.OnClickListener` inside the `onBindViewHolder` method. Otherwise there will be a lot of unnecessary `setOnClickListener` calls. It is wiser to set the `View.OnClickListener` inside the `onCreateViewHolder` method or in the constructor of the ViewHolder.

### Interface Approach
Let's declare first a custom interface:
```kotlin
interface OnItemClickListener {
    fun onItemClick(item: Todo, position: Int)
}
```

Invoke our custom `clickListener` when the row view was clicked:
```kotlin
data class TodoVH(
    val root: View,
    val clickListener: OnItemClickListener
): RecyclerView.ViewHolder(root), View.OnClickListener {

    // ...
    lateinit var todo: Todo

    init {
        root.setOnClickListener(this)
    }

    fun bindData(data: Todo){
        // ...
        todo = data
    }

    override fun onClick(v: View?) {
        clickListener.onItemClick(todo, adapterPosition)
    }
}
```

Pass the `clickListener` down from Adapter to ViewHolder:
```kotlin
class TodoAdapter(val data: List<Todo>, val clickListener: OnItemClickListener): RecyclerView.Adapter<TodoVH>() {
    override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): TodoVH {
        val rootView = LayoutInflater.from(parent.context)
            .inflate(R.layout.row_default, parent, false)
        return TodoVH(rootView, clickListener)
    }

    // ...
```

Implement the interface in MainActivity:
```kotlin
class MainActivity : AppCompatActivity(), OnItemClickListener {

    override fun onCreate(savedInstanceState: Bundle?) {
        // ...
        val todoAdapter = TodoAdapter(initialData, this)
        // ...
    }

    override fun onItemClick(item: Todo, position: Int) {
        Toast.makeText(this, item.description, Toast.LENGTH_SHORT).show()
    }
}

```

### Function Type Approach
Let's first declare our function type:
```kotlin
typealias OnItemClick = (Todo, Int) -> Unit
```

Invoke our `onItemClick` when the row view was clicked:
```kotlin
data class TodoVH(
    val root: View,
    val onItemClick: OnItemClick
): RecyclerView.ViewHolder(root) {
    // ...
    lateinit var todo: Todo

    init {
        // Pass a lambda instead of interface
        root.setOnClickListener{ onItemClick(todo, adapterPosition) }
    }

    fun bindData(data: Todo){
        // ...
        todo = data
    }
}
```

Pass the `onItemClick` down from Adapter to ViewHolder:
```kotlin
class TodoAdapter(val data: List<Todo>, val onItemClick: OnItemClick): RecyclerView.Adapter<TodoVH>() {
    override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): TodoVH {
        val rootView = LayoutInflater.from(parent.context)
            .inflate(R.layout.row_default, parent, false)
        return TodoVH(rootView, onItemClick)
    }

   // ...
```

Simply implement as lambda in MainActivity:
```kotlin
class MainActivity : AppCompatActivity() {

    override fun onCreate(savedInstanceState: Bundle?) {
        // ...
        val todoAdapter = TodoAdapter(initialData){todo, position ->
            Toast.makeText(this, todo.description, Toast.LENGTH_SHORT).show()
        }
        // ...
    }
}
```

## Multiple View Types
RecyclerView has the ability to render different types of row view. In our example we like to show different icons based on the Todo's type.

Let's extend our Todo model with type:
```kotlin
enum class TodoType(val value: Int) { Work(0), Home(1), Hobby(2) }
data class Todo(
    val id: UUID, 
    val description: String,
    val isDone: Boolean,
    val dueDate: Date, 
    val type: TodoType 
) 
```

First we need to create 3 icons. (Right click on the drawable folder -> New -> Vector Asset) Then add an ImageView in our row layout. Duplicate this row layout twice so that we have for each type an separate row layout. Don't forget to change the drawable in each layout.
```xml
<ImageView
    android:id="@+id/imageView"
    android:layout_width="wrap_content"
    android:layout_height="wrap_content"

    ...
    app:srcCompat="@drawable/ic_work" />
```

In order to render different view types, we have to provide own logic in the `getItemViewType` method. Since we have unique values for each Todo type, the type value is simply passed on. In the `onCreateViewHolder` method, the corresponding layout is created based on the `viewType`.
```kotlin
class TodoAdapter(val data: List<Todo>, val onItemClick: OnItemClick): RecyclerView.Adapter<TodoVH>() {
    override fun getItemViewType(position: Int): Int {
        return data[position].type.value
    }
    
    override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): TodoVH {
        val layout = when(viewType) {
            TodoType.Work.value -> R.layout.row_work
            TodoType.Home.value -> R.layout.row_home
            else -> R.layout.row_hobby
        }

        val rootView = LayoutInflater.from(parent.context).inflate(layout, parent, false)
        return TodoVH(rootView, onItemClick)
    }
    // ...
}
```

Doesn't it look beautiful?
![Screenshot](https://dev-to-uploads.s3.amazonaws.com/i/i3ir8r241m9e0ysg5mwf.jpg)

By the way, you could also add header and footer to your recyclerview with this approach.

## Load More / Infinite Scrolling

The basic idea is that we use a custom scroll listener to check whether the last item is already visible when scrolling down. When yes, we check if there are more items in our repository available. If that is also true, we display the loading row and we let the new data load. As soon as the data is loaded, it is inserted and the loading row is hidden.

Our scroll listener:
```kotlin
class InfiniteScrollListener(
    val hasMoreItems: (currentSize: Int)->Boolean,
    val loadMoreItems: (currentSize: Int)->Unit
): RecyclerView.OnScrollListener(){
    var isLoading = false

    override fun onScrolled(recyclerView: RecyclerView, dx: Int, dy: Int) {
        super.onScrolled(recyclerView, dx, dy)

        val layoutManager = recyclerView.layoutManager as LinearLayoutManager
        val hasScrolledDown = dy > 0

        if(!hasScrolledDown) { return }
        val currentSize = layoutManager.itemCount

        if(!isLoading && hasMoreItems(currentSize)){
            isLoading = true
            loadMoreItems(currentSize)
        }
    }
}
```

We introduce a marker interface in order to unify the types `LoadItem` and `Todo`. This allows us to put both types in a list.
```kotlin
sealed class Item
object LoadItem: Item()

data class Todo(...): Item()
```

Extend the adapter so that it can show/hide the loading row and insert new data set. Note that we also adapted the type parameters of list and adapter.
```kotlin
const val LOAD_TYPE = -1

data class LoadVH(val root: View): RecyclerView.ViewHolder(root)

// Note that we changed to `MutableList<Item>` and to `RecyclerView.ViewHolder`
class TodoAdapter(val data: MutableList<Item>, val onItemClick: OnItemClick): RecyclerView.Adapter<RecyclerView.ViewHolder>() {
    override fun getItemViewType(position: Int): Int {
        val item = data[position]
        return when(item){
            is Todo -> item.type.value
            is LoadItem -> LOAD_TYPE
        }
    }

    override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): RecyclerView.ViewHolder {
        // ...
        val rootView = LayoutInflater.from(parent.context)
            .inflate(layout, parent, false)

        if (viewType == LOAD_TYPE){ return LoadVH(rootView) }

        return TodoVH(rootView, onItemClick)
    }
    // ...

    fun showLoading(){
        data.add(LoadItem)
        notifyItemInserted(data.size -1)
    }

    fun hideLoading(){
        data.removeIf{i -> i == LoadItem}
        notifyItemRemoved(data.size -1)
    }

    fun insertData(newData: List<Todo>){
        val begin = data.size
        val end = begin + newData.size - 1

        data.addAll(newData)
        notifyItemRangeInserted(begin, end)
    }
}
```

With the `Handler`, the network delay is simulated. The implementations for `hasMoreItems` and `loadMoreItems` look as follows:
```kotlin
class MainActivity : AppCompatActivity() {
    private lateinit var scrollListener: InfiniteScrollListener
    private lateinit var todoAdapter: TodoAdapter

    private val todoRepo = DefaultTodoRepo()
    private val PAGE_SIZE = 5

    fun loadMoreItems(currentSize: Int){
        todoAdapter.showLoading()

        // artificial delay
        Handler().postDelayed({
            val newData = todoRepo.getMany(currentSize, PAGE_SIZE)
            todoAdapter.insertData(newData)

            scrollListener.isLoading = false
            todoAdapter.hideLoading()
        }, 4000)
    }

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_main)

        val initialData = todoRepo.getMany(0, 10).map { it as Item }.toMutableList()
        todoAdapter = TodoAdapter(initialData){_, _ ->  }

        scrollListener = InfiniteScrollListener(
            {currentSize ->  todoRepo.totalSize() > currentSize},
            this::loadMoreItems
        )

        val recyclerView: RecyclerView = findViewById(R.id.recyclerview)
        recyclerView.apply {
            adapter = todoAdapter
            setHasFixedSize(true)
            addOnScrollListener(scrollListener)
        }
    }
}
```

## Data Binding
It is expected that your are a bit familiar with data binding and MVVM architecture.

Add the following lines in your `build.gradle (:app)`:
```groovy
//...
apply plugin: 'kotlin-kapt'

android {
    // ...

    dataBinding {
        enabled true
    }
}

dependencies {
    //...

    implementation "androidx.lifecycle:lifecycle-viewmodel-ktx:2.2.0"
    implementation "androidx.lifecycle:lifecycle-livedata-ktx:2.2.0"
}
```

Let's move every business logic to the ViewModel. Note that we don't expose the mutable livedata.
```kotlin
class MainViewModel : ViewModel(), LoadMoreListener{
    private val PAGE_SIZE = 5
    private val todoRepo: TodoRepository = DefaultTodoRepo()

    private var _data: MutableLiveData<List<Item>> = MutableLiveData(emptyList())
    val data: LiveData<List<Item>> = _data

    private var _isLoadingVisible: MutableLiveData<Boolean> = MutableLiveData(false)
    val isLoadingVisible: LiveData<Boolean> = _isLoadingVisible

    init {
        val initialData = todoRepo.getMany(0,10)
        _data.value = initialData
    }

    override fun loadMore(currentSize: Int) {
        val isLoading = _isLoadingVisible.value!!

        if (!isLoading && todoRepo.totalSize() > currentSize) {
            _isLoadingVisible.postValue(true)

            // artificial delay
            Handler().postDelayed({
                val newData = todoRepo.getMany(currentSize, PAGE_SIZE)
                val oldData = _data.value ?: emptyList()
                _data.postValue(oldData + newData)

                _isLoadingVisible.postValue(false)
            }, 4000)
        }
    }
}
```

We need to refactor the `loadMoreItems` from a function type to an interface because the function type causes errors with the binding adapter. Furthermore, we moved the `hasMoreItems` check within the `loadMore` method (see code snippet above).
```kotlin
interface LoadMoreListener{
    fun loadMore(currentSize: Int)
}

class InfiniteScrollListener(
    val loadMoreListener: LoadMoreListener
): RecyclerView.OnScrollListener(){
    override fun onScrolled(recyclerView: RecyclerView, dx: Int, dy: Int) {
        super.onScrolled(recyclerView, dx, dy)

        val layoutManager = recyclerView.layoutManager as LinearLayoutManager
        val hasScrolledDown = dy > 0

        if(!hasScrolledDown) { return }

        val currentSize = layoutManager.itemCount
        loadMoreListener.loadMore(currentSize)
    }
}
```
We slightly adapted the TodoAdapter:
```kotlin
class TodoAdapter(var data: List<Item>): RecyclerView.Adapter<RecyclerView.ViewHolder>() {
    //...

    fun setLoading(isVisible: Boolean){
        if(isVisible) {
            data = data + listOf(LoadItem)
            notifyItemInserted(data.size - 1)
        } else {
            data = data.filter { i -> i != LoadItem }
            notifyItemRemoved(data.size - 1)
        }
    }

    fun updateData(newData: List<Item>){
        data = newData
        notifyDataSetChanged()
    }
}
```

Like the extension method allows us to extend an existing class with additional method in Kotlin. Similarly, the binding adapter allows us to define additional attributes for an existing view in XML. Note that the first parameter is the extending view itself. The other parameters are the new attributes. They should match in length with the annotation parameters above.
```kotlin
@BindingAdapter(value = ["data", "loadingVisibility","loadMoreItems"])
fun modifyAdapter(
    recyclerView: RecyclerView,
    data: LiveData<List<Item>>,
    isLoadingVisible: LiveData<Boolean>,
    loadMoreItems: LoadMoreListener
){
    if (recyclerView.adapter == null){
        recyclerView.setHasFixedSize(true)
        recyclerView.adapter = TodoAdapter(data.value ?: emptyList())

        val scrollListener = InfiniteScrollListener(loadMoreItems)
        recyclerView.addOnScrollListener(scrollListener)

    }else{
        val todoAdapter = recyclerView.adapter as TodoAdapter
        val items = data.value ?: emptyList()

        todoAdapter.updateData(items)
        todoAdapter.setLoading(isLoadingVisible.value ?: false)
    }
}
```

Now we can simply wire up our RecyclerView with our ViewModel. Isn't that cool?
```xml
<?xml version="1.0" encoding="utf-8"?>
<layout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:app="http://schemas.android.com/apk/res-auto"
    xmlns:tools="http://schemas.android.com/tools">

    <data>
        <variable
            name="viewmodel"
            type="in.abaddon.demorv.MainViewModel" />
    </data>

    <androidx.constraintlayout.widget.ConstraintLayout
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        tools:context=".MainActivity">

        <androidx.recyclerview.widget.RecyclerView
        ...
            app:data="@{viewmodel.data}"
            app:loadingVisibility="@{viewmodel.isLoadingVisible}"
            app:loadMoreItems="@{viewmodel}"
         />
    </androidx.constraintlayout.widget.ConstraintLayout>
</layout>
```

Finally our Activity:
```kotlin
class MainActivity : AppCompatActivity() {
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)

        val activityMainBinding: ActivityMainBinding = 
            DataBindingUtil.setContentView(this, R.layout.activity_main)
        val mainViewModel = ViewModelProvider(this).get(MainViewModel::class.java)

        activityMainBinding.viewmodel = mainViewModel
        // it's necessary because we use LiveData
        activityMainBinding.lifecycleOwner = this
    }
}
```

## DiffUtil
In the previous chapter, we adapted the `updateDate` method so that `notifyDataSetChanged()` is called every time. Since the RecylerView cannot know which items were modified, all the visible items are recreated. Therefore, the  `notifyDataSetChanged()` call is very expensive.
```kotlin
fun updateData(newData: List<Item>){
    data = newData
    notifyDataSetChanged() // <-- expensive call
}
```

That is why Android offers a helper class named `DiffUtil` to detect the changes. In order to use the DiffUtil, we need a custom class which extends `DiffUtil.Callback`. The `areItemsTheSame` is used to check whether two items should represent the same thing. The `areContentsTheSame` is used to check if the two items have the same data.
```kotlin
class TodoDiffCallback(val old: List<Item>, val new: List<Item>): DiffUtil.Callback(){
    override fun getOldListSize(): Int = old.size
    override fun getNewListSize(): Int = new.size

    // Compare the identifiers
    override fun areItemsTheSame(oldItemPosition: Int, newItemPosition: Int): Boolean {
        val oldItem = old[oldItemPosition]
        val newItem = new[newItemPosition]

        return when {
            oldItem is Todo && newItem is Todo -> oldItem.id == newItem.id
            oldItem is LoadItem && newItem is LoadItem -> true
            else -> false
        }
    }

    // Compare the contents
    override fun areContentsTheSame(oldItemPosition: Int, newItemPosition: Int): Boolean {
        val oldItem = old[oldItemPosition]
        val newItem = new[newItemPosition]

        return when {
            oldItem is Todo && newItem is Todo -> oldItem == newItem
            oldItem is LoadItem && newItem is LoadItem -> true
            else -> false
        }
    }
}
```

Using TodoDiffCallback, we can compute the diff. Thanks to the diff, only the modified items are updated.
```kotlin
class TodoAdapter(var data: List<Item>): RecyclerView.Adapter<RecyclerView.ViewHolder>() {
    // ...

    fun updateData(newData: List<Item>){
        val diffCallback = TodoDiffCallback(data, newData)
        val diffResult = DiffUtil.calculateDiff(diffCallback)

        data = newData
        diffResult.dispatchUpdatesTo(this)
    }
}
```


## References

- [RecyclerView Item Click Listener the Right Way](https://medium.com/android-gate/recyclerview-item-click-listener-the-right-way-daecc838fbb9) by Ngenge Senior
- [Android RecyclerView with multiple view type (multiple view holder)](https://medium.com/@droidbyme/android-recyclerview-with-multiple-view-type-multiple-view-holder-af798458763b) by Droid By Me
- [How to add Load More / Infinite Scrolling in Android using Kotlin](https://johncodeos.com/how-to-add-load-more-infinite-scrolling-in-android-using-kotlin/) by John Codeos
- [Binding adapters](https://developer.android.com/topic/libraries/data-binding/binding-adapters) by Android
- [The powerful tool DiffUtil in RecyclerView](https://blog.mindorks.com/the-powerful-tool-diff-util-in-recyclerview-android-tutorial) by Himanshu Singh
- Cover Image by [Annie Spratt](https://unsplash.com/@anniespratt?utm_source=unsplash&amp;utm_medium=referral&amp;utm_content=creditCopyText) from [Unsplash](https://unsplash.com/?utm_source=unsplash&amp;utm_medium=referral&amp;utm_content=creditCopyText)