Repositoryから取得したLiveDataをViewModelでキャッシュしておくには？

# こうだ

![cached_live_data.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/82681/a4ce2b7e-ebc6-33d7-7e1b-1d97fdb32834.gif)

※Developer options の Don't keep activities を有効にしています

# SavedStateHandle

AndroidのライフサイクルではActivityインスタンスやFragmentインスタンスはいつ破棄されるかわかりません。
これらが持つViewModelインスタンスも同様です。

破棄され、再生成されるとそれまでのメモリー上のデータは破棄されます。これはよろしくありません。

これを避けるためにデータの保存/復元を行う必要があり、そのヘルパークラスであるSavedStateHandleを使うと楽に実装できるというのは以前書かせていただきました。

[ViewModelのデータを保存/復元するにはSavedStateHandleを使う
](https://qiita.com/que9/items/4c93197c4e4de10e483e)

# Activityが再生成されると最初から読み込みし直し

上記のケースは、EditTextの入力値のような、データがViewModel内で完結するものでした。
では、**データがリポジトリー層から取得される場合はどうすればいいのでしょうか？**

例を示します。

```Kotlin
class User(...): Parcelable() {
    ...
}

class UserRepository {
    fun getUser(): LiveData<User> {
        ...
    }
}


class MainViewModel(
    private val userRepository: UserRepository
) {
    val user: LiveData<User> = ？ // userRepository.getUser()したいが…
}
```

上記のコードは `UserRepository` の戻り値がLiveDataになっています。
SavedStateHandleは初期値にLiveDataを取ることはできませんから、SavedStateHandleは使えません。

「何もしない」のも1つの答えです。
通常、リポジトリー層自体が永続化の役割を持っているのですから、あえてViewModelで保存/復元を行わなくてもいいというものです。

```Kotlin
class MainViewModel(
    private val userRepository: UserRepository
) {
    val user: LiveData<User> = userRepository.getUser() // 何も考えずに代入しとけばいいんだよ！
}
```

しかし、 LiveDataにデータがemitされるのに時間がかかる場合はどうでしょうか？

```Kotlin
class UserRepository {
    fun getUser(): LiveData<User> {
        liveData(GlocalContext.coroutineContext + Dispatchers.IO) {
            // サーバーとの通信やデータ整形などのとても時間のかかる処理
            val user: User = veryHeavyProcess()
            
            emit(user)
        }
    }
}
```

このような場合でも、最終的にはデータは表示されます。しかし再度読み込みが行われるため表示が完了するまで時間はかかります。ユーザーからしたらストレスになるでしょう。

// ---------- このへんから追記＆修正 ----------

これを解決するためにはどこかでデータをキャッシュしておく必要があります。

リポジトリー層でキャッシュしておくこともできるでしょう。
実際、今回の例もリポジトリー層でキャッシュした方がいい雰囲気のコードです（※後で気づいたが例示として悪かった）。

しかし、リポジトリー層で取得したエンティティクラスを画面表示（プレゼンテーション）用にこねくり回している場合、こねくりまわしたデータは既にリポジトリー層の手から離れているためキャッシュ処理はViewModel層が受け持つよりほかありません。

// ---------- このへんまで ----------

したがって、 **「画面にはgetUserを使いつつViewModelでgetUserを監視しキャッシュする。ViewModel再生成時はキャッシュした値を初期値として使い、遅延して読み込まれたgetUserの値を使う」** ということをする必要があります。

# CachedLiveDataを作ったぞ

上記を解決するためのクラス **`CachedLiveData`** および `CachedLiveData` を簡単に生成できるSavedStateHandleの拡張関数を作りました。

[CachedLiveData with SavedStateHandle extension
](https://gist.github.com/quwac/6dd0a749ee2cb7f83d63a0f0d32173cb)

以下のように使います。

```Kotlin
class MainViewModel(
    private val handle: SavedStateHandle,
    private val userRepository: UserRepository
) {
    val user: LiveData<User> = handle.getCachedLiveData<User>("user", userRepository.getUser()) // これだけ
}
```

第1引数には `SavedStateHandle#getLiveData` と同じくキー名を指定します。
第2引数には ソースとして使用したいLiveDataを指定します。
これだけで「画面にはgetUserを使いつつViewModelでgetUserを監視しキャッシュする。ViewModel再生成時はキャッシュした値を初期値として使い、遅延して読み込まれたgetUserの値を使う」ことをしてくれます。

結果は冒頭に貼った動画のとおりです。
Simple LiveDataが何も使わず単純に `userRepository.getUser` を使った結果、 CachedLiveDataが `CachedLiveData` を使った結果です。
両者画面表示が完了したところから動画は始まっています。
ホームボタンを押してランチャー画面を表示させ、アプリアイコンをタップしてアプリを再表示させたところ、Simple LiveDataの方はデータの再読み込みのため表示されるまでに時間がかかるのに対し、CachedLiveDataの方はデータが最初から表示されています。
これはCachedLiveDataによりデータがキャッシュされていたということです。

# 仕組み

[CachedLiveData with SavedStateHandle extension
](https://gist.github.com/quwac/6dd0a749ee2cb7f83d63a0f0d32173cb)

を見ていただければわかるとは思いますが、念のため仕組みの解説をしておきます。

```CachedLiveData.kt
fun <T : Any> SavedStateHandle.getCachedLiveData(key: String, source: LiveData<T>): LiveData<T> {
    val cache = getLiveData<T>(key)
    return CachedLiveData(source, cache)
}

private class CachedLiveData<T>(
    source: LiveData<T>,
    private val cache: MutableLiveData<T>
) : MediatorLiveData<T>() {

    ...

    private var isCacheInSource = true

    ...

    init {
        addSource(source) {
            if (isCacheInSource) {
                removeSource(cache)
                isCacheInSource = false

                ...
            }
            value = it
            cache.value = it
        }
        addSource(cache) {
            value = it
        }
    }

    ...

}
```

引数の`source` に `userRepository.getUser` が、`cache` には `handle.getLiveData("user")` が代入されます。

`CachedLiveData` は基本的には `source` の中身が反映され続けます。
しかし `source` の中身が最初に反映されるまでの間は `cache` の中身が反映されます。
`source` の中身が反映されるようになると `cache` の中身を反映する処理は停止します。そして、`source` の中身を `cache` の中身として反映する動作を始めます。

上記のほか、CachedLiveDataの観測状態と `cache` の観測状態を一致させる処理をつけたら完成です。

# おわりに

良きViewModelライフを！