【Kotlin】RecyclerViewで無限スクロールを実装する (APIからデータ取得編)
Koltin でRecyclerViewを使って無限スクールを実装する方法をメモります。
今回はAPIからデータを取得して表示する方法編です。
実際にはこちらの方がよく使うパターンだと思います。
gradleファイルの修正
以下のライブラリを追加します。
dependencies { ・・・ implementation 'com.squareup.retrofit2:retrofit:2.0.2' implementation 'com.squareup.retrofit2:converter-gson:2.0.2' implementation 'androidx.recyclerview:recyclerview:1.1.0' }
レイアウトファイルの追加
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/recyclerview" android:layout_width="match_parent" android:layout_height="match_parent" /> </androidx.constraintlayout.widget.ConstraintLayout>
レコード用レイアウトファイル
row.xml
<?xml version="1.0" encoding="utf-8"?> <LinearLayout xmlns:android="http://schemas.android.com/apk/res/android" android:orientation="vertical" android:layout_width="match_parent" android:background="@color/colorPrimaryDark" android:layout_height="44dp"> <TextView android:id="@+id/textTitle" android:layout_width="match_parent" android:layout_height="match_parent" android:layout_margin="8dp" android:textStyle="bold" android:textSize="17sp" android:textColor="@android:color/white" android:gravity="center_vertical" android:text="Lorem Ipsum" /> <TextView android:id="@+id/subtitle" android:layout_width="match_parent" android:layout_height="match_parent" android:layout_margin="8dp" android:textStyle="bold" android:textSize="17sp" android:textColor="@android:color/white" android:gravity="center_vertical" android:text="Lorem Ipsum" /> </LinearLayout>
ローディング表示用レコードのレイアウトファイル
データ読み込み中に一覧の一番下にローディング表示したレコードを表示させます。 そのレコードのレイアウトファイルです。
progressbar.xml
<?xml version="1.0" encoding="utf-8"?> <LinearLayout xmlns:android="http://schemas.android.com/apk/res/android" android:layout_width="match_parent" android:layout_height="50dp" android:orientation="vertical"> <LinearLayout android:layout_width="match_parent" android:layout_height="wrap_content" android:gravity="center_horizontal" android:orientation="horizontal"> <ProgressBar android:id="@+id/progressbar" android:layout_width="24dp" android:layout_height="wrap_content" android:layout_gravity="center_horizontal" /> </LinearLayout> </LinearLayout>
Adapterファイルの作成
Adapterとは?
Viewとデータの橋渡しをするもの
RecyclerViewAdapter.kt
package com.example.android.sample.retrofitinfinite import android.view.LayoutInflater import android.view.View import android.view.ViewGroup import android.widget.Toast import androidx.recyclerview.widget.RecyclerView import kotlinx.android.synthetic.main.row.view.* import java.lang.IllegalArgumentException class RecyclerViewAdapter(var list: ArrayList<Data>) : RecyclerView.Adapter<RecyclerView.ViewHolder>(){ companion object{ private const val VIEW_TYPE_DATA = 0 private const val VIEW_TYPE_PROGRESS = 1 } override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): RecyclerView.ViewHolder { return when(viewType){ VIEW_TYPE_DATA -> { val view = LayoutInflater.from(parent.context).inflate(R.layout.row, parent, false) DataViewHolder(view) } VIEW_TYPE_PROGRESS -> { val view = LayoutInflater.from(parent.context).inflate(R.layout.progressbar, parent, false) ProgressViewHolder(view) } else -> throw IllegalArgumentException("Different View type") } } override fun getItemCount(): Int { return list.size } override fun onBindViewHolder(holder: RecyclerView.ViewHolder, position: Int) { if(holder is DataViewHolder) { holder.textTitle.text = list.get(position).title holder.textSubtitle.text = list.get(position).subtitle } } inner class DataViewHolder(itemView: View) : RecyclerView.ViewHolder(itemView){ var textTitle = itemView.textTitle var textSubtitle = itemView.subtitle init { itemView.setOnClickListener{ Toast.makeText(itemView.context, list.get(adapterPosition).title, Toast.LENGTH_LONG).show() } } } inner class ProgressViewHolder(itemView: View): RecyclerView.ViewHolder(itemView){ } override fun getItemViewType(position: Int): Int { var viewtype = list.get(position).category return when(viewtype){ "data" -> VIEW_TYPE_DATA else -> VIEW_TYPE_PROGRESS } } }
Dataクラスの作成
データクラスとは?
何もしない、データを保持するためだけのクラス。 Kotlinでは、これは データクラス と呼ばれ、 data としてマークされています。
1レコードごとのデータをここで定義する
Data.kt
package com.example.android.sample.retrofitinfinite class Data(var category: String) { var title: String? = null //タイトル var subtitle: String? = null //サブタイトル init { this.category = category } }
インターフェースの実装
Retrofit では API のリクエスト先のエンドポイントを Interface で定義します。
DataApi.kt
package com.example.android.sample.retrofitinfinite import retrofit2.Call import retrofit2.http.GET import retrofit2.http.Query interface DataApi { @GET("data.php") fun getData(@Query("index") index: Int): Call<List<Data>> }
APIリクエストを生成するクラス
定義したインターフェースからAPIのリクエストを生成するクラスを作成する
RetrofitInstance.kt
package com.example.android.sample.retrofitinfinite import retrofit2.Retrofit import retrofit2.converter.gson.GsonConverterFactory class RetrofitInstance { companion object{ fun getRetrofitInstance(): Retrofit{ val retrofit = Retrofit.Builder().baseUrl("https://www.androidride.com/").addConverterFactory( GsonConverterFactory.create() ).build() return retrofit } } }
MainActivity.tk の修正
必要なファイルは揃ったのでMainActivity.ktを修正します。
package com.example.android.sample.retrofitinfinite import androidx.appcompat.app.AppCompatActivity import android.os.Bundle import android.widget.Adapter import android.widget.Toast import androidx.recyclerview.widget.LinearLayoutManager import androidx.recyclerview.widget.RecyclerView import kotlinx.android.synthetic.main.activity_main.* import retrofit2.Call import retrofit2.Callback import retrofit2.Response import retrofit2.Retrofit class MainActivity : AppCompatActivity() { lateinit var list: ArrayList<Data> lateinit var adapter: RecyclerViewAdapter var notLoading = true lateinit var layoutManager: LinearLayoutManager lateinit var api: DataApi lateinit var retrofit: Retrofit override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) setContentView(R.layout.activity_main) list = ArrayList() adapter = RecyclerViewAdapter(list) recyclerview.setHasFixedSize(true) layoutManager = LinearLayoutManager(this) recyclerview.layoutManager = layoutManager recyclerview.adapter = adapter retrofit = RetrofitInstance.getRetrofitInstance() api = retrofit.create(DataApi::class.java) load(0) addScrolllistener() } private fun addScrolllistener() { recyclerview.addOnScrollListener(object: RecyclerView.OnScrollListener(){ override fun onScrolled(recyclerView: RecyclerView, dx: Int, dy: Int) { if(notLoading && layoutManager.findLastCompletelyVisibleItemPosition() == list.size - 1){ list.add(Data("progress")) adapter.notifyItemInserted(list.size - 1) notLoading = false val call: Call<List<Data>> = api.getData(list.size -1) call.enqueue(object : Callback<List<Data>>{ override fun onResponse( call: Call<List<Data>>?, response: Response<List<Data>>? ) { list.removeAt(list.size - 1) adapter.notifyItemRemoved(list.size) if(response!!.body().isNotEmpty()){ list.addAll(response.body()) adapter.notifyDataSetChanged() notLoading = true } else { Toast.makeText(applicationContext, "End of data reached", Toast.LENGTH_LONG).show() } } override fun onFailure(call: Call<List<Data>>?, t: Throwable?) { } }) } } }) } private fun load(i: Int) { val call: Call<List<Data>> = api.getData(0) call.enqueue(object: Callback<List<Data>>{ override fun onFailure(call: Call<List<Data>>?, t: Throwable?) { } override fun onResponse(call: Call<List<Data>>?, response: Response<List<Data>>?) { if(response!!.isSuccessful){ list.addAll(response!!.body()) adapter.notifyDataSetChanged() } } }) } }