【Kotlin研修15日目】リサイクラービューの実装

# リサイクラービュー(RecyclerView)
[参考1: RecyclerViewでのリスト作成](https://developer.android.com/guide/topics/ui/layout/recyclerview)
[参考2: 研修14日目(CoordinatorLayout)](https://qiita.com/b150005/items/fc85877a55efa1613e3d#coordinatorlayout)
`CoordinatorLayout`で**スクロール連動**させるために必要な`NestedScrollingChild`インタフェースを実装した`ListView`。

`リサイクラービュー`が保持する`レイアウトマネージャ`を通じて`リスト`の`レイアウト`を決定し、
`アダプタクラス`によって生成される`ビューホルダ`を通じて、`アイテム内ビュー`に`リストデータ`が埋め込まれる。

![リサイクラービュー.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/673880/ba61f9d4-965f-f83c-54f2-e6064cc2e298.png)

##  RecyclerViewとListView
[参考: 研修2日目](https://qiita.com/b150005/items/7059179037a56df42c7c)
`リスト`形式の`ビュー`を提供する`RecyclerView`と`ListView`の違いは、以下の通り。

|ビュー|スクロール連動|レイアウトの柔軟性|アダプタクラス|区切り線|リスナインタフェース|
|:--|:--:|:--:|:--:|:--:|:--:|
|`RecyclerView`|o|o|x|x|o|
|`ListView`|x|x|o|o|o|

## レイアウトマネージャ
`リサイクラービュー`の`レイアウト配置`と`アイテム管理`を行うクラス。
標準で`LinearLayoutManager`・`GridLayoutManager`・`StaggeredGridLayoutManager`の3つの`レイアウトマネージャ`が用意されている。

また、`RecyclerView.LayoutManager`を実装することで、独自定義した`レイアウトマネージャ`を作成することもできる。

![LayoutManager.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/673880/5717bf53-6c26-0266-81b7-c98df9a8b65e.png)

### LinearLayoutManager
`アイテム`を`縦のリスト`に並べる`レイアウトマネージャ`。

### GridLayoutManager
`アイテム`を`格子状`に並べる`レイアウトマネージャ`。

### StaggeredGridLayoutManager
`アイテム`を`スタッガード格子状`に並べる`レイアウトマネージャ`。

## アダプタ
[参考: 研修2日目(Adapter)](https://qiita.com/b150005/items/7059179037a56df42c7c#adapter%E3%82%AF%E3%83%A9%E3%82%B9)
`アクティビティクラス`で記述した`リストデータ`を`ビューホルダ`が管理する`ビュー`に紐づける(=`バインド`)、
`RecyclerView.Adapter`クラスの`実装クラス`。

`ListView`とは異なり、`リサイクラービュー`には`アダプタクラス`が用意されていないため、
`RecyclerView.Adapter`クラスを実装した`アダプタクラス`を独自に定義する必要がある。

### RecyclerView.Adapter
`リサイクラービュー`の`アダプタ`を定義する`抽象クラス`。

`アイテム内ビュー`を管理する`ビューホルダ`を生成するメソッド(=`onCreateViewHolder()`)、
`ビューホルダ`が管理する`ビュー`に`リストデータ`を`バインド`するメソッド(=`onBindViewHolder()`)、
また、`リサイクラービュー`が表示する`リストデータ`の件数を取得するメソッド(=`getItemCount()`)を**抽象的に定義**する。

## ビューホルダ
`リストデータ`を表示する`リストアイテム`内の`ビュー`を管理する、`RecyclerView.ViewHolder`クラスの実装クラス。

### RecyclerView.ViewHolder
`リストデータ`を表示する`リストアイテム`内の`ビュー`を管理する`抽象クラス`。

***

# リサイクラービューの実装
`リサイクラービュー`を実装する手順は、以下の通り。

> 1. `リストデータ`を定義・返却するメソッドを記述
> 2. 「`リストデータ`を表示する`ビュー`」を定義する`ビューホルダクラス`の作成
> 3. `リストデータ`を`リサイクラービュー`に表示する`アダプタクラス`の作成
> 4. `リサイクラービュー`に`レイアウトマネージャ`と`アダプタ`をセット

## リストデータの定義
`リサイクラービュー`に表示する`リストデータ`を定義する。

### サンプルコード

```kotlin:MainActivity.kt
// RecyclerViewのItemのリスト定義・作成
private fun createSetMealList(): MutableList<MutableMap<String, Any>> {

    // MutableList
    val menuList: MutableList<MutableMap<String, Any>> = mutableListOf()

    // --- リストデータの定義開始 ---
    // MutableMapの定義
    var menu = mutableMapOf<String, Any>(
        "name" to "item 1",
        "price" to 750,
        "desc" to "This description is about item 1."
    )
    // MutableListに追加
    menuList.add(menu)

    // MutableMapの定義
    menu = mutableMapOf<String, Any>(
        "name" to "item 2",
        "price" to 900,
        "desc" to "This description is about item 2."
    )
    // MutableListに追加
    menuList.add(menu)
    ...   // 以降繰り返し
    // --- リストデータの定義終了 ---
    
    // 定義したMutableListを返却
    return menuList
}
```

## ビューホルダの作成
`リストデータ`を表示する`ビュー`を定義する、`RecyclerView.ViewHolder`の`実装クラス`(=`ビューホルダ`)を作成する。

ただし、`RecyclerView.ViewHolder`の`クラスコンストラクタ`は引数に`View`オブジェクトをとるため、
`ビューホルダ`の`クラスコンストラクタ`も引数に`View`オブジェクトをとるように定義する必要がある。

なお、ここでの`View`オブジェクトとは、`リサイクラービュー`の`アイテムビュー`を指す。

### 定義

```kotlin
RecyclerView.ViewHolder(@NonNull itemView: View)
```

### サンプルコード

```kotlin:MainActivity.kt
// アイテム内ビュー(=TextView)を保持するビューホルダ
private inner class RecyclerListViewHolder(itemView: View): RecyclerView.ViewHolder(itemView) {
    // Item内に表示される1つ目のTextView
    var _tvMenuNameRow: TextView

    // Item内に表示される2つ目のTextView
    var _tvMenuPriceRow: TextView

    // 初期化時に呼び出される処理(=イニシャライザ)
    init {
        // Item内のTextView
        // -> アダプタクラスのonBindViewHolder()メソッドによってリストデータが反映される
        _tvMenuNameRow = itemView.findViewById(R.id.tvMenuNameRow)
        _tvMenuPriceRow = itemView.findViewById(R.id.tvMenuPriceRow)
    }
}
```

## アダプタクラスの作成
`ビューホルダ`が保持する`ビュー`に`リストデータ`を紐づける、`RecyclerView.Adapter`の`実装クラス`(=`アダプタクラス`)を作成する。

`アイテム内ビュー`を管理する`ビューホルダ`を生成する`onCreateViewHolder()`メソッドでは、
`ビューホルダ`の`クラスコンストラクタ`の引数に`アイテムビュー`が必要となるため、
`リサイクラービュー`の`レイアウトファイル`の`インフレート`を行う必要がある。

### インフレート(inflate)
[参考: 研修4日目](https://qiita.com/b150005/items/1f58b3418bb1690c2f4b#%E3%82%A4%E3%83%B3%E3%83%95%E3%83%AC%E3%83%BC%E3%83%88inflate)
`レイアウトファイル`(=`XML`ファイル)を`Javaオブジェクト`の`ビュー`として`実体化`すること。

`LayoutInflater`クラスの`from()`メソッドを用いて`コンテキスト`から`LayoutInflater`オブジェクトを取得し、
`inflate()`メソッドを用いて`インフレート`を行う。

#### LayoutInflater
`UI部品`をインフレートするメソッドを定義するクラス。

#### 定義
[参考: LayoutInflater](https://qiita.com/Bth0061/items/c4f66477979d064913e4)

```kotlin
// LayoutInflaterオブジェクトの取得
LayoutInflater.from(context: Context!): LayoutInflater!
// パラメータ
// context: LayoutInflaterを利用するコンテキスト

// レイアウトファイルのインフレート
LayoutInflater.inflate(
    resource: Int, 
    root: ViewGroup?
    attachToRoot: Boolean
): View!
// パラメータ
// resource: インフレートするレイアウトファイル名のID(R値)
// root: レイアウトファイルの実体化先となるビューグループ
// attachToRoot: rootパラメータのビューグループを親レイアウト部品として設定するかどうか
```

### 定義

```kotlin:RecyclerView.Adapterの抽象メソッド
// RecyclerViewによって最初に呼び出される処理
@NonNull
RecyclerView.Adapter.onCreateViewHolder(
    @NonNull parent: ViewGroup, 
    viewType: Int
): <ビューホルダ>
// パラメータ
// parent: ビューホルダが保持するビューの親ビューグループ(=アイテムビュー)
// viewType: アイテムビューのビュータイプを識別する整数値

// RecyclerViewがアダプタからビューホルダを受け取った際にRecyclerViewから呼び出される処理
RecyclerView.Adapter.onBindViewHolder(
    @NonNull holder: <ビューホルダ>, 
    position: Int
): Unit
// パラメータ
// holder: ビューホルダオブジェクト
// position: リストデータがバインドされるアイテムのポジション番号(≒行番号)

// リストデータの項目数を取得
RecyclerView.Adapter.getItemCount(): Int
```

### サンプルコード

```kotlin:MainActivity.kt
// 独自定義するアダプタクラス
// <- RecyclerView.Adapterクラスの実装クラス
// <- クラスコンストラクタは一度のみ呼ばれ、クラス内でしか利用しない(=機密性を持たせる)ため、
//    "private val"を付与
private inner class RecyclerListAdapter(
    private val _listData: MutableList<MutableMap<String, Any>>
): RecyclerView.Adapter<RecyclerListViewHolder>() {

    // RecyclerViewによって最初に呼び出される処理
    override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): RecyclerListViewHolder {

        // LayoutInflaterオブジェクト
        val inflater = LayoutInflater.from(this@MainActivity)

        // レイアウトファイルのインフレート(=アイテムビュー)
        val view = inflater.inflate(R.layout.row, parent, false)

        // ビューホルダの生成
        val holder = RecyclerListViewHolder(view)

        // ビューホルダを返却
        return holder
    }

    // RecyclerViewがアダプタからビューホルダを受け取った際にRecyclerViewから呼び出される処理
    override fun onBindViewHolder(holder: RecyclerListViewHolder, position: Int) {

        // リストデータのアイテム
        val item = _listData[position]

        // 指定したキーと一致するリストデータ
        val menuName = item["name"] as String
        val menuPrice = item["price"] as Int

        // Int型 → String型 への変換
        val menuPriceStr = menuPrice.toString()

        // ビューホルダが保持するビューにデータを反映
        holder._tvMenuNameRow.text = menuName
        holder._tvMenuPriceRow.text = menuPriceStr
    }

    // リストデータの項目数を取得
    override fun getItemCount(): Int {
        // リストデータの項目数を返却
        // <- リストデータはクラスコンストラクタの引数から取得
        return _listData.size
    }
}
```

## レイアウトマネージャ・アダプタのセット
[参考: 研修2日目](https://qiita.com/b150005/items/7059179037a56df42c7c#listview%E3%81%AB%E5%AF%BE%E3%81%99%E3%82%8Barrayadapter%E3%82%AA%E3%83%96%E3%82%B8%E3%82%A7%E3%82%AF%E3%83%88%E3%81%AE%E3%82%BB%E3%83%83%E3%83%88)
`レイアウトマネージャ`は`RecyclerView`オブジェクトの`layoutManager`プロパティ、
`アダプタ`は`RecyclerView`オブジェクトの`adapter`プロパティにセットする。

### 定義

```kotlin
// LinearLayoutManagerオブジェクトの生成
LinearLayoutManager(context: Context!)
// パラメータ
// context: LinearLayoutManagerを利用するコンテキスト
```

### サンプルコード

```kotlin:MainActivity.kt
class MainActivity : AppCompatActivity() {
    override fun onCreate(savedInstanceState: Bundle?) {
        ...
        // RecyclerView
        val lvMenu = findViewById<RecyclerView>(R.id.lvMenu)

        // レイアウト配置とリストアイテムを管理するLinearLayoutManager(=レイアウトマネージャ)
        val layout = LinearLayoutManager(this@MainActivity)

        // RecyclerViewにLinearLayoutManagerをセット
        lvMenu.layoutManager = layout

        // リストデータ
        val menuList = createSetMealList()

        // リストデータを紐づけるRecyclerListAdapter
        val adapter = RecyclerListAdapter(menuList)

        // RecyclerViewにRecyclerListAdapterをセット
        lvMenu.adapter = adapter
    }
    ...
}
```

***

# LinearLayoutManagerのリサイクラービューへの区切り線の追加
`レイアウトマネージャ`に`LinearLayoutManager`を採用する`リサイクラービュー`は、
`リストアイテム`間の`区切り線`(=`Divider`)を標準で用意していないため、
`区切り線`を表示したい場合は、`DividerItemDecoration`オブジェクトを利用する必要がある。

## DividerItemDecoration
`LinearLayoutManager`を採用する`リサイクルビュー`への`区切り線`を追加する、
`RecyclerView.ItemDecoration`クラスの`実装クラス`。

### RecyclerView.ItemDecoration
`アイテムビュー`の`装飾スタイル`を定義する`抽象クラス`。

## 定義

```kotlin
// DividerItemDecorationオブジェクトの生成
DividerItemDecoration(context: Context!, orientation: Int)
// パラメータ
// context: DividerItemDecorationを利用するコンテキスト
// orientation: 区切り線の方向(=垂直または水平方向)を表すDividerItemDecorationクラス定数
// <- LinearLayoutManagerのorientationプロパティを利用するのが一般的

// リサイクラービューへの区切り線の追加
RecyclerView.addItemDecoration(
    @NonNull decor: RecyclerView.ItemDecoration
): Unit
// パラメータ
// decor: アイテムビューに追加するItemDecorationオブジェクト
```

### 区切り線の方向を表すDividerItemDecorationクラス定数

|定数名|区切り線の方向|
|:--|:--:|
|`HORIZONTAL`|水平|
|`VERTICAL`|垂直|

## サンプルコード

```kotlin:MainActivity.kt
// RecyclerView
val lvMenu = findViewById<RecyclerView>(R.id.lvMenu)

// 区切り線を表示するDividerItemDecorationオブジェクト
val decorator = DividerItemDecoration(this@MainActivity, layout.orientation)

// リサイクラービューへの区切り線の追加
lvMenu.addItemDecoration(decorator)
```

***

# リサイクラービューへのリスナ定義
`リサイクラービュー`は`ListView`とは異なり、専用の`リスナインタフェース`が用意されていないため、
`View.OnClickListener`インタフェースを実装した`リスナクラス`を独自定義する必要がある。

また、`アイテムビュー`に対して`リスナ`として`リスナクラスオブジェクト`をセットする必要があるため、
`アダプタクラス`の`onCreateViewHolder()`メソッド内で`インフレート`した`レイアウトファイル`(=`アイテムビュー`)に対して、`リスナ定義`を行う。

## サンプルコード

```kotlin:MainActivity.kt
class MainActivity : AppCompatActivity() {
    // アダプタクラス
    // <- RecyclerView.Adapterクラスの実装クラス
    private inner class RecyclerListAdapter(
        private val _listData: MutableList<MutableMap<String, Any>>
    ): RecyclerView.Adapter<RecyclerListViewHolder>() {
        // RecyclerViewによって最初に呼び出される処理
        override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): RecyclerListViewHolder {
            ...
            // LayoutInflaterオブジェクト
            val inflater = LayoutInflater.from(this@MainActivity)

            // レイアウトファイルのインフレート(=アイテムビュー)
            val view = inflater.inflate(R.layout.row, parent, false)

            // インフレートしたアイテムビューをリスナとしてセット
            view.setOnClickListener(ItemClickListener())
            ...
        }
        ...
    }

    // リサイクラービューのアイテムの"タップ"イベントを検知するリスナクラス
    // <- View.OnClickListenerインタフェースの実装クラス
    private inner class ItemClickListener: View.OnClickListener {
        // "タップ"イベント検知時に呼び出される処理(イベントハンドラ)
        override fun onClick(view: View?) {
            ...   // タップイベント検知時の処理
        }
    }
}
```