RecyclerView をより速く

ある人は言いました。**速さとは正義である**。

冗談はさておき、[RecyclerView](https://developer.android.com/reference/androidx/recyclerview/widget/RecyclerView) の表示ってもっと速くなんないかなというお話です。[ViewPager2](https://developer.android.com/reference/androidx/viewpager2/widget/ViewPager2) を使って、画面を左右に移動できる Andoroid アプリを作っているのですが、RecyclerView や ListView を使っていると各行のレイアウト生成に時間がかかり、左右の移動がカクカクになってます。

私は、初めて読んだ本に「RecyclerView は、まず行のレイアウトファイル(XML)を作成して...」 と書いてあったので、なにも考えずにレイアウトファイルを作成していましたが、そもそもレイアウトファイルとかいらない子なのではっと思い立ち、レイアウトをコード生成して速さを比較してみました。結果としては、まぁまぁ速くなった気がします。

## 環境
Android Studio: Koala | 2024.1.1 Patch 1
計測に使用した端末: 
AQUOS sense6 [Andorid 13 (API 33)]
Amazon KFRAPWI [Andorid 11 (API 30)]

## テストアプリの見た目
RecyclerView で TextView が3つある行を30個表示させるだけです。

![testapp.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3853334/4e154c66-d0d1-6ff6-961a-a0e99cbeeb92.png)

## 比較するコード
レイアウトファイル(XML)を読み込むアダプターとレイアウトをコード生成するアダプターで速さを比較してきます。ちなみ、Main Activity は、RecyclerView を表示するだけです。

```kotlin:MainActivity.kt
class MainActivity: AppCompatActivity() {
   override fun onCreate(savedInstanceState: Bundle?) {
      super.onCreate(savedInstanceState)
      setContentView(R.layout.activity_main)
      val rv = findViewById<RecyclerView>(R.id.recyclerView)
      rv.layoutManager = LinearLayoutManager(this)
      rv.adapter = TestAdapterLayout(this)
      //rv.adapter = TestAdapterCode(this)
   }
}
```

### 1. レイアウトファイル読み込みアダプター
レイアウトファイル（listview_item.xml）からレイアウトを生成し、行データを管理するアダプター
```kotlin:レイアウトファイルを読み込み
class TestAdapterLayout(private val context: Context): RecyclerView.Adapter<TestAdapterLayout.ViewHolder>() {
   override fun getItemCount(): Int = 30
   inner class ViewHolder(itemView: View): RecyclerView.ViewHolder(itemView) {
      val textView1: TextView = itemView.findViewById(R.id.textView1)
      val textView2: TextView = itemView.findViewById(R.id.textView2)
      val textView3: TextView = itemView.findViewById(R.id.textView3)
   }
   override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ViewHolder {
      val view = LayoutInflater.from(context).inflate(R.layout.listview_item, parent, false)
      return ViewHolder(view)
   }
   override fun onBindViewHolder(holder: ViewHolder, position: Int) {
      holder.textView1.text = "A$position"
      holder.textView2.text = "B$position"
      holder.textView3.text = "C$position"
   }
}
```
```xml:listview_item.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="wrap_content"
    xmlns:tools="http://schemas.android.com/tools">
    <TextView
        android:id="@+id/textView1"
        android:layout_width="0dp"
        android:layout_height="wrap_content"
        android:layout_weight="1"
        android:textAlignment="center"/>
    <TextView
        android:id="@+id/textView2"
        android:layout_width="0dp"
        android:layout_height="wrap_content"
        android:layout_weight="1"
        android:textAlignment="center"/>
    <TextView
        android:id="@+id/textView3"
        android:layout_width="0dp"
        android:layout_height="wrap_content"
        android:layout_weight="1"
        android:textAlignment="center"/>
</LinearLayout>
```
### 2. レイアウトコード生成アダプター
コードからレイアウトを生成し、行データを管理するアダプター
```kotlin:レイアウトをコード生成
class TestAdapterCode(): RecyclerView.Adapter<TestAdapterCode.ViewHolder>() {
   override fun getItemCount(): Int = 30
   inner class ViewHolder(itemView: View): RecyclerView.ViewHolder(itemView) {
      private val linearLayout = itemView as ViewGroup
      val textView1: TextView = linearLayout.getChildAt(0) as TextView
      val textView2: TextView = linearLayout.getChildAt(1) as TextView
      val textView3: TextView = linearLayout.getChildAt(2) as TextView
   }
   override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ViewHolder {
      val view = LinearLayout(parent.context)
      view.orientation = LinearLayout.HORIZONTAL
      view.layoutParams = LinearLayout.LayoutParams(
         LinearLayout.LayoutParams.MATCH_PARENT,
         LinearLayout.LayoutParams.WRAP_CONTENT)
         
      repeat(3){
         val textView = TextView(parent.context)
         textView.layoutParams = LinearLayout.LayoutParams(
            0,
            LinearLayout.LayoutParams.WRAP_CONTENT,
            1f)
         textView.textAlignment = View.TEXT_ALIGNMENT_CENTER
         view.addView(textView)
      }
      return ViewHolder(view)
   }
   override fun onBindViewHolder(holder: ViewHolder, position: Int) {
      holder.textView1.text = "A$position"
      holder.textView2.text = "B$position"
      holder.textView3.text = "C$position"
   }
}
```

## 計測方法
onCreateViewHolder のレイアウト読み込み or レイアウトコード生成にどれだけかかったか単純に時間を測って確認します。

```kotlin:計測コード(抜粋)
private var totalTime = 0L
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ViewHolder {
    val startTime = System.nanoTime()

    //レイアウト読み込み or レイアウトコード生成処理を実行
    
    val endTime = System.nanoTime()
    val durationMillis = (endTime - startTime) / 1_000_000
    totalTime += durationMillis
    Log.d("ViewHolderTiming", "onCreateViewHolder took $durationMillis ms, total time: $totalTime ms")
    return ViewHolder(view)
}
```
```text:ログ出力形式（Logcat）
onCreateViewHolder took xxx ms, total time: yyy ms
```
## 実測結果
### AQUOS sense6
レイアウトファイル読み込み方式とレイアウトコード生成方式を比較すると、30個の行アイテムの生成時間が半分以下になりました。両方とも1回目の生成にはちょっと時間がかかるようです。
#### 1. レイアウトファイルを読み込み
```text:実行結果
onCreateViewHolder took 18 ms, total time: 18 ms
onCreateViewHolder took 6 ms, total time: 24 ms
onCreateViewHolder took 7 ms, total time: 31 ms
onCreateViewHolder took 5 ms, total time: 36 ms
(中略)
onCreateViewHolder took 5 ms, total time: 174 ms
onCreateViewHolder took 6 ms, total time: 180 ms
onCreateViewHolder took 6 ms, total time: 186 ms
onCreateViewHolder took 7 ms, total time: 193 ms
```
#### 2. レイアウトをコード生成
```text:実行結果
onCreateViewHolder took 5 ms, total time: 5 ms
onCreateViewHolder took 4 ms, total time: 9 ms
onCreateViewHolder took 3 ms, total time: 12 ms
onCreateViewHolder took 3 ms, total time: 15 ms
(中略)
onCreateViewHolder took 2 ms, total time: 68 ms
onCreateViewHolder took 2 ms, total time: 70 ms
onCreateViewHolder took 2 ms, total time: 72 ms
onCreateViewHolder took 3 ms, total time: 75 ms
```

### Amazon KFRAPWI
端末のスペックの問題かと思いますが、AQUOS Sense 6 よりも1個あたりの生成時間は長くなりますが、こちらも30個の行アイテムが生成されるまでの時間が半分以下になっています。
#### 1. レイアウトファイルを読み込み
```text:実行結果
onCreateViewHolder took 42 ms, total time: 42 ms
onCreateViewHolder took 13 ms, total time: 55 ms
onCreateViewHolder took 12 ms, total time: 67 ms
onCreateViewHolder took 12 ms, total time: 79 ms
(中略)
onCreateViewHolder took 11 ms, total time: 357 ms
onCreateViewHolder took 13 ms, total time: 370 ms
onCreateViewHolder took 11 ms, total time: 381 ms
onCreateViewHolder took 11 ms, total time: 392 ms
```
#### 2. レイアウトをコード生成
```text:実行結果
onCreateViewHolder took 7 ms, total time: 7 ms
onCreateViewHolder took 4 ms, total time: 11 ms
onCreateViewHolder took 4 ms, total time: 15 ms
onCreateViewHolder took 4 ms, total time: 19 ms
（中略）
onCreateViewHolder took 4 ms, total time: 118 ms
onCreateViewHolder took 4 ms, total time: 122 ms
onCreateViewHolder took 4 ms, total time: 126 ms
onCreateViewHolder took 4 ms, total time: 130 ms
```

## さいごに
レイアウトファイル(XML)を読み込むよりもレイアウトをコード生成する方が、2倍以上速くなるという結果に満足です。ちょっとコードが長くなりますが、単純なレイアウトであれば、コードから生成するのも簡単なのでそこまでの苦労なく高速化できると思います。

一般的なスマホのリフレッシュレートは60Hzなので、 ViewPager でのカクツキを抑えるには、View の合計生成時間が、`1秒/60Hz = 16~17ms` 以内であるのが理想ですが、行アイテムが多いと普通に超えてきてしまいます。

テストアプリのように小さい行データが30個あるような構成のアプリはないとは思いますが、行データの数が多い場合には、レイアウト構成の変更を視野に入れて設計が必要かなと思いました。

もっとこうすれば速くなるよとかここがおかしいよって気づいた方はコメントいただけると助かります。

## 参考URL
https://techracho.bpsinc.jp/kawawa/2019_08_19/79159