Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

How to implement paging in Leanback VerticalGridSupportFragment using Paging Library?

We are trying to implement paging in Leanback VerticalGridSupportFragment with Architecture Components Paging Library. Leanback on it's own doesn't have any sort of out-of-box compatibility with Paging Library so we extended it's ObjectAdapter class and managed to implement append and clear operations quite easily but we are having a hard time trying to make modify operation work. During content modification operation, Paging Library's PagedList class computes the diff using AsyncPagedListDiffer which internally uses PagedStorageDiffHelper which is a package-private class and it internally uses package-private PagedStorage field of PagedList to get access to actual underlying data. Thus we can't implement the same logic as Paging Library uses internally because of visibility restrictions. We are looking for a clean and clever way to make Leanback work together with Paging without extracting and modifying internals of any of the two. This is our implementation of ObjectAdapter which supports appending and clearing data but does not support content modification.

Has anybody ever managed to implement paging in Leanback through Paging Library?

class LeanbackVerticalGridPagedListAdapter<T>(
    presenter: Presenter,
    private val stubItem: T
) : ObjectAdapter(presenter) {

    private val mUpdateCallback = object : ListUpdateCallback {

        override fun onInserted(position: Int, count: Int) {
            notifyItemRangeInserted(position, count)
        }

        override fun onRemoved(position: Int, count: Int) {
            notifyItemRangeRemoved(position, count)
        }

        override fun onMoved(fromPosition: Int, toPosition: Int) {
            notifyItemMoved(fromPosition, toPosition)
        }

        override fun onChanged(position: Int, count: Int, payload: Any?) {
            notifyItemRangeChanged(position, count, payload)
        }
    }

    private var mPagedList: PagedList<T>? = null
    private var mSnapshot: PagedList<T>? = null

    private val mPagedListCallback = object : PagedList.Callback() {
        override fun onInserted(position: Int, count: Int) {
            mUpdateCallback.onInserted(position, count)
        }

        override fun onRemoved(position: Int, count: Int) {
            mUpdateCallback.onRemoved(position, count)
        }

        override fun onChanged(position: Int, count: Int) {
            mUpdateCallback.onChanged(position, count, null)
        }
    }

    override fun size(): Int =
        mPagedList?.size
            ?: mSnapshot?.size
            ?: 0

    override fun get(index: Int): T? =
        mPagedList?.let {
            it.loadAround(index)
            it[index] ?: stubItem
        } ?: mSnapshot?.let {
            it[index]
        } ?: throw IndexOutOfBoundsException("Item count is zero, getItem() call is invalid")

    fun submitList(pagedList: PagedList<T>?) {
        if (pagedList == null) {
            val removedCount = size()
            if (mPagedList != null) {
                mPagedList!!.removeWeakCallback(mPagedListCallback)
                mPagedList = null
            } else if (mSnapshot != null) {
                mSnapshot = null
            }
            // dispatch update callback after updating mPagedList/mSnapshot
            mUpdateCallback.onRemoved(0, removedCount)
            return
        }

        if (mPagedList == null && mSnapshot == null) {
            // fast simple first insert
            mPagedList = pagedList
            pagedList.addWeakCallback(null, mPagedListCallback)

            // dispatch update callback after updating mPagedList/mSnapshot
            mUpdateCallback.onInserted(0, pagedList.size)
            return
        }

        if (mPagedList != null) {
            // first update scheduled on this list, so capture mPages as a snapshot, removing
            // callbacks so we don't have resolve to updates against a moving target
            mPagedList!!.removeWeakCallback(mPagedListCallback)
            mSnapshot = mPagedList!!.snapshot() as PagedList<T>
            mPagedList = null
        }

        if (mSnapshot == null || mPagedList != null) {
            DevUtil.crashDuringDevelopment(IllegalStateException("must be in snapshot state to diff"))
        }
    }
}
like image 229
Wojciech Sadurski Avatar asked May 09 '19 08:05

Wojciech Sadurski


1 Answers

If someone is still looking for it, got it working by porting PagedListAdapter:

import androidx.leanback.widget.ArrayObjectAdapter
import androidx.leanback.widget.Presenter
import androidx.leanback.widget.PresenterSelector
import androidx.paging.AsyncPagedListDiffer
import androidx.paging.PagedList
import androidx.recyclerview.widget.AsyncDifferConfig
import androidx.recyclerview.widget.DiffUtil
import androidx.recyclerview.widget.ListUpdateCallback

/**
 * Base adapter class from leanback's ArrayObjectAdapter which supports paging
 * @link androidx.paging.PagedListAdapter
 * Use
 * @link
 * #submitList
 * methods to submit PagedList returned by paging library
 */
@Suppress("unused")
class PagedArrayObjectAdapter<T> : ArrayObjectAdapter, ListUpdateCallback {

    private val logTag = PagedArrayObjectAdapter::class.java.name
    private val differ: AsyncPagedListDiffer<T>
    private val listener: AsyncPagedListDiffer.PagedListListener<T> =
        AsyncPagedListDiffer.PagedListListener { previousList, currentList ->
            @Suppress("DEPRECATION")
            onCurrentListChanged(currentList)
            onCurrentListChanged(previousList, currentList)
        }

    constructor(diffCallback: DiffUtil.ItemCallback<T>) : this(
        AsyncDifferConfig.Builder<T>(diffCallback).build()
    )

    constructor(config: AsyncDifferConfig<T>) : super() {
        differ = AsyncPagedListDiffer(this, config)
        differ.addPagedListListener(listener)
    }

    constructor(
        presenter: Presenter,
        diffCallback: DiffUtil.ItemCallback<T>
    ) : this(
        presenter,
        AsyncDifferConfig.Builder<T>(diffCallback).build()
    )

    constructor(
        presenter: Presenter,
        config: AsyncDifferConfig<T>
    ) : super(presenter) {
        differ = AsyncPagedListDiffer(this, config)
        differ.addPagedListListener(listener)
    }

    constructor(
        presenterSelector: PresenterSelector,
        diffCallback: DiffUtil.ItemCallback<T>
    ) : this(
        presenterSelector,
        AsyncDifferConfig.Builder<T>(diffCallback).build()
    )

    constructor(
        presenterSelector: PresenterSelector,
        config: AsyncDifferConfig<T>
    ) : super(
        presenterSelector
    ) {
        differ = AsyncPagedListDiffer(this, config)
        differ.addPagedListListener(listener)
    }

    /**
     * @link
     * ListUpdateCallback#onInserted
     */
    override fun onInserted(position: Int, count: Int) {
        notifyItemRangeInserted(position, count)
    }

    /**
     * @link
     * ListUpdateCallback#onRemoved
     */
    override fun onRemoved(position: Int, count: Int) {
        notifyItemRangeRemoved(position, count)
    }

    /**
     * @link
     * ListUpdateCallback#onMoved
     */
    override fun onMoved(fromPosition: Int, toPosition: Int) {
        notifyItemMoved(fromPosition, toPosition)
    }

    /**
     * @link
     * ListUpdateCallback#onChanged
     */
    override fun onChanged(position: Int, count: Int, payload: Any?) {
        notifyItemRangeChanged(position, count, payload)
    }

    /**
     * Set the new list to be displayed.
     *
     *
     * If a list is already being displayed, a diff will be computed on a background thread, which
     * will dispatch Adapter.notifyItem events on the main thread.
     *
     * @param pagedList The new list to be displayed.
     */
    fun submitList(pagedList: PagedList<T>?) {
        differ.submitList(pagedList)
    }

    /**
     * Set the new list to be displayed.
     *
     *
     * If a list is already being displayed, a diff will be computed on a background thread, which
     * will dispatch Adapter.notifyItem events on the main thread.
     *
     *
     * The commit callback can be used to know when the PagedList is committed, but note that it
     * may not be executed. If PagedList B is submitted immediately after PagedList A, and is
     * committed directly, the callback associated with PagedList A will not be run.
     *
     * @param pagedList The new list to be displayed.
     * @param commitCallback Optional runnable that is executed when the PagedList is committed, if
     * it is committed.
     */
    fun submitList(pagedList: PagedList<T>?, commitCallback: Runnable?) {
        differ.submitList(pagedList, commitCallback)
    }


    /**
     * @link
     * ArrayObjectAdapter#get
     */
    override fun get(index: Int): T? {
        return differ.getItem(index)
    }

    /**
     * @link
     * ArrayObjectAdapter#size
     */
    override fun size(): Int {
        return differ.itemCount
    }

    /**
     * Returns the PagedList currently being displayed by the Adapter.
     * <p>
     * This is not necessarily the most recent list passed to
     * @link
     * #submitList(PagedList)
     * because a diff is computed asynchronously between the new list and the current list before
     * updating the currentList value. May be null if no PagedList is being presented.
     *
     * @return The list currently being displayed.
     *
     * @link
     * #onCurrentListChanged(PagedList, PagedList)
     */
    fun getCurrentList(): PagedList<T>? = differ.currentList

    @Deprecated("")
    fun onCurrentListChanged(currentList: PagedList<T>?) {
    }

    @Suppress("MemberVisibilityCanBePrivate", "UNUSED_PARAMETER")
    fun onCurrentListChanged(previousList: PagedList<T>?, currentList: PagedList<T>?) {
    }
}
like image 200
Swapnil Bhoite Avatar answered Oct 13 '22 12:10

Swapnil Bhoite