Android RecyclerView single choice adapter

bigyan4424

Bigyan Thapa

Posted on December 6, 2019

Android RecyclerView single choice adapter

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

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

RecyclerView Adapter

    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 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

   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!

💖 💪 🙅 🚩
bigyan4424
Bigyan Thapa

Posted on December 6, 2019

Join Our Newsletter. No Spam, Only the good stuff.

Sign up to receive the latest update from our blog.

Related