androidpaginationandroid-jetpackmulti-select

Android Jetpack: How to to multi selection in recycler view with PagingDataAdapter?


I would like to do multi selection in my android app. I have done it before, but only with an ArrayAdapter.

I have a Flow as my DataSet and I use a PagingDataAdapter with a ViewHolder.

My question is, how to do multi selection if the dataset ist not just a List and i can't really access it that easy.

If your want to lookup the code, rewiew this:

Fragment Adapter ViewHolder ViewModel


Solution

  • I have implemented a custom way of doing it.

    I used an observable list in the adapter and expose methods to the viewholders to select themselfs.

    You could of course create a base class for this. I should eventually do that too :)

    My Code:

    Adapter

    class PhotoAdapter(
        private val context: Context,
        private val photoRepository: PhotoRepository,
        private val viewPhotoCallback: KFunction1<Int, Unit>,
        val lifecycleOwner: LifecycleOwner
    ) : PagingDataAdapter<Photo, PhotoItemViewHolder>(differCallback) {
    
        /**
         * Holds the layout positions of the selected items.
         */
        val selectedItems = ObservableArrayList<Int>()
    
        /**
         * Holds a Boolean indicating if multi selection is enabled. In a LiveData.
         */
        var isMultiSelectMode: MutableLiveData<Boolean> = MutableLiveData(false)
    
        override fun onBindViewHolder(holderItem: PhotoItemViewHolder, position: Int) {
            holderItem.bindTo(this, getItem(position))
        }
    
        override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): PhotoItemViewHolder =
            PhotoItemViewHolder(parent, context, photoRepository)
    
        /**
         * Called by ui. On Click.
         */
        fun viewPhoto(position: Int) {
            viewPhotoCallback.invoke(getItem(position)?.id!!)
        }
    
        /**
         * Disables multi selection.
         */
        fun disableSelection() {
            selectedItems.clear()
            isMultiSelectMode.postValue(false)
        }
    
        /**
         * Enables multi selection.
         */
        fun enableSelection() {
            isMultiSelectMode.postValue(true)
        }
    
        /**
         * Add an item it the selection.
         */
        fun addItemToSelection(position: Int): Boolean = selectedItems.add(position)
    
        /**
         * Remove an item to the selection.
         */
        fun removeItemFromSelection(position: Int) = selectedItems.remove(position)
    
        /**
         * Indicate if an item is already selected.
         */
        fun isItemSelected(position: Int) = selectedItems.contains(position)
    
        /**
         * Indicate if an item is the last selected.
         */
        fun isLastSelectedItem(position: Int) = isItemSelected(position) && selectedItems.size == 1
    
        /**
         * Select all items.
         */
        fun selectAll() {
            for (i in 0 until itemCount) {
                if (!isItemSelected(i)) {
                    addItemToSelection(i)
                }
            }
        }
    
        /**
         * Get all items that are selected.
         */
        fun getAllSelected(): List<Photo> {
            val items = mutableListOf<Photo>()
            for(position in selectedItems) {
                val photo = getItem(position)
                if (photo != null) {
                    items.add(photo)
                }
            }
            return items
        }
    
        companion object {
            private val differCallback = object : DiffUtil.ItemCallback<Photo>() {
    
                override fun areItemsTheSame(oldItem: Photo, newItem: Photo): Boolean =
                    oldItem.id == newItem.id
    
                override fun areContentsTheSame(oldItem: Photo, newItem: Photo): Boolean =
                    oldItem == newItem
    
            }
        }
    
    }
    

    ViewHolder

    class PhotoItemViewHolder(
        parent: ViewGroup,
        private val context: Context,
        private val photoRepository: PhotoRepository
    ) : RecyclerView.ViewHolder(
        LayoutInflater.from(parent.context).inflate(R.layout.photo_item, parent, false)
    ) {
        private val imageView: ImageView = itemView.findViewById(R.id.photoItemImageView)
        private val checkBox: CheckBox = itemView.findViewById(R.id.photoItemCheckBox)
    
        var photo: Photo? = null
        private lateinit var adapter: PhotoAdapter
    
        /**
         * Binds the parent adapter and the photo to the ViewHolder.
         */
        fun bindTo(adapter: PhotoAdapter, photo: Photo?) {
            this.photo = photo
            this.adapter = adapter
            imageView.setOnClickListener {
                if (adapter.isMultiSelectMode.value!!) {
                    // If the item clicked is the last selected item
                    if (adapter.isLastSelectedItem(layoutPosition)) {
                        adapter.disableSelection()
                        return@setOnClickListener
                    }
                    // Set checked if not already checked
                    setItemChecked(!adapter.isItemSelected(layoutPosition))
                } else {
                    adapter.viewPhoto(layoutPosition)
                }
            }
    
            imageView.setOnLongClickListener {
                if (!adapter.isMultiSelectMode.value!!) {
                    adapter.enableSelection()
                    setItemChecked(true)
                }
                true
            }
    
            adapter.isMultiSelectMode.observe(adapter.lifecycleOwner, {
                if (it) { // When selection gets enabled, show the checkbox
                    checkBox.show()
                } else {
                    checkBox.hide()
                }
            })
    
            adapter.selectedItems.addOnListChangedCallback(onSelectedItemsChanged)
    
            listChanged()
            loadThumbnail()
        }
    
        /**
         * Listener for changes in selected images.
         * Calls [listChanged] whatever happens.
         */
        private val onSelectedItemsChanged =
            object : ObservableList.OnListChangedCallback<ObservableList<Int>>() {
    
                override fun onChanged(sender: ObservableList<Int>?) {
                    listChanged()
                }
    
                override fun onItemRangeChanged(
                    sender: ObservableList<Int>?,
                    positionStart: Int,
                    itemCount: Int
                ) {
                    listChanged()
                }
    
                override fun onItemRangeInserted(
                    sender: ObservableList<Int>?,
                    positionStart: Int,
                    itemCount: Int
                ) {
                    listChanged()
                }
    
                override fun onItemRangeMoved(
                    sender: ObservableList<Int>?,
                    fromPosition: Int,
                    toPosition: Int,
                    itemCount: Int
                ) {
                    listChanged()
                }
    
                override fun onItemRangeRemoved(
                    sender: ObservableList<Int>?,
                    positionStart: Int,
                    itemCount: Int
                ) {
                    listChanged()
                }
    
            }
    
        private fun listChanged() {
            val isSelected = adapter.isItemSelected(layoutPosition)
            val padding = if (isSelected) 20 else 0
    
            checkBox.isChecked = isSelected
            imageView.setPadding(padding)
        }
    
        private fun setItemChecked(checked: Boolean) {
            layoutPosition.let {
                if (checked) {
                    adapter.addItemToSelection(it)
                } else {
                    adapter.removeItemFromSelection(it)
                }
            }
        }
    
        /**
         * Load the thumbnail for the [photo].
         */
        private fun loadThumbnail() {
            GlobalScope.launch(Dispatchers.IO) {
                val thumbnailBytes =
                    photoRepository.readPhotoThumbnailFromInternal(context, photo?.id!!)
                if (thumbnailBytes == null) {
                    Timber.d("Error loading thumbnail for photo: $photo.id")
                    return@launch
                }
                val thumbnailBitmap =
                    BitmapFactory.decodeByteArray(thumbnailBytes, 0, thumbnailBytes.size)
                runOnMain { // Set thumbnail in main thread
                    imageView.setImageBitmap(thumbnailBitmap)
                }
            }
        }
    }