BaseAdapter で ViewHolder パターンを実装する

# 概要
BaseAdapter で ViewHolder を実装する方法について述べます。主に Spinner を扱う際に役立ちます。

## なぜやるのか？
Spinner と BaseAdapter を組み合わせた実装だと、LayoutInfrater.inflate する行で Android Studio の警告が出ます。

![waning.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/102004/867933cf-74fa-5cb4-377e-ef5a046df5d1.png)

> Unconditional layout inflation from view adapter: Should use View Holder pattern (use recycled view passed into this method as the second parameter) for smoother scrolling.

別にこれを放置してもビルドはできますし、Spinner でスクロールのパフォーマンスが致命的になるほど要素を詰め込むことはそうそうないので、直さなくても構わないのではないかと思ってしまいがちです。実際私もずっと放置していました。

(ちなみに、リンク先を開くと [RecyclerView のドキュメント](https://developer.android.com/guide/topics/ui/layout/recyclerview#ViewHolder) が出てきます)

……が、昨今では技術者の評価に GitHub のリポジトリーを見ることも増えてきているようです。こうした雑なコードを残しておくのは自身の今後に悪い影響を与えかねないため、直しておくべきでしょう。

というのは冗談です。ViewHolder パターン自体は普遍的なデザインパターンですので、覚えておいて損はしません。私も RecyclerView を使い始めるまでは何度か書いていました。もう数年前になるので完全に忘れていましたが、せっかくなので思い出します。

[BaseAdapterで画像とテキストをListView表示](https://akira-watson.com/android/baseadapter.html)

# 実装

## 1. ViewHolder クラスを定義
各 View を持つクラスを定義します。これは Kotlin だと簡単です。Item を受け取って属性を各 View に当てる関数もこのクラスに実装させると良いでしょう。

```kotlin
class ViewHolder(private val icon: ImageView, private val text: TextView) {

    fun bindItem(item: Item) {
        icon.setImageDrawable(AppCompatResources.getDrawable(context, item.iconId))
        text.setText(item.id)
    }

}
```

## 2. getView で convertView の状態による分岐を書く

### 2-1 convertView が null の時
以下の 1-6 の処理をします。

1. Item を入れる Layout を inflate
2. 要素 View を find
3. ViewHolder を初期化
4. view の tag に ViewHolder のインスタンスをセット
5. viewHolder に item の属性を設定
6. inflate した Layout を return

```kotlin
    if (convertView == null) {
        val view = inflater.inflate(LAYOUT_ID, parent, false)
        val icon = view.findViewById<ImageView>(R.id.image)
        val text = view.findViewById<TextView>(R.id.text)
        val viewHolder = ViewHolder(icon, text)
        view.tag = viewHolder
        viewHolder.bindItem(item)
        return view
    }
```

### 2-2 convertView が null でない時
Layout の inflate は不要なので、 tag にセットした ViewHolder を取り出して item の属性を View に当てればよいです。

```kotlin
    val viewHolder = (convertView.tag as ViewHolder)
    viewHolder.bindItem(item)
    return convertView
```

## 全体のコード

```kotlin
override fun getView(position: Int, convertView: View?, parent: ViewGroup): View {
    val item = items[position]

    if (convertView == null) {
        val view = inflater.inflate(LAYOUT_ID, parent, false)
        val icon = view.findViewById<ImageView>(R.id.image)
        val text = view.findViewById<TextView>(R.id.text)
        val viewHolder = ViewHolder(icon, text)
        view.tag = viewHolder
        viewHolder.bindItem(item)
        return view
    }

    val viewHolder = (convertView.tag as ViewHolder)
    viewHolder.bindItem(item)
    return convertView
}
```

# おわりに
Spinner を扱う際に知っておくと有益な ViewHolder パターンの実装について紹介しました。Kotlin だと大分書きやすくなったという印象を受けました。そもそも Spinner 自体そんなに使わないかもしれないですが、忘れた時にこのドキュメントが役立つとちょっとうれしいです。

----
# 参考
- [Unconditional layout, inflation from view adapter: Should use View Holder pattern](https://stackoverflow.com/questions/25381435/unconditional-layout-inflation-from-view-adapter-should-use-view-holder-patter)
- [BaseAdapterで画像とテキストをListView表示](https://akira-watson.com/android/baseadapter.html)