LoginSignup
9
9

More than 3 years have passed since last update.

【Android】RecyclerView で無限スクロールを実装する

Posted at

やりたいこと

RecyclerView で作ったリストに無限スクロールを実装してみます。

API でデータセットを取得してリスト表示するような機能を想定しています。

画面生成時にリストを 10 件しておき、リストの下端までスクロールしたタイミングで 10 件ずつ追加で表示するサンプルアプリを作っていきます。

最終的に作成したアプリは Github で公開していますので参考にしていただければと思います。

実装手順

1. レイアウトファイル作成

RecyclerView のレイアウトと、リストに表示させる1行分のレイアウトを作成します。

activity_main.xml
<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:app="http://schemas.android.com/apk/res-auto"
    xmlns:tools="http://schemas.android.com/tools"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    tools:context=".MainActivity">

    <androidx.recyclerview.widget.RecyclerView
        android:id="@+id/recycler_view"
        android:layout_width="0dp"
        android:layout_height="0dp"
        app:layout_constraintBottom_toBottomOf="parent"
        app:layout_constraintEnd_toEndOf="parent"
        app:layout_constraintStart_toStartOf="parent"
        app:layout_constraintTop_toTopOf="parent" />

    <ProgressBar
        android:id="@+id/progress_bar"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        app:layout_constraintBottom_toBottomOf="parent"
        app:layout_constraintEnd_toEndOf="parent"
        app:layout_constraintStart_toStartOf="parent"
        app:layout_constraintTop_toTopOf="parent"
        android:visibility="invisible"/>

</androidx.constraintlayout.widget.ConstraintLayout>
list_item.xml
<?xml version="1.0" encoding="utf-8"?>
<TextView xmlns:android="http://schemas.android.com/apk/res/android"
    android:id="@+id/list_item"
    android:layout_width="match_parent"
    android:layout_height="80dp"
    android:gravity="center_vertical"
    android:padding="16dp"
    android:textSize="20sp">
</TextView>

2. ビューホルダーとアダプターの作成

リストに表示させるデータを保持するビューホルダーと、ビューホルダーを管理してリストを画面表示するロジックを担うアダプターを作成します。

MyAdapter.kt
class MyAdapter(private val listData: MutableList<String>) : RecyclerView.Adapter<MyAdapter.MyViewHolder>() {

    override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): MyViewHolder {
        val inflater = LayoutInflater.from(parent.context)
        val textView = inflater.inflate(R.layout.list_item, parent, false) as TextView
        return MyViewHolder(textView)
    }

    override fun onBindViewHolder(holder: MyViewHolder, position: Int) {
        holder.textView.text = listData[position]
    }

    override fun getItemCount(): Int {
        return listData.size
    }

    /**
     * リストデータを追加して画面に反映させるメソッド。
     */
    fun add(listData: List<String>) {
        this.listData += listData
        notifyDataSetChanged()
    }

    class MyViewHolder(val textView: TextView): RecyclerView.ViewHolder(textView)
}

MyAdapter クラス内に add() メソッドを作成しています。

リスト下端までスクロールしたタイミングでデータを追加で取得するので、取得したデータをこのメソッドに渡してリストを更新するようにします。

3. 無限スクロールの実装

MainActivity に無限スクロールの実装を行います。

MainActivity.kt
class MainActivity : AppCompatActivity() {

    /**
     * API から取得するデータセットを想定したプロパティ。
     */
    private val dataSet = mutableListOf<String>()

    /**
     * API に問い合わせ中は true になる。
     */
    private var nowLoading = false

    private lateinit var myAdapter: MyAdapter

    private val handler = Handler()

    private val progressBar by lazy { findViewById<ProgressBar>(R.id.progress_bar) }

    init {
        // 仮想データセットを生成。
        for (i in 0..99) { dataSet.add("Number $i") }
    }

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_main)

        val listData = runBlocking { fetch(0) }
        myAdapter = MyAdapter(listData as MutableList<String>)
        findViewById<RecyclerView>(R.id.recycler_view).also {
            it.layoutManager = LinearLayoutManager(this)
            it.adapter = myAdapter
            it.addItemDecoration(DividerItemDecoration(this, DividerItemDecoration.VERTICAL))
            it.addOnScrollListener(InfiniteScrollListener())
        }
    }

    /**
     * API でリストデータを取得することを想定したメソッド。
     */
    private suspend fun fetch(index: Int): List<String> {
        // API 問い合わせの待ち時間を仮想的に作る。
        handler.post { progressBar.visibility = View.VISIBLE }
        delay(3000)
        handler.post { progressBar.visibility = View.INVISIBLE }

        return when (index) {
            in 0..90 -> dataSet.slice(index..index + 9)
            in 91..99 -> dataSet.slice(index..99)
            else -> listOf()
        }
    }

    /**
     * API でリストデータを取得して画面に反映することを想定したメソッド。
     */
    private suspend fun fetchAndUpdate(index: Int) {
        val fetchedData = withContext(Dispatchers.Default) {
            fetch(index)
        }

        // 取得したデータを画面に反映。
        if (fetchedData.isNotEmpty()) {
            handler.post { myAdapter.add(fetchedData) }
        }
        // 問い合わせが完了したら false に戻す。
        nowLoading = false
    }

    /**
     * リストの下端までスクロールしたタイミングで発火するリスナー。
     */
    inner class InfiniteScrollListener : RecyclerView.OnScrollListener() {
        override fun onScrolled(recyclerView: RecyclerView, dx: Int, dy: Int) {
            super.onScrolled(recyclerView, dx, dy)

            // アダプターが保持しているアイテムの合計
            val itemCount = myAdapter.itemCount
            // 画面に表示されているアイテム数
            val childCount = recyclerView.childCount
            val manager = recyclerView.layoutManager as LinearLayoutManager
            // 画面に表示されている一番上のアイテムの位置
            val firstPosition = manager.findFirstVisibleItemPosition()

            // 何度もリクエストしないようにロード中は何もしない。
            if (nowLoading) {
                return
            }

            // 以下の条件に当てはまれば一番下までスクロールされたと判断できる。
            if (itemCount == childCount + firstPosition) {
                // API 問い合わせ中は true となる。
                nowLoading = true
                GlobalScope.launch {
                    fetchAndUpdate(myAdapter.itemCount)
                }
            }
        }
    }
}

init {...} について

今回は実際に API へ問い合わせは行いませんので、その代わりに文字列型のリストデータを作成しています。

onCreate() について

fetch() メソッドで API からリストデータを取得し、そのデータを引数として MyAdapter オブジェクトを作成しています。

また、RecyclerView にレイアウトマネージャー、アダプター、デコレーション(区切り線)、スクロールリスナー(無限スクロール)を設定しています。

fetch() について

API に問い合わせてリストデータを 10 件取得することを想定したメソッドです。インデックスを引数として取るので、仮に引数が 20 だった場合、20 ~ 29 のデータを返却します。

取得までにかかる時間を delay() で仮想的に実装しています。待ち時間は ProgressBar を表示させて、さらにそれっぽくしてます。

fetchAndUpdate() について

API からのデータ取得と、取得したデータを画面に反映させる処理を一緒に行うメソッドです。InfiniteScrollListener から呼び出して使います。

InfiniteScrollListener について

無限スクロールの実装部分です。

変数 itemCount、childCount、firstPosition の役割はコメントに書かれているとおりです。条件式にあるとおり itemCount == childCount + firstPosition のとき、下端までスクロールされたと判断できます。

Untitled Diagram.jpg

下端までスクロールされてデータを取得する際、変数 nowLoading を ture にします。データを取得している途中でもユーザーが上下にスクロールすると onScrolled() が発火してしまい、同じデータを何度もリクエストしてしまいます。これを防止するためにリクエスト中は変数 nowLoading を true にして制御します。その後データ取得が完了したタイミングで false に戻します。

以上で冒頭の GIF と同じ無限スクロールが実装できたと思います。

参考

RecyclerViewでスクロールが一番下まで行ったらロードする実装

9
9
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
9
9