Android RecyclerView ページングリスナーを自作する

今回はRecyclerViewのページングリスナーを自作したので共有します。
どうしてJetpack Paging Libraryを使わないのかという経緯もご説明します。

## コード

とりあえず全体のコードを先に載せてしまいます。
すべてのLayoutManagerに対応しています。
特にStaggeredGridLayoutManagerは結構苦労しました。

- LinearLayoutManager
- GridLayoutManager
- StaggeredGridLayoutManager

また、任意にページング取得開始位置なども設定できるのでよしなに変えてください。
あとは、recyclerViewのaddOnScrollListenerで追加してあげます。

```kotlin
import androidx.recyclerview.widget.GridLayoutManager
import androidx.recyclerview.widget.LinearLayoutManager
import androidx.recyclerview.widget.RecyclerView
import androidx.recyclerview.widget.StaggeredGridLayoutManager

/**
 * RecyclerViewのページングリスナー
 */
class RecyclerViewPagingListener(
    layoutManager: RecyclerView.LayoutManager,
    direction: LoadOnScrollDirection
) : RecyclerView.OnScrollListener() {

    /**
     * 追加読み込みリスナー
     */
    private var onLoadMoreListener: ((page: Int, total: Int) -> Unit)? = null

    /**
     * 現在読み込んだページ数
     */
    private var pendingCurrentPage = 0

    /**
     * 現在読み込んだアイテム数
     */
    private var pendingTotalItemCount = 0

    /**
     * ロード中フラグ
     */
    private var loading = true

    /**
     * 検知する方向
     */
    private val pendingDirection: LoadOnScrollDirection

    /**
     * LinearLayoutManager
     */
    private var pendingLinearLayoutManager: LinearLayoutManager? = null

    /**
     * GridLayoutManager
     */
    private var pendingGridLayoutManager: GridLayoutManager? = null

    /**
     * StaggeredGridLayoutManager
     */
    private var pendingStaggeredGridLayoutManager: StaggeredGridLayoutManager? = null

    init {
        // パフォーマンスのためonScrolledでキャストしない
        when (layoutManager) {
            is LinearLayoutManager -> {
                pendingLinearLayoutManager = layoutManager
            }
            is GridLayoutManager -> {
                pendingGridLayoutManager = layoutManager
            }
            is StaggeredGridLayoutManager -> {
                pendingStaggeredGridLayoutManager = layoutManager
            }
            else -> {
                throw IllegalArgumentException("unsupported this layout manager.")
            }
        }
        pendingDirection = direction
    }

    override fun onScrolled(view: RecyclerView, dx: Int, dy: Int) {
        var lastVisibleItemPosition = 0
        var firstVisibleItemPosition = 0
        var totalItemCount = 0
        when {
            pendingLinearLayoutManager != null -> {
                val layoutManager = requireNotNull(pendingLinearLayoutManager)
                totalItemCount = layoutManager.itemCount
                lastVisibleItemPosition = layoutManager.findLastVisibleItemPosition()
                firstVisibleItemPosition = layoutManager.findFirstVisibleItemPosition()
            }
            pendingGridLayoutManager != null -> {
                val layoutManager = requireNotNull(pendingGridLayoutManager)
                totalItemCount = layoutManager.itemCount
                lastVisibleItemPosition = layoutManager.findLastVisibleItemPosition()
                firstVisibleItemPosition = layoutManager.findFirstVisibleItemPosition()
            }
            pendingStaggeredGridLayoutManager != null -> {
                val layoutManager = requireNotNull(pendingStaggeredGridLayoutManager)
                val lastVisibleItemPositions = layoutManager.findLastVisibleItemPositions(null)
                val firstVisibleItemPositions = layoutManager.findFirstVisibleItemPositions(null)
                totalItemCount = layoutManager.itemCount
                lastVisibleItemPosition = getLastVisibleItem(lastVisibleItemPositions)
                firstVisibleItemPosition = getFirstVisibleItem(firstVisibleItemPositions)
            }
        }
        when (pendingDirection) {
            LoadOnScrollDirection.BOTTOM -> {
                if (totalItemCount < pendingTotalItemCount) {
                    pendingCurrentPage = STARTING_PAGE_INDEX
                    pendingTotalItemCount = totalItemCount
                    if (totalItemCount == 0) {
                        loading = true
                    }
                }
                if (loading && totalItemCount > pendingTotalItemCount) {
                    loading = false
                    pendingTotalItemCount = totalItemCount
                }
                if (!loading && lastVisibleItemPosition + PREFETCH_ITEM_POSITION > totalItemCount) {
                    pendingCurrentPage++
                    onLoadMoreListener?.invoke(pendingCurrentPage, totalItemCount)
                    loading = true
                }
            }
            LoadOnScrollDirection.TOP -> {
                if (totalItemCount < pendingTotalItemCount) {
                    pendingCurrentPage = STARTING_PAGE_INDEX
                    pendingTotalItemCount = totalItemCount
                    if (totalItemCount == 0) {
                        loading = true
                    }
                }
                if (loading && totalItemCount > pendingTotalItemCount) {
                    loading = false
                    pendingTotalItemCount = totalItemCount
                }
                if (!loading && firstVisibleItemPosition < PREFETCH_ITEM_POSITION) {
                    pendingCurrentPage++
                    onLoadMoreListener?.invoke(pendingCurrentPage, totalItemCount)
                    loading = true
                }
            }
        }
    }

    private fun getLastVisibleItem(lastVisibleItemPositions: IntArray): Int {
        var maxSize = 0
        for (i in lastVisibleItemPositions.indices) {
            if (i == 0) {
                maxSize = lastVisibleItemPositions[i]
            } else if (lastVisibleItemPositions[i] > maxSize) {
                maxSize = lastVisibleItemPositions[i]
            }
        }
        return maxSize
    }

    private fun getFirstVisibleItem(firstVisibleItemPositions: IntArray): Int {
        var maxSize = 0
        for (i in firstVisibleItemPositions.indices) {
            if (i == 0) {
                maxSize = firstVisibleItemPositions[i]
            } else if (firstVisibleItemPositions[i] > maxSize) {
                maxSize = firstVisibleItemPositions[i]
            }
        }
        return maxSize
    }

    /**
     * 追加読み込み
     *
     * @param block 現在のページ数, 現在のアイテム数を通知するブロック構文
     */
    fun setOnLoadMoreListener(block: (page: Int, total: Int) -> Unit) {
        onLoadMoreListener = block
    }

    /**
     * リセット
     * PullToRefresh時に行う
     */
    fun reset() {
        pendingCurrentPage = 0
        pendingTotalItemCount = 0
    }

    /**
     * スクロール検知方向
     */
    enum class LoadOnScrollDirection {
        TOP, BOTTOM
    }

    companion object {
        /**
         * 開始ページ
         */
        private const val STARTING_PAGE_INDEX = 0

        /**
         * プリフェッチする位置
         *
         * "5"に設定した場合
         * "100"アイテムあると"95"が見えた時にロード開始する
         */
        private const val PREFETCH_ITEM_POSITION = 10
    }
}
```

## Jetpack Paging Libraryを使わない経緯

Jetpack Paging Libraryは便利です。
実際に上記のコードを実装しなくてもページングを実現できます。

しかし、アイテムの要素を更新したい要望を実現するにはJetpack Roomを同時に使う必要があります。
ページングのためだけにデータ層まで変えないといけないのか？？

これが非常に重荷だったので、今回は独自に作ることにしました。
とりあえず取得だけして表示だけするようなリストならJetpack Paging Libraryでも問題ないと思います。


以上です〜