A few additions to the existing answer:
- Treat your Adapter data as non-mutable. Do NOT mutate data inside the adapter. The Adapter needs to well... adapt data from a Model into a View (Holder). It keeps track of where each data belongs in terms of positioning, and that's pretty much all it needs to do (aside from inflating the views naturally).
- If you supply a list of
Thing, where isSelected is not part of the model, but rather part of your need to keep track of whether the user has selected it or not, then create a simple data class like
data class ThingSelection(val thing: Thing, val isSelected: Boolean)
And use that in your adapter instead of just a List<Thing>, use List<ThingSelection>.
- Do not do what you do in the
onBindViewHolder. All you should do there is
val item = getItem(position)
holder.bind(item)
It's the ViewHolder's job to set its properties, background color, etc. based on your business rules.
If your "item" is a ThingSelection then the viewHolder's bind method can do
if (item.isSelected) { ... }
Pass anything else you need to this method as well, bind(...) is whatever you need it to be.
What to do when the user changes the item selection?
You have a click listener, but don't mutate data, have your callback/listener pass what happened to the caller.
I imagine in your viewHolder bind method you'd do something like:
view.setOnClickListener {
externalListenerThatTheAdapterReceivedFromTheOutside.onThingClicked(thing, thing.isSelected)
}
It's this external listener's responsibility to:
Do what it takes (possibly pass it directly to a ViewModel so it work on this new event), to ensure the adapter receives the new data (now where the Thing.isSelected is correct given what the user did).
Ensure the new immutable list is supplied to the adapter so the adapter can compute its new data and update (hint: use ListAdapter<T, K> with a DiffUtil.ItemCallback<T>, it makes your life easier.
UPDATE: Data Class
data class ItemWithSelection(val item: Item, val isSelected: Boolean)
Then change your adapter to be:
class YourAdapter(): ListAdapter<ItemWithSelection, YourViewHolder>(DiffUtilCallback()) {
// You only need to override these
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): YourViewHolder {
val view = LayoutInflater.from(parent.context).inflate(R.layout.your_item_layout, parent, false) // or use ViewBinding, whatever.
return YourViewHolder(view) // you can pass more stuff if you need here, like a listener...
}
override fun onBindViewHolder(holder: YourViewHolder, position: Int) {
val item = getItem(position)
holder.bindData(item, item.isSelected) //Item is of type ItemWithSelection...
}
}
// Somewhere in your ViewHolder...
fun bindData(item: ItemWithSelection, isSelected: Boolean) {
// do what it needs, e.g.:
if (isSelected) { ... }
}
// You also need a DiffUtilCallback...
internal class DiffUtil.ItemCallback<ItemWithSelection>() {
internal class DiffUtilCallback : DiffUtil.ItemCallback<ItemWithSelection>() {
override fun areItemsTheSame(oldItem: ItemWithSelection, newItem: ItemWithSelection) = oldItem.item.xxx == newItem.item.xxx
override fun areContentsTheSame(oldItem: ItemWithSelection, newItem: ItemWithSelection) = oldItem == newItem
}
With all that wired up...
In your ViewModel or "layer where you get the data" (most likely not the fragment/activity) provide a List<ItemWithSelection>...
How to construct this really depends on how you store what is selected.
But let's say you have two lists for the sake of simplicity:
val all: List<Item>
val selected: List<Item>
When it's time to produce a list for the UI, you could (note: I'm gonna assume you use Coroutines... you pick your own flavor of asynchronous/reactive programming:
class YourViewModel: ViewModel() {
fun getYourListWithSelections() {
viewModelScope.launch(Dispatchers.Computation) {//for e.g.
val tempSelection = mutableListOf<ItemWithSelection>()
all.mapTo(tempSelection) {
ItemWithSelection(item = it, isSelected = selection.contains(it))
}
// Assuming you have a MutableLiveData somewhere in this viewmodel...
_uiState.postValue(SomeSealedClass.ListAvailable(tempSelection))
}
}
This is naturally observed from the Fragment...
class YourFragment: Fragment() {
fun observe() {
viewModel.uiState.observe(viewLifecycleOwner, { viewState ->
when (viewState) {
is SomeSealedClass.ListAvailable -> adapter.submitList(viewState.items.toMutableList())//need to pass a new list or ListAdapter won't refresh the list.
else ... //exercise for the reader ;)
}
})
}
}
All that is really missing here are the boiler plate stuff.
- How does the ViewHolder (which sets a
click listener on the View pass the info back to the ViewModel?
Supply your adapter with a Listener of your choice:
interface ItemClickListener {
fun onItemClicked(val item: Item, val isSelected: Boolean)
}
Implement that and pass it to your adapter when you create it:
class YourAdapter(private val listener: ItemClickListener): ListAdapter<ItemWithSelection, YourViewHolder>(DiffUtilCallback()) {
And pass it in your fragment:
class YourFragment: Fragment() {
private val listener = object : ItemClickListener {
fun onItemClicked(val item: Item, val isSelected: Boolean) {
// pass it directly, it's the VM job to deal with this and produce a new list via LiveData.
viewModel.onItemClicked(item, isSelected)
}
}
val adapter = YourAdapter(listener)
Hope that helps.