bg

How to implement an EmptyState for RecyclerView

author

Stepan Revytskyi

/

Android Developer

4 min read

4 min read

Some months ago, on my previous project, I faced the task: to implement an EmptyState for Android 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. It is not that challenging to implement an EmptyState for RecyclerView, if you get deeper into it, so I am going to tell you the easiest way to do it. 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, so you need to know how to show an empty view with RecycleView. And there are a few ways to solve this problem, for example:

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

Although all these options are worth trying for implementing the RecyclerView with empty state, 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 the additional 3rd library inside your project makes you dependent on someone else. You will fail in the future when a new version of RecyclerView comes, but the author stopped supporting his library for Android EmptyRecyclerView.

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. Implementing the Android EmptyRecyclerView here would overcomplicate, and as you know, complex structures are often less stable.

What can I offer you?

I will show you how you can create custom RecyclerViewwith method setEmptyView and delegate this obligation to someone else.

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, so if RecyclerView is empty it’ll show a message accordingly.

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 use method setEmptyView() to provide different View or Layout to RecyclerView
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 and how to check if RecyclerView is empty in Android. Now you don’t need to write any code inside your Presenter or ViewModel to handle it, our CustomRecyclerView will do it for you, making the implementation of RecyclerView with EmptyState on Android so much more convenient.

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, to make your EmptyRecyclerView more appealing to the users.

Hopefully, now you know how to check if RecyclerView is empty, how to make sure your RecyclerView with Empty State on Android looks appealing, and how to properly implement your EmptyRecyclerView. 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