Android RecyclerView single choice adapter
Tags: android, recyclerview, databinding, kotlin
Published: 2019-12-06 03:39:39

More often than not, we as android developers come across a requirement to display a list of objects and make that list `selectable` in a `single choice` mode. Over the time, several ways of implementations have been done. I wanted to share the approach that I took recently to achieve this functionality. This requires basic understanding of `kotlin`, `data binding` and a bit of `mvvm`.

The basic idea here is to keep track of the `selected` position in the adapter as `observable` value and right after this changes, call `notifyItemChanged()` for the `oldPosition` and the `newPosition` with will re-bind the user to the views that will in turn reverse the `check-mark` for the associated `RadioButton`. When the list item is clicked, it updates the `selectedPosition` to the `position` of the clicked list item.

*User*
```kotlin
    data class User(val name: String, val address: String, val age: Int)
```

*RecyclerView Adapter*

```kotlin
    class UserAdapter : RecyclerView.Adapter<UserAdapter.UserViewHolder> {
        
        var users: List<User> = emptyList()
            set(value) {
                field = value
                notifyDataSetChanged()
            }

       // This keeps track of the currently selected position
       var selectedPosition by Delegates.observable(-1) { property, oldPos, newPos ->
            if (newPos in items.indices) {
                notifyItemChanged(oldPos)
                notifyItemChanged(newPos)
           }
       }

        override fun onCreateViewHolder(parent: ViewGroup, viewType:   Int): UserViewHolder {
            val layoutInflater = LayoutInflater.from(parent.context)
            val viewBinding: ViewListItemBinding =
                        DataBindingUtil.inflate(layoutInflater,R.layout.view_list_item, parent, false)
        return UserViewHolder(viewBinding)
        }

       override fun getItemCount(): Int = users.size

       override fun onBindViewHolder(holder: UserViewHolder, position: Int) {
        if (position in users.indices){
            holder.bind(items[position], position == selectedPosition)
            holder.itemView.setOnClickListener { selectedPosition = position }
        }
    }

    inner class UserViewHolder(private val viewBinding: ViewListItemBinding) :
        RecyclerView.ViewHolder(viewBinding.root) {

        fun bind(user: User, selected: Boolean) {
            with(event) {
                viewBinding.tvUserName.text = name
                viewBinding.tvAddress.text = address
                viewBinding.tvAge.text = "$age"
                viewBinding.btnChecked.isChecked = selected
            }
        }
    }
```

*View List Item*
```xml
    <?xml version="1.0" encoding="utf-8"?>
    <layout xmlns:android="http://schemas.android.com/apk/res/android"
      xmlns:tools="http://schemas.android.com/tools">
      <LinearLayout
        android:id="@+id/mainContainer"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:clickable="true"
        android:focusable="true"
        android:foreground="?android:attr/selectableItemBackground"
        android:minHeight="72dp"
        android:orientation="horizontal"
        android:padding="16dp">

        <androidx.appcompat.widget.AppCompatRadioButton
          android:id="@+id/btnChecked"
          android:layout_width="wrap_content"
          android:layout_height="wrap_content"
          android:layout_gravity="start|center_vertical"
          android:layout_marginEnd="@dimen/margin_8dp"
          android:clickable="false"
          tools:checked="true" />

          <LinearLayout
             android:layout_width="0dp"
             android:layout_height="wrap_content"
             android:layout_marginStart="8dp"
             android:layout_weight="1"
             android:orientation="vertical">

             <androidx.appcompat.widget.AppCompatTextView
                android:id="@+id/tvUserName"
                android:layout_width="wrap_content"
                android:layout_height="wrap_content"
                tools:text="User User" />

             <androidx.appcompat.widget.AppCompatTextView
                android:id="@+id/tvAddress"
                android:layout_width="wrap_content"
                android:layout_height="wrap_content"
                tools:text="United States" />

             <androidx.appcompat.widget.AppCompatTextView
                android:id="@+id/tvAge"
                android:layout_width="wrap_content"
                android:layout_height="wrap_content"
                tools:text="26" />
           </LinearLayout>

       </LinearLayout>
   </layout>
```

*User Fragment*
```kotlin
   class UserFragment : Fragment() {

     private lateinit binding: FragmentUserBinding
     private lateinit viewModel: UserViewModel
     private val adapter: UserAdapter = UserAdapter()     

     override onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? {
     super.onCreateView(inflater, container, savedInstanceState)
     binding = DataBindingUtil.inflate(inflater, R.layout.fragment_user, container, false)
     
     binding.recyclerView.layoutManager = LinearLayoutManager(context)
     binding.recyclerView.adapter = adapter
     
     return binding.root
     }

     override fun onActivityCreated(savedInstanceState: Bundle?) {
        super.onActivityCreated(savedInstanceState)
        // initialize UserViewModel here ... the viewModel exposes     `users` as LiveData which we observe here and assign to the adapter
        
        viewModel.users.observe(this, Observer { users ->
            adapter.users = users
        }
     }
   }
```
The source of data is the `ViewModel`. The view model fetches the users using some `repository` in the background thread and exposes that as `LiveData`. The `Fragment` can initialize the view model and `observe` the live data exposed by the view model. When it receives the data, it sets that data to the `adapter` which then binds that data to the `RecyclerView`. This is a pretty basic version of the implementation. We can take this approach and expand into multiple dimensions to achieve various different behaviors.

Please provide comments if you have questions or feedback.
Thank you!