Android RecyclerView single choice adapter
Bigyan Thapa
Posted on December 6, 2019
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!
Posted on December 6, 2019
Join Our Newsletter. No Spam, Only the good stuff.
Sign up to receive the latest update from our blog.