KotlinでTODOアプリをつくる～その３～

引き続き、Android向けTODOアプリ作成についてまとめます。

## その３で扱う内容
これまで、[その１](https://qiita.com/satona-oinuma/items/dfd9bebf9c2e00247c49)ではviewBinding、[その２](https://qiita.com/satona-oinuma/items/f31a4a948eb05a918ee1)ではFlagmentを扱いました。
その3ではリストの表示方法として、RecyclerViewを扱います。

### クラス構成
今回新たに用意するクラスはありません。
今まで作成した6つのクラスのうち、主に以下の2つを使用します。
- CategoryAdapter
- ItemAdapter
後述しますが、どちらもRecyclerViewのAdapterを継承したクラスです。

また、前回に引き続き、以下の4つのファイルを使用します。
- MainActivity.kt：Activity管理クラスのファイル
- MainFragment.kt：Fragment管理クラスのファイル
- layout/activity_main.xml：Activity開始時に表示されるレイアウトのファイル
- layout/fragment_item_list.xml：Fragment開始時に呼び出されるレイアウトファイル

## リストの表示：RecyclerView
RecyclerViewは、リストを表示するときに使用するAPIです。
Androidでリスト表示を実現するには`ListView`や`RecyclerView`などの選択肢がありますが、`RecyclerView`はその名の通りViewをリサイクルする仕組みをもつAPIです。
画面外にあるViewを画面内に使いまわすことで効率的に描画ができ、大量のデータをリスト表示する際に適しています。
また、`ListView`と比べて柔軟にカスタマイズできるため、独自のリストを作りたい場合や2列のリストを表示したい場合などにも適しています。

まずは、RecyclerViewを実装するために必要なものを確認しましょう。
RecyclerViewを実装するためには、大きく5つの要素が必要です。

1. RecyclerViewを呼び出すクラス（今回はFragment継承クラス）
2. RecyclerViewの要素を管理するためのクラス（RecyclerView.Adapter継承クラス）
3. 要素のViewを保持するクラス（RecyclerView.ViewHolder継承クラス）
4. RecyclerViewのレイアウト
5. RecyclerViewに設置する要素のレイアウト


### 呼び出し口
RecyclerViewを実装するための5要素、1つ目はRecyclerViewの呼び出し口です。
呼び出しは、RecyclerViewを表示したい画面の管理クラスから行います。
その際、以下2つの設定が必須です。
- LayoutManagerの登録
- Adapterの登録
また、リスト要素ごとに区切り線を入れたい場合はここで設定します。

#### LayoutManagerの登録
 LayoutManagerは、リストの表示方法を管理するためのものです。
設定できる値には以下の3つなどがあり、必要に応じて使い分けます。
- LinearLayoutManager：1列または1行で表示する
- GridLayoutManager：複数列、各列の要素の高さ(または各行の要素の幅)は一定で表示する
- StaggeredGridLayoutManager：複数列、各列の要素の高さ(または各行の要素の幅)は要素に合わせて表示する

今回のTODOアプリでは、以下を使用します。
- カテゴリ一覧画面：GridLayoutManager
GridLayoutManagerを設定する際には、GridLayoutManagerの第一引数にcontext、第2引数に列数または行数を指定します。
```Kotlin
with(recyclerView) {
    // columnCountは 2 を設定
    layoutManager = GridLayoutManager(context, columnCount)
    ...
}
```
- タスク一覧画面：LinearLayoutManager
LinearLayoutManagerを設定する際は、第一引数にcontextを指定します。
```Kotlin
with(recyclerView) {
    layoutManager = LinearLayoutManager(context)
    ...
}
```


#### Adapterの登録
RecyclerView.Adapterクラスを継承したクラスを用意し、そのインスタンスをAdapterとして設定します。
Adapter継承クラスの内容については後述します。

- カテゴリ一覧画面
```Kotlin
with(recyclerView) {
    // Adapter継承クラスのインスタンスを生成
    categoryAdapter = CategoryAdapter()
    // Adapterに設定
    adapter = categoryAdapter
    ...
}
```
- タスク一覧画面
```Kotlin
with(recyclerView) {
    // Adapter継承クラスのインスタンスを生成
    itemAdapter = ItemAdapter()
    // Adapterに設定
    adapter = itemAdapter
    ...
}
```

#### リスト要素ごとに区切り線を入れる場合
今回はタスク一覧画面にのみ区切り線を入れます。
RecyclerViewのレイアウトを修飾するためにaddItemDecorationメソッドを利用し、引数には区切り線を設定するためのDividerItemDecorationのインスタンスを追加します。
DividerItemDecorationは第一引数にcontext、第二引数に向きを指定してインスタンスを生成します。
向きには以下のような値も入りますが、LinearLayoutManagerのgetOrientatioを使用すると現在のレイアウトの向きに合わせて自動で設定することができるため便利です。

- LinearLayout.HORIZON：水平に要素を並べる場合に使用
- LinearLayout.VERTICAL：垂直に要素を並べる場合に使用

```Kotlin
with(recyclerView) {
    ...
    addItemDecoration(
        DividerItemDecoration(context, LinearLayoutManager(context).orientation)
    )
}
```

以上、呼び出し口全体としては以下のようになります。

- カテゴリ一覧画面
```Kotlin:MainFragment.kt
// レイアウトファイルに定義したRecyclerViewを取得
// it は RecyclerViewを定義したレイアウトのBindingクラスのインスタンス
val recyclerView = it.recycle
with(recyclerView) {
     // LayoutManagerを設定
    layoutManager = GridLayoutManager(context, columnCount)
    if (categoryAdapter == null) categoryAdapter = CategoryAdapter()
     // Adapterを設定
    adapter = categoryAdapter
}
```

- タスク一覧画面
```Kotlin:MainFragment.kt
with(recyclerView) {
     // LayoutManagerを設定
     layoutManager = LinearLayoutManager(context)
     if (itemAdapter == null) itemAdapter = ItemAdapter()
     // Adapterを設定
     adapter = itemAdapter
     // 区切り線を追加
     addItemDecoration(
         DividerItemDecoration(context, LinearLayoutManager(context).orientation)
     )
}
```

### Adapter継承クラス
RecyclerViewを実装するための5要素、2つ目はAdapter継承クラスです。
こちらはRecyclerViewに表示する個々の要素の表示方法などを管理するためのクラスです。
RecyclerView.Adapterを継承したクラスを作成し、以下の3つのメソッドをoverride（=親クラスのメソッドを子クラスで利用できる形で実装）する必要があります。
- onCreateViewHolder
- onBindViewHolder
- getItemCount()

Adapter継承クラスとしてはCategoryAdapterクラスとItemAdapterクラスの2つがありますが、ほぼ同じ実装のためCategoryAdapterを例にします。

CategoryAdapterでは、以下のようにRecyclerView.Adapterを継承しています。
```Kotlin
class CategoryAdapter: RecyclerView.Adapter<CategoryAdapter.CategoryViewHolder>(){
```
`<>`内にはRecyclerView.ViewHolderを継承したクラスが入ります。

そして、プロパティとして、表示する要素情報をまとめたcategoriesリストを実装します。
このリストはAdapter継承クラスの初期化時点で必要な材料で、これがないと画面上に要素が表示できません。

データ管理周りについてはその４以降で取り扱う予定のため、現在はただただ表示に必要な情報をまとめている状態です。

```Kotlin
class CategoryAdapter: RecyclerView.Adapter<CategoryAdapter.CategoryViewHolder>() {

    //RecyclerViewの素になる情報をまとめたMutableList
    private var categories: MutableList<Category> = mutableListOf(
        Category("home🏠"), Category("friend👧"),
        Category("pet🐈"), Category("event★")
    )

    data class Category(
        val name: String
    )
    ...
```

#### onCreateViewHolder
Adapterクラスからoverrideするものの1つ、ViewHolderを生成するためのメソッドです。
新しいViewHolderを生成する必要があるたび、つまり画面を開いたときやスクロールしたときに毎回実行されます。

```Kotlin
// 戻り値の型はViewHolder継承クラス
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): CategoryViewHolder {
    // 戻り値にはViewHolderクラスのインスタンスを指定
    return CategoryViewHolder(
        CategoryLayoutBinding.inflate(
            LayoutInflater.from(parent.context),
            parent,
            false
        )
    )
}
```

#### onBindViewHolder
Adapterクラスからoverrideするものの2つ目、要素を画面内に表示するためのメソッドです。
要素が画面内に表示される際に毎回動きます。

onBindViewHolderでは、リストの何番目の情報を表示しようとしているかを示す`position`を利用できます。
そのため、このメソッド内でリストの中のposition番目の要素情報と、実際に表示されるviewのレイアウトを結びつけます。

```Kotlin
override fun onBindViewHolder(holder: CategoryViewHolder, position: Int) {
    val item = categories[position]
    // レイアウトのテキストにposition番目のカテゴリ名を設定
    holder.contentView.text = item.name
    // 各要素にclick処理を追加 
    // listenerにタスク一覧画面に遷移する処理やチェックマークの表示非表示切り替えなどを設定(省略)
    holder.contentView.setOnClickListener(listener)
    }
```

#### getItemCount
Adapterクラスからoverrideするものの最後は、RecyclerViewに表示する要素の個数を数えるためのメソッドです。
getItemCountでカウントした数だけ要素が表示できるようになります。
getItemCountメソッドでは戻り値として、要素情報をまとめたリストのサイズを設定します。

```Kotlin
// categoriesリストのサイズを返す
override fun getItemCount(): Int = categories.size
```

### ViewHolder
RecyclerViewを実装するための5要素、3つ目はRecyclerView.Holderを継承したクラスです。
AdapterクラスのonCreateViewHolderでインスタンスを生成します。
実装内容が少ないため、今回はAdapter継承クラスのinnerクラスとして実装しています。

```Kotlin
// innerクラスとしてRecyclerView.ViewHolder継承クラスを作成
inner class CategoryViewHolder(binding: CategoryLayoutBinding) : RecyclerView.ViewHolder(binding.root) {
    // viewの要素としてカテゴリ名を表示するためのTextViewを設定
    val contentView: TextView = binding.category
}
```

### RecyclerViewレイアウト
RecyclerViewを実装するための5要素、4つ目はRecyclerViewのレイアウトです。
Designなどでこれだけを表示してもただの枠ですが、このレイアウトが要素を表示する土台となります。

```Kotlin:flagment_item_list.xml
<androidx.recyclerview.widget.RecyclerView
        android:id="@+id/recycle"
        android:layout_gravity="center"
        android:layout_width="match_parent"
        android:layout_height="0dp"
        android:layout_marginStart="16dp"
        android:layout_marginEnd="16dp"
        app:layout_constraintBottom_toBottomOf="parent"
        app:layout_constraintStart_toStartOf="parent"
        app:layout_constraintTop_toBottomOf="@id/toolbar"
        tools:listitem="@layout/item_layout" />
```

### RecyclerViewに表示する要素のレイアウト
RecyclerViewを実装するための5要素、最後はリスト要素のレイアウトです。
土台となるRecycerViewの上で、たくさんリサイクルされるレイアウトです。

- カテゴリ一覧画面

```Kotlin:category_layout.xml
<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout 
    xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_width="match_parent"
    android:layout_height="wrap_content"
    xmlns:app="http://schemas.android.com/apk/res-auto">

    <TextView
        android:id="@+id/category"
        android:background="?colorPrimaryVariant"
        android:textColor="?android:textColorSecondary"
        android:textSize="25sp"
        android:gravity="center"
        android:layout_width="120dp"
        android:layout_height="120dp"
        android:layout_marginVertical="20dp"
        app:layout_constraintStart_toStartOf="parent"
        app:layout_constraintTop_toTopOf="parent"
        app:layout_constraintBottom_toBottomOf="parent"
        app:layout_constraintEnd_toEndOf="parent"/>

</androidx.constraintlayout.widget.ConstraintLayout>
```
Design上ではただの四角形に見えますが、これを元にadapter継承クラス内(onBindViewHolder)でタイトルなどが設置され、要素viewとして表示されます。
<img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1396691/3f3b5e17-176d-9933-505a-1f91feffee86.png" width=60%>


- タスク一覧画面

```Kotlin:item_layout.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"
    android:orientation="horizontal">

    <TextView
        android:id="@+id/item_number"
        android:layout_width="40dp"
        android:layout_height="40dp"
        android:gravity="center"
        android:textAppearance="?attr/textAppearanceListItem" />

    <TextView
        android:id="@+id/label"
        android:layout_width="0dp"
        android:layout_height="40dp"
        android:layout_weight="1"
        android:layout_marginEnd="10dp"
        android:gravity="center_vertical"
        android:textAppearance="?attr/textAppearanceListItem" />

    <ImageView
        android:id="@+id/check_icon"
        android:layout_width="40dp"
        android:layout_height="40dp"
        android:layout_marginEnd="10dp"
        android:src="@drawable/ic_baseline_check_circle_outline_24" />

</LinearLayout>
```
Desing上では以下のように見えます。
横長のレイアウトとチェックマークがありますが、これを元に、Adapter継承クラス内(onBindViewHolder)でタスク名の追加、チェックマークの表示/非表示切り替え設定を行います。
<img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1396691/902e8e39-2950-9ee7-752f-656309210273.png" width=60%>


以上から、画面ができたことでTODOアプリらしくなってきました。
![LinearLayout.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1396691/501790ea-d596-de73-50c7-7bf7cfe74a6c.png)
ただし、まだまだ完成には遠いです。

続編に続きます。
※作成後、リンクを貼りますので少々お待ちください。

参考
・[Developers：RecyclerView で動的リストを作成する](https://developer.android.com/guide/topics/ui/layout/recyclerview?hl=ja)
・[RecyclerView の基本](https://maku77.github.io/android/ui/recycler-view.html)
・[Qiita：RecyclerViewの基本](https://qiita.com/naoi/items/f8a19d6278147e98bbc2)
・[Developers：DividerItemDecoration]()