3

I'm trying to use Selection ( https://developer.android.com/reference/androidx/recyclerview/selection/package-summary ) with the new paging library ( so paging 3 )

With Paging 2 that was doable because we were using PagedListAdapter and that was working will with the Selection library but now with the new PagingDataAdapter I can't make it work anymore.

https://developer.android.com/reference/kotlin/androidx/paging/PagingDataAdapter

We are losing getCurrentList(), setHasStableIds() will now return UnsupportedOperationException.

So if anyone have it woring I would greatly appreciate some help on this.

Neodigi
  • 193
  • 4
  • 19

3 Answers3

2

In the new Paging3 library, you can get the currentlist with the snapShot() method of the PagingDataAdapter class.

I recently had the same key provider problem so I implemented a custom one like so:

class MyItemKeyProvider(private val adapter: MyPagingAdapter) :
    ItemKeyProvider<String>(SCOPE_CACHED) {
    override fun getKey(position: Int): String = adapter.snapshot().items[position].id
    override fun getPosition(key: String): Int = adapter.snapshot().items.indexOfFirst { it.id == key }
Joseph Geoffery
  • 676
  • 8
  • 6
  • and how did you make that work with ItemDetailsLookup? Because we don't have it the item to get the id. Only the recyclerview – Caique Oct 14 '22 at 18:35
0

If you already have it working with Paging 2 I would suggest starting by implementing your own ItemKeyProvider<K>. The packaged StableIdKeyProvider can not be used since it requires enabling of stable IDs and, as you stated, stable IDs are not supported with PagingDataAdapter.

Dharman
  • 30,962
  • 25
  • 85
  • 135
StPatrick
  • 190
  • 3
  • 8
0

Paging v3 does not work with recyclerview-selection out-of-the box because selection library does use stable ids, but paging v3 does not support them.

To make them work with each other you need to implement your own stable id key provider. It holds item ids once they bound to recycler view.

class MyStableIdKeyProvider(
    private val mRecyclerView: RecyclerView
) : ItemKeyProvider<Long>(SCOPE_CACHED) {

    init {
        mRecyclerView.addOnChildAttachStateChangeListener(
            object : RecyclerView.OnChildAttachStateChangeListener {
                override fun onChildViewAttachedToWindow(view: View) {
                    onAttached(view)
                }

                override fun onChildViewDetachedFromWindow(view: View) {
                    onDetached(view)
                }
            }
        )
    }

    private val mPositionToKey = mutableMapOf<Int, Long>()
    private val mKeyToPosition = mutableMapOf<Long, Int>()

    fun  /* synthetic access */onAttached(view: View) {
        val holder = mRecyclerView.findContainingViewHolder(view)
        if (holder == null) {
            Log.w(TAG, "Unable to find ViewHolder for View. Ignoring onAttached event.")
            return
        }
        val position = holder.bindingAdapterPosition
        val myItemId = (holder as ItemVH).myItemId
        if (position != RecyclerView.NO_POSITION && myItemId != null) {
            mPositionToKey[position] = myItemId
            mKeyToPosition[myItemId] = position
        }
    }

    fun  /* synthetic access */onDetached(view: View) {
        val holder = mRecyclerView.findContainingViewHolder(view)
        if (holder == null) {
            Log.w(TAG, "Unable to find ViewHolder for View. Ignoring onDetached event.")
            return
        }
        val position = holder.bindingAdapterPosition
        val myItemId = (holder as ItemVH).myItemId
        if (position != RecyclerView.NO_POSITION && myItemId != null) {
            mPositionToKey.remove(position)
            mKeyToPosition.remove(myItemId)
        }
    }

    override fun getKey(position: Int): Long? = mPositionToKey[position]

    override fun getPosition(key: Long): Int = mKeyToPosition[key] ?: RecyclerView.NO_POSITION

    companion object {
        private const val TAG = "MyKeyProvider"
    }
}

Your ItemVH has to provide its id. You can't use getItemId() because it always returns NO_ID.

class ItemVH(
    view: View,
    selectionTracker: SelectionTracker<Long>,
) : RecyclerView.ViewHolder(view) {

    protected var myItem: MyItem? = null
    val myItemId: Long? get() = myItem?.id

    fun bind(myItem: MyItem, position: Int) {

        this.myItem = myItem
        
        // Put data to item's views
        // ..
    
        // Update checkmark visibility passing `myItemId` to the selection tracker
        if (selectionTracker.isSelected(myItemId)) {
            selectedView.visibility = View.VISIBLE
        } else {
            selectedView.visibility = View.GONE
        }

    }

    fun getItemDetails() = object: ItemDetailsLookup.ItemDetails<Long>() {

        override fun getPosition() = bindingAdapterPosition

        override fun getSelectionKey() = myItemId
    }

    fun clear() {
        myItem = null
    }
}

Adapter is quite simple

class MyItemListAdapter : PagingDataAdapter<MyItem, ItemVH>(DIFF_CALLBACK) {

    lateinit var selectionTracker: SelectionTracker<Long>

    override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ItemVH {
        val view = LayoutInflater.from(parent.context).inflate(R.layout.item, parent, false)
        return ItemVH(view, selectionTracker)
    }

    override fun onBindViewHolder(viewHolder: ItemVH, position: Int) {
        val myItem = getItem(position)
        if (myItem == null) {
            viewHolder.clear()
        } else {
            viewHolder.bind(myItem, position)
        }
    }
    
    companion object {
        private val DIFF_CALLBACK = object :
            DiffUtil.ItemCallback<MyItem>() {
                // ...
        }
    }
}

Item details lookup is required to build selection tracker:

class ItemListLookup(
    private val rv: RecyclerView
) : ItemDetailsLookup<Long>() {

    override fun getItemDetails(e: MotionEvent): ItemDetails<Long>? {
        val view = rv.findChildViewUnder(e.x, e.y)
        if(view != null) {
            return (rv.getChildViewHolder(view) as ItemVH)
                .getItemDetails()
        }
        return null
    }
}

Fragment ties all together

class MyFragment : Fragment() {

    private val adapter = MyItemListAdapter()
    
    override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
        super.onViewCreated(view, savedInstanceState)

        view.recyclerView.adapter = adapter

        adapter.selectionTracker = SelectionTracker.Builder<Long>(
            "item_selection",
            view.recyclerView,
            MyStableIdKeyProvider(view.recyclerView),
            ItemListLookup(view.recyclerView),
            StorageStrategy.createLongStorage()
        ).build()

        adapter.selectionTracker.addObserver(object : SelectionTracker.SelectionObserver<Long>() {
            override fun onItemStateChanged(key: Long, selected: Boolean) {
                // handle selection change event
            }

            override fun onSelectionCleared() {
                // clear selection
            }
        })
        
        // ...
    }
}
geekyvad
  • 101
  • 3