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

API でデータセットを取得してリスト表示するような機能を想定しています。
画面生成時にリストを 10 件しておき、リストの下端までスクロールしたタイミングで 10 件ずつ追加で表示するサンプルアプリを作っていきます。
最終的に作成したアプリは Github で公開していますので参考にしていただければと思います。
実装手順
1. レイアウトファイル作成
RecyclerView のレイアウトと、リストに表示させる1行分のレイアウトを作成します。
<?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>
<?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. ビューホルダーとアダプターの作成
リストに表示させるデータを保持するビューホルダーと、ビューホルダーを管理してリストを画面表示するロジックを担うアダプターを作成します。
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 に無限スクロールの実装を行います。
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
のとき、下端までスクロールされたと判断できます。
下端までスクロールされてデータを取得する際、変数 nowLoading を ture にします。データを取得している途中でもユーザーが上下にスクロールすると onScrolled()
が発火してしまい、同じデータを何度もリクエストしてしまいます。これを防止するためにリクエスト中は変数 nowLoading を true にして制御します。その後データ取得が完了したタイミングで false に戻します。
以上で冒頭の GIF と同じ無限スクロールが実装できたと思います。