ListAdapterを使う場合、最初からRoomを使うのが結局一番楽だった

# 概要

せっかくここ数ヶ月で最新のAndroidの開発のキャッチアップを行おうとしていたんだから、以前作成した[赤外線リモコンの送信機](https://qiita.com/mickie895/items/efcfe32b7c410adf577a)を動かすアラームアプリを開発しようとした。
簡単なプロトタイプを作ろうとしていたが、なんだかんだで最初からRoomを利用して開発するのが一番楽だったため、そこに至るまでの流れを公開しておく。

## 作成しているアプリの目的

AndroidからBLEでESP32と通信し、一定の時間にリモコンの信号を送信するアプリケーション。今回の範囲では、まずはAndroidの中でアラームを起動するタイミングを保存するようにする。
とりあえず、アラーム名と何時、何曜日に起動するかを編集できるようにすればよいだろうから、下記のようなUIを考える。

![Screenshot_20230620_233031.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/395414/46172263-2b4f-24c1-791b-d46d1ae28ec4.png)

# プログラムの構成

とりあえずで、こんな感じのクラスの構成を考えてみた。
上のUIでは各編集ボタンを押したときに対応するダイアログを出そうとしているが、蛇足になるので省略してある。

```plantuml
@startuml
class Fragment{
    ViewModel viewModel
}
class ListAdapter{
    List<Schedule> items
}
class ViewModel{
    LiveData getAlarms()
}
class Repository{
    List<Schedule> alarms
}
class Schedule{
    int alarmId
    string alarmName
    bool[] WeeldayEnables
    int hour
    int minute
}

Fragment o-- ListAdapter
Fragment o-- ViewModel
ViewModel o-- Repository
Repository o-- Schedule
ListAdapter o-- Schedule

@enduml
```

# 適当に実装したときの問題点

例えば、編集したいからって上記のScheduleクラスやリポジトリを下記のように実装して、ListAdapterのsubmitList(items)で表示の更新をしようとしても2つの理由で一切更新されずに困ることになる。

```kotlin
data class Schedule(
    var alarmId: Int,
    var alarmName: String
    // 以下省略
)

class Repository(val items MutableList<Schedule> = mutableListOf()) {
}
```

## ListAdapter.submitListの挙動により更新されない

[ListAdapterのソースコード](https://cs.android.com/androidx/platform/frameworks/support/+/androidx-main:recyclerview/recyclerview/src/main/java/androidx/recyclerview/widget/ListAdapter.java?q=file:androidx%2Frecyclerview%2Fwidget%2FListAdapter.java%20class:androidx.recyclerview.widget.ListAdapter)でsubmitListのメソッドから順番にメソッドを追っていくと、最終的に下記のjavaのメソッドにいきあたる。

```java
    public void submitList(@Nullable final List<T> newList,
            @Nullable final Runnable commitCallback) {
        // incrementing generation means any currently-running diffs are discarded when they finish
        final int runGeneration = ++mMaxScheduledGeneration;

        if (newList == mList) {
            // nothing to do (Note - still had to inc generation, since may have ongoing work)
            if (commitCallback != null) {
                commitCallback.run();
            }
            return;
        }

// 以下省略

    }
```

Javaの文脈で```newList==mList```である場合、何もしないと言っている。
つまり、MutableListを作成し、それに対して配列の追加などの操作を行ってからsubmitList()を行った場合、リストの参照自体は変わっていないため```newList==mList```が成立してしまい、なにもしなくなってしまう。

## DiffUtilが働かない

ListAdapterを使って実装する場合、[公式のサンプルソース](https://github.com/android/views-widgets-samples/blob/main/RecyclerViewKotlin/app/src/main/java/com/example/recyclersample/flowerList/FlowersAdapter.kt)のようにDiffUtilを作成する必要がある。

Q. ところで、data classの変数を書き換え可能にして、変数を書き換えてDiffUtilが働くようにした場合、DiffUtilのoldItemとnewItemには何が入ってくるだろうか。
A. 一つのインスタンスの中身を書き換えているので、どちらも同じインスタンスが入ることになる。

要は別の参照をもつインスタンスを対象にしない限り、DiffUtilは機能不全を起こす。

# 適当に実装したときの対処と自分の選択

このように更新が行われていない状態になってしまった場合、対応方法は主に2つある。

- [RecyclerView.Adapter](https://developer.android.com/reference/androidx/recyclerview/widget/RecyclerView.Adapter)のnotify～～系のメソッドを手動で呼び出し、更新があったことを明示する
- リスト内に格納されているインスタンスは都度新しいインスタンスを作成させるようにし、ListAdapterに送信するリストは毎回コピーを行うようにする

自分の場合、アラームの設定情報はデータとして保存しておくことが前提となっていたため、上記の2つのどちらかをプロトタイプのためだけに書き捨てるのも面倒に思い、結局「最初からRoom使うのが一番コスト低いや」という結論に至った。
もしRoomを使わない場合、適切なnotify系のメソッドを都度起動するよりListやデータ構成に手を入れたほうが扱いが良さそうに見える。
そもそもリファレンスのsubmitList関数自体に「非同期でリストの中身の調査を行う」と書かれていたこともあるので、submitListで送信した関数をすぐに書き換えたりすると表示が正しく反映されなくなることも予想できるというのも理由になっている。

# まとめ

- ListAdapterを利用する場合は値を格納する不変のオブジェクトを利用しよう
- リストを渡す際にはリストのコピーを渡すようにしておこう
- Roomなどの、既存のライブラリの利用をまずは考えよう