How to implement an EmptyState for RecyclerView

Some months ago, on my previous project, I faced the task: to implement an empty state for RecyclerView. And in this article, I want to tell you about my experience with this. I will show you how to implement an EmptyState for RecyclerView without any code inside your Presenter or ViewModel. No more words, let’s get to the deal.

So, we all know about the state of RecyclerView when we have not yet downloaded data into it and have a blank screen. From a UI/UX point of view, it looks awful to the user, so you need to somehow show the user that the data is not yet available or is in the process of being downloaded. And there are a few ways to solve this problem, for example:

  1. 3rd libraries for RecyclerView, which have this functionality already.
  2. New view and the possibility for changing “visibility” from inside our Presenter or ViewModel.
  3. Empty ViewHolder with custom layout and additional code inside our Presenter or ViewModel.

Although all these options are worth implementing, in my opinion, they just overcomplicate such an easy task.

Why are these ways wrong?

If you look at the first point, you will see that additional 3rd library inside your project makes you dependent on someone another. You will fail in the future when a new version of RecyclerView comes, but the author stopped supporting his library.
Second and third ways are followed by additional code inside Presenter or ViewModel, which I would leave as plain as possible, and avoid unnecessary code there.


What can I offer you?

I will show you how you can create custom RecyclerView and delegate this obligation to someone another.

  1. Create CustomRecyclerView

class CustomRecyclerView @JvmOverloads constructor
context: Context, attrs:
AttributeSet? = null,
defStyleAttr: Int = 0
) : RecyclerView(context, attrs, defStyleAttr) {
override fun setAdapter(adapter: Adapter<*>?) {
super.setAdapter(adapter)
}
}

  2. Add variable and set method

class CustomRecyclerView @JvmOverloads constructor(
    context: Context, attrs:
    AttributeSet? = null,
    defStyleAttr: Int = 0
) : RecyclerView(context, attrs, defStyleAttr) {
    private lateinit var emptyView: View
    ...
    
    fun setEmptyView(emptyView: View) {
        this.emptyView = emptyView
    }
}

3.  Add method which will control content on screen

class CustomRecyclerView @JvmOverloads constructor
    context: Context, attrs:
    AttributeSet? = null,
    defStyleAttr: Int = 0
) : RecyclerView(context, attrs, defStyleAttr) {
    ...
    private fun checkIfEmpty() {
        emptyView?.let { emptyView ->
            adapter?.let { adapter ->
                emptyView.isVisible = adapter.itemCount == 0
            }
        }
    }
    ...
}

4. Add AdapterDataObserver and override methods.

This observer will be triggered when data inside your RecyclerView is changed, and our CustomRecyclerView makes decisions about his state

class CustomRecyclerView @JvmOverloads constructor
    context: Context, attrs:
    AttributeSet? = null,
    defStyleAttr: Int = 0
) : RecyclerView(context, attrs, defStyleAttr) {
    private lateinit var emptyView: View
    private val observer: AdapterDataObserver = object : AdapterDataObserver() {
        override fun onChanged() {
            checkIfEmpty()
        }
        override fun onItemRangeRemoved(positionStart: Int, itemCount: Int) {
            checkIfEmpty()
        }
        override fun onItemRangeMoved(fromPosition: Int, toPosition: Int, itemCount: Int) {
            checkIfEmpty()
        }
        override fun onItemRangeInserted(positionStart: Int, itemCount: Int) {
            checkIfEmpty()
        }
        override fun onItemRangeChanged(positionStart: Int, itemCount: Int) {
            checkIfEmpty()
        }
        override fun onItemRangeChanged(positionStart: Int, itemCount: Int, payload: Any?) {
            checkIfEmpty()
        }
    }
    
    ...
}

  5.Add some code inside setAdapter method
class CustomRecyclerView @JvmOverloads constructor
    context: Context, attrs:
    AttributeSet? = null,
    defStyleAttr: Int = 0
) : RecyclerView(context, attrs, defStyleAttr) {
    ...
    override fun setAdapter(adapter: Adapter<*>?) {
        this.adapter?.unregisterAdapterDataObserver(observer)
        super.setAdapter(adapter)
        this.adapter?.registerAdapterDataObserver(observer)
        checkIfEmpty()
    }
    ...
}

So, we have finished with our CustomRecyclerView, and now we can start implementing it inside our app.

  1. Add CustomRecyclerView inside your layout
 <?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout 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"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    tools:context=".MainActivity">
        <com.avocadochif.emptystateviewforrecyclerview.CustomRecyclerView
        android:id="@+id/dataRV"
        android:layout_width="match_parent"
        android:layout_height="0dp"
        app:layout_constraintBottom_toTopOf="@id/bottomContainer"
        app:layout_constraintEnd_toEndOf="parent"
        app:layout_constraintStart_toStartOf="parent"
        app:layout_constraintTop_toTopOf="parent" />
</androidx.constraintlayout.widget.ConstraintLayout>

  2. Add control buttons
 <?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout 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"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    tools:context=".MainActivity">
    ...
    <LinearLayout
        android:id="@+id/bottomContainer"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:orientation="horizontal"
        app:layout_constraintBottom_toBottomOf="parent"
        app:layout_constraintStart_toStartOf="parent">
        <Button
            android:id="@+id/addBtn"
            android:layout_width="0dp"
            android:layout_height="wrap_content"
            android:layout_marginStart="16dp"
            android:layout_marginEnd="8dp"
            android:layout_weight="1"
            android:text="@string/add_btn_title"
            android:textAllCaps="false" />
        <Button
            android:id="@+id/removeBtn"
            android:layout_width="0dp"
            android:layout_height="wrap_content"
            android:layout_marginStart="8dp"
            android:layout_marginEnd="16dp"
            android:layout_weight="1"
            android:text="@string/remove_btn_title"
            android:textAllCaps="false" />
    </LinearLayout>
</androidx.constraintlayout.widget.ConstraintLayout>

 3. Add EmptyView
<code> <?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout 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"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    tools:context=".MainActivity">
    ...
    
    <TextView
        android:id="@+id/emptyViewTV"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:text="@string/empty_view_description"
        android:visibility="gone"
        app:layout_constraintBottom_toTopOf="@id/bottomContainer"
        app:layout_constraintEnd_toEndOf="parent"
        app:layout_constraintStart_toStartOf="parent"
        app:layout_constraintTop_toTopOf="parent" />
    ...
</androidx.constraintlayout.widget.ConstraintLayout>

 4.Create layout for ViewHolder
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_width="match_parent"
    android:layout_height="wrap_content"
    android:orientation="vertical">
    <TextView
        android:id="@+id/labelTV"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:layout_marginStart="16dp"
        android:layout_marginTop="8dp"
        android:layout_marginBottom="8dp" />
</LinearLayout>

 5. Create adapter for RecyclerView
 class DataRecyclerViewAdapter : RecyclerView.Adapter<DataRecyclerViewAdapter.TextViewHolder>() {
    private val data: MutableList<String> = mutableListOf()
    fun addItem(text: String) {
        data.add(text)
        notifyItemRangeInserted(data.size - 1, 1)
    }
    fun removeItem() {
        if (data.isNotEmpty()) {
            data.removeAt(data.size - 1)
            notifyItemRemoved(data.size)
        }
    }
    override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): TextViewHolder {
        return TextViewHolder(LayoutInflater.from(parent.context).inflate(R.layout.item_text, parent, false))
    }
    override fun getItemCount(): Int {
        return data.size
    }
    override fun onBindViewHolder(holder: TextViewHolder, position: Int) {
        holder.bind(data[position])
    }
    class TextViewHolder(view: View) : RecyclerView.ViewHolder(view) {
        fun bind(text: String) {
            itemView.labelTV.text = text
        }
    }
}

 6. Write your MainActivity

You can provide different View or Layout to setEmptyView() method
 class MainActivity : AppCompatActivity() {
    private var adapter: DataRecyclerViewAdapter = DataRecyclerViewAdapter()
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_main)
        initViews()
    }
    private fun initViews() {
        initDataRV()
        initButtons()
    }
    private fun initDataRV() {
        dataRV.layoutManager = LinearLayoutManager(this)
        dataRV.setEmptyView(emptyViewTV)
        dataRV.adapter = adapter
    }
    private fun initButtons() {
        addBtn.setOnClickListener { adapter.addItem(UUID.randomUUID().toString()) }
        removeBtn.setOnClickListener { adapter.removeItem() }
    }
}

 So, in this article, I showed you the best way, as for me, How to implement EmptyState for RecyclerView. Now you don’t need to write any code inside your Presenter or ViewModel to handle it, our CustomRecyclerView will do it for you.

Of course, you can upgrade this view and add new features, for instance, animation and illustrations, or create your library to use it in the future.
I hope this article was interesting for you and you found what you were searching for. I invite everyone to comments where you can express your thoughts and suggestions.

Thanks for reading.

 

Stepan Revytskyi 
Android Developer in NerdzLab