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!
Top comments (0)