πŸšƒ Display a list of items πŸšƒ

RecyclerView is a "new" way of displaying lists, that is more efficient, and uses less memory, as it is recycling views that disappeared, to show the new elements of the list that showed up.

<androidx.recyclerview.widget.RecyclerView
    android:id="@+id/example_recycler_view"
    android:layout_width="match_parent"
    android:layout_height="match_parent" />

Scrollbars

You may enable scrollbars (vertical, horizontal, none, both with "|")

<androidx.recyclerview.widget.RecyclerView
    ...
+    android:scrollbars="vertical"
     />

Layout

By default, the list is using a ListView, which is similar to a LinearLayout with an orientation set to vertical. The next element is below the previous one. You can change the layout inside the XML

<androidx.recyclerview.widget.RecyclerView
    ...
+    app:layoutManager="LinearLayoutManager"
+    android:orientation="horizontal"
     />
<androidx.recyclerview.widget.RecyclerView
    ...
+    app:layoutManager="GridLayoutManager"
     />

Or, inside the code

val recyclerView : RecyclerView = ...
with(recyclerView) {
    // ➑️ Horizontal LinearLayout
    layoutManager = LinearLayoutManager(context, LinearLayoutManager.HORIZONTAL, false)
    // ➑️ Two-columns GridLayout
    layoutManager = GridLayoutManager(context, 2)
}

Adapter

Let's say we have a list of elements of type Data

class Data(val message: String)
// example
val myList = listOf(Data("one"), Data("two"))

We need to create a View for one item. Create an XML Layout. It's worth noting that you should NOT use match_parent to both horizontal/vertical, otherwise each item will take the whole screen.

Ex: data_item.xml | An item with only a TextView
<?xml version="1.0" encoding="utf-8"?>
<!-- ➑️ There is only one item, so we use a FrameLayout -->
<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_width="wrap_content"
    android:layout_height="wrap_content">

    <!-- ➑️ Don't forget to give an Id to your TextView -->
    <TextView
        android:id="@+id/message"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        />

</FrameLayout>

Now, we need to write an Adapter. This is a class that will handle displaying an item of our list (Data) inside one instance of data_item.xml.

DummyAdapter
class DummyAdapter(private val items: List<Data>) : RecyclerView.Adapter<DummyAdapter.ViewHolder>() {
    // ➑️ The function bind takes "data", and fills our Item
    class ViewHolder(view: View) : RecyclerView.ViewHolder(view) {
        private val messageView = view.findViewById<TextView>(R.id.message)

        fun bind(data: Data) {
            messageView.text = data.message
        }
    }

    // ➑️ We are linking data_item.xml here
    override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ViewHolder {
        return ViewHolder(LayoutInflater.from(parent.context).inflate(R.layout.data_item, parent, false))
    }

    override fun onBindViewHolder(holder: ViewHolder, position: Int) {
        holder.bind(items[position])
    }

    override fun getItemCount(): Int {
        return items.size
    }
}

Then, inside an Activity/a Fragment, we need to link the recycler view with the adapter.

val recyclerView : RecyclerView = ...
with(recyclerView) {
+    adapter = DummyAdapter(myList)
}

Additional notes

Optimization: lists of fixed size
val recyclerView : RecyclerView = ...
with(recyclerView) {
+    setHasFixedSize(true)
}
Style: add a separator between items
val recyclerView : RecyclerView = ...
with(recyclerView) {
+    addItemDecoration(DividerItemDecoration(context, DividerItemDecoration.HORIZONTAL))
}
πŸ€” Dynamic lists: notify πŸ€”

The example is using a static unchangeable list. The easy+bad patch would be to rely on notifyDataSetChanged which means we are updating the whole view, for a potentially minor change.

-class DummyAdapter(private val items: List<Data>) : RecyclerView.Adapter<DummyAdapter.ViewHolder>() {
+class DummyAdapter(private var items: List<Data>) : RecyclerView.Adapter<DummyAdapter.ViewHolder>() {

+    fun updateList(newItems: List<Data>) {
+        items = newItems
+        notifyDataSetChanged()
+    }
}

You can improve the code above, only update one index, and use a variant of notify such as notifyItemInserted(position). Or, you can use LiveData (inside a ViewModel) and a ListAdapter.

⚑ Dynamic Lists: ListAdapter ⚑

We need to add a method to compute the difference between two values.

data class Data(val message: String) {
    companion object DataDiff : DiffUtil.ItemCallback<Data>() {
        override fun areItemsTheSame(oldItem: Data, newItem: Data): Boolean {
            /* you should do something appropriate */
            return oldItem == newItem ||
                    oldItem.message == newItem.message
        }

        override fun areContentsTheSame(oldItem: Data, newItem: Data): Boolean {
            /* check every attribute that may have changed */
            return oldItem.message == newItem.message
        }
    }
}

Then, we replace our Adapter with a ListAdapter.

-class DummyAdapter(private val items: List<Data>) : RecyclerView.Adapter<DummyAdapter.ViewHolder>() {
+class DummyAdapter(private val items: LiveData<List<Data>>) : ListAdapter<Data, DummyAdapter.ViewHolder>(Data) {
        private val messageView = view.findViewById<TextView>(R.id.message)

        fun bind(data: Data) {
            messageView.text = data.message
        }
    }

    override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ViewHolder {
        return ViewHolder(LayoutInflater.from(parent.context).inflate(R.layout.data_item, parent, false))
    }

    override fun onBindViewHolder(holder: ViewHolder, position: Int) {
-        holder.bind(items[position])
+        holder.bind(items.value!![position])
    }

    override fun getItemCount(): Int {
-        return items.size
+        return items.value?.size ?: 0
    }
}

Then, you can use the code below to update the recycler view when the list is updated. The ListAdapter will compute changes in a background thread, and only update what changed.

viewModel.myList.observe(this) {
  with(binding.recyclerView.adapter as DummyAdapter) {
    submitList(it)
  }
}

⚠️ It's worth noting that this is only working because items inside our ListAdapter is a LiveData. If it was a normal list, we would have to re-assign the attribute as we did with notify.

See also


πŸ‘» To-do πŸ‘»

Stuff that I found, but never read/used yet.

binding.recyclerView.itemAnimator = null
binding.recyclerView.scrollToPosition(0)