Home » Android » android – Best practice using RecyclerView, ListAdapter and ViewModel for saving checked status-Exceptionshub

android – Best practice using RecyclerView, ListAdapter and ViewModel for saving checked status-Exceptionshub

Posted by: admin February 26, 2020 Leave a comment

Questions:

I am using RecyclerView with ListAdapter which using DiffUtil internally to show the data. Of course, all the data was provided by ViewModel via LiveData.

Now each item view has a CheckBox, meanwhile I implemented drag selection function by adding RecyclerView.SimpleOnItemTouchListener. Selection status is now maintained by the adapter, it is not a good idea since status will lost after screen rotation.

I want to let ViewModel save the selection status and it is my idea:

  1. Fragment will call the ViewModel to change the status when receiving the relevant callback.
  2. ViewModel will change the data list (or create a new one if it is not mutable) and post to LiveData.
  3. Fragment receives the data change and submits it to ListAdapter.

The problem is that the selection status can change very frequently if user acitvate drag selection. DifferUtil has to spend a long time comparing if there are a lot of data. Unlike saving status inside the adapter, now I cannot notify item changed accurately because I don’t know which position is changed.

The core idea of MVVM is data-driven view, but in this case we lost additional information(e.g. position) causing extra calculations. So what is the best practice?

Part of the adapter code (based on GroupListAdapter which extend ListAdapter):

    private val selectedGroups = mutableSetOf<Int>()
    private val selectedImages = mutableSetOf<Image>()

    override fun onBindGroupViewHolder(holder: StubViewHolder, groupIndex: Int, flattedPosition: Int) {
        holder.setText(R.id.itemImageTitleTextView, getGroupItem(groupIndex).title)
    }

    override fun onBindChildViewHolder(holder: StubViewHolder, groupIndex: Int, childIndex: Int, flattedPosition: Int) {
        val data = getChildItem(groupIndex, childIndex)
        GlideApp.with(fragment).load(data.uri).into(holder.getView(R.id.itemImageView))
    }

    override fun onBindGroupViewHolder(holder: StubViewHolder, groupIndex: Int, flattedPosition: Int,
                                       payloads: MutableList<Any>) {
        holder.setGroupSelectionMode(isInSelectMode())
        holder.setGroupChecked(selectedGroups.contains(flattedPosition))
    }

    override fun onBindChildViewHolder(holder: StubViewHolder, groupIndex: Int, childIndex: Int, flattedPosition: Int,
                                       payloads: MutableList<Any>) {
        if (isInSelectMode()) {
            holder.setImageSelectionMode(true)
            if (selectedImages.contains(getItem(flattedPosition))) {
                holder.getView<MaskImageView>(R.id.itemImageView).isChecked = true
                holder.setImageChecked(true)
            } else {
                holder.getView<MaskImageView>(R.id.itemImageView).isChecked = false
                holder.setImageChecked(false)
            }
        } else {
            holder.getView<MaskImageView>(R.id.itemImageView).isChecked = false
            holder.setImageSelectionMode(false)
        }
    }

    fun setSelected(position: Int, isSelected: Boolean, refreshItem: Boolean = true) {
        if (!isChildItem(position)) {
            return
        }
        if (isSelected) {
            selectedImages.add(getItem(position) as Image)
        } else {
            selectedImages.remove(getItem(position))
        }
        if (refreshItem) {
            notifyItemChanged(position, true)
        }
        onSelectChangedCallback?.invoke(selectedImages.size)
    }
How to&Answers:

May be I’m mistaken, but looks like you are not using paging? Depending on your data nature, you could use any descendant datasources of https://developer.android.com/reference/androidx/paging/DataSource.html. Your DiffUtils would then operate on much smaller subset of data. You could offset selected items (I’m assuming it can be more then one) to viewmodel, keep them as state, and even dynamically wrap your original data into some wrapper that has indication of selection – inside datasource itself. Thus your adapter would be purely data driven.