Search code examples
androidmvvmviewmodellistadapterandroid-diffutils

android ListAdapter selected state using MVVM


I cannot find a good way for selected state in ListAdapter using MVVM architecture. i am also using repository LiveData implemented for firestore by @Doug Stevenson

I am trying to avoid notifyDataSetChanged() since causing noticeable glitch at the UI

During couple of days trying, this is my last attempt which does not work since oldList is saved as reference so the DiffUtil.ItemCallback not detecting any change. It seems like i need to do a deep cloning to the list but i figure there must be a better performance solution.

MainViewModel

class MainViewModel @ViewModelInject constructor(private val glistRepo: GlistRepository): ViewModel() {
  
    private val selected = MutableLiveData<HashMap<String,Glist>>()

    init {
        selected.value = HashMap()
    }

    fun getSelected(): LiveData<HashMap<String,Glist>> {
        return selected
    }

    fun onItemClicked(glist: Glist) {
        selected.value?.set(glist.id, glist)
        selected.value = selected.value //inform observers
    }

    ....
}

MainFragment

viewModel.getSelected().observe(viewLifecycleOwner, Observer {
            adapter.setSelected(it.keys.toList())
        })

RepositoryCommon

For the selected state, i added a sel object but since i do not want to add it to the database, i added to the wrapper interface

interface QueryItem<T> {
    val item: T
    val id: String
    var sel: Boolean //The selected state
}

MainAdapter

class MainAdapter(private val clickListener: MainAdapterListener):
    ListAdapter<QueryItem<Glist>, MainAdapter.ViewHolder>(asyncDifferConfig) {

    private val selected = HashSet<String>()
    private var oldList: List<QueryItem<Glist>>? = ArrayList()

    fun setSelected(list: List<String>) {
        selected.clear()
        selected.addAll(list)
        //Instead of using notifyDataSetChanged() i am trying to trigger the diffUtil. Not working!
        submitList(oldList)
    }

    override fun submitList(list: List<QueryItem<Glist>>?) {
        if (selected.size != 0 && oldList != null && oldList!!.isNotEmpty()) {
            var id: String
            for (i in oldList!!.indices) {
                oldList!![i].sel = false
                id = oldList!![i].id
                 selected.forEach { key ->
                    if (id == key) {
                        oldList!![i].sel = true
                    }
                }
            }
        }

        oldList = list
        super.submitList(oldList)
    }

QueryItemDiffCallback

open class QueryItemDiffCallback<T> : DiffUtil.ItemCallback<QueryItem<T>>() {
    override fun areItemsTheSame(oldItem: QueryItem<T>, newItem: QueryItem<T>): Boolean {
        return (oldItem.id == newItem.id && oldItem.sel == newItem.sel)
    }

    @SuppressLint("DiffUtilEquals")  // equals() is OK for data classes
    override fun areContentsTheSame(oldItem: QueryItem<T>, newItem: QueryItem<T>): Boolean {
        return (oldItem.item == newItem.item && oldItem.sel == newItem.sel)
    }
}

Solution

  • I am not sure that is the best practice or even if it matches MVVM but considering, it was best i could think of. If you have other ideas, please let me know and don't be hurry to downgrade

    In viewmodel i saved the selected list and the last item id changed for easy notifyItemChanged() in MainAdapter. In MainAdapter i inject the selected state in onBindViewHolder using Data Binding variable.

    MainViewModel

    class MainViewModel @ViewModelInject constructor(private val glistRepo: GlistRepository): ViewModel() {
    
    
        private val selected = MutableLiveData<HashMap<String,Glist>>()
        var lastSelectedId = ""
            private set
    
        init {
            selected.value = HashMap()
        }
    
        private fun toggleSelected(glist:Glist) {
            lastSelectedId = glist.id
            if (selected.value!!.containsKey(glist.id)) {
                selected.value?.remove(lastSelectedId)
            } else {
                selected.value?.set(lastSelectedId, glist)
            }
            selected.value = selected.value //Notify observers
        }
    
        fun getSelected(): LiveData<HashMap<String,Glist>> {
            return selected
        }
    
        fun onItemClicked(glist: Glist, short: Boolean) {
            if (!short || selected.value!!.size != 0) {
                toggleSelected(glist)
            } else {
                selected.value?.clear()
                _navigateToItem.value = glist.id
            }
        }
    
        ...
    
    }
    

    MainFragment

    viewModel.getSelected().observe(viewLifecycleOwner, Observer {
                adapter.setSelected(it.keys.toList(),viewModel.lastSelectedId)
            })
    

    MainAdapter

    class MainAdapter(private val clickListener: MainAdapterListener):
        ListAdapter<QueryItem<Glist>, MainAdapter.ViewHolder>(asyncDifferConfig) {
    
        ...
    
        private val selected = HashSet<String>()
    
        fun setSelected(list: List<String>, lastChangedId: String) {
            selected.clear()
            selected.addAll(list)
            for (i in 0 until currentList.size) {
                if (currentList[i].id == lastChangedId) {
                    notifyItemChanged(i)
                    return
                }
            }
        }
    
        override fun onBindViewHolder(holder: ViewHolder, position: Int) {
            val item = getItem(position)
            holder.bind(item,clickListener,selected.contains(item.id))
        }
    
        ...
    
        class ViewHolder private constructor(val binding: ListMainBinding): RecyclerView.ViewHolder(binding.root){
    
            fun bind(item: QueryItem<Glist>, clickListener: MainAdapterListener, isSelected: Boolean) {
                binding.glist = item.item
                binding.clickListener = clickListener
                binding.isSelected = isSelected
                binding.executePendingBindings()
            }
    
            ...
        }
    }