Preferences DataStoreの無限ループエラー対処

# Preferences DataStoreの無限ループエラー対処

### やりたいこと
・Preferences DataStoreに設定値を保存する
・設定値を呼び出し、他の設定値を更新する

### 躓いたこと
・処理が無限ループになる

同様のエラーで困っている方の助けになれば幸いです。

2022/12現在、`Preferences DataStore` は`SharedPreferences` に代わるものとして改善された新しいデータ ストレージ ソリューションとされています。

参考 

https://developer.android.com/codelabs/android-preferences-datastore?hl=ja#0

簡単な例としてトークン(String)、リフレッシュトークン(String)、有効期限(Long)の3情報を保存している
`DataStore`があるとします。`DataStore`を扱う`UserLocalDataSource`クラスは以下の実装になります。

```UserLocalDataSource.kt
class UserLocalDataSource @Inject constructor(
    private val dataStore: DataStore<Preferences>
){
    private object PreferencesKeys {
        val KEY_NAME = stringPreferencesKey("TOKEN")
        val KEY_NAME2 = stringPreferencesKey("REFRESH_TOKEN")
        val KEY_NAME3 = longPreferencesKey("EXPIRE")
    }

    suspend fun saveToken(token: String){
        dataStore.edit { preferences: MutablePreferences ->
            preferences[PreferencesKeys.KEY_NAME] = token
        }
    }

    suspend fun saveToken(refreshToken: String){
        dataStore.edit { preferences: MutablePreferences ->
            preferences[PreferencesKeys.KEY_NAME2] = refreshToken
        }
    }

    suspend fun saveExpire(expire: Long){
        dataStore.edit { preferences: MutablePreferences ->
            preferences[PreferencesKeys.KEY_NAME3] = expire
        }
    }

    fun loadToken(): Flow<String?>{
        val counter = PreferencesKeys.KEY_NAME
        return dataStore.data
            .map { preferences ->
                preferences[counter]
            }
    }

    fun loadRefreshToken(): Flow<String?>{
        val counter = PreferencesKeys.KEY_NAME2
        return dataStore.data
            .map { preferences ->
                preferences[counter]
            }
    }

    fun loadExpire(): Flow<Long?>{
        val counter = PreferencesKeys.KEY_NAME3
        return dataStore.data
            .map { preferences ->
                preferences[counter]
            }
    }
}
```

```DataStore.kt
@Module
@InstallIn(SingletonComponent::class)
object DataStore {
    @Provides
    @Singleton
    fun providePreferencesDataStore(
        @ApplicationContext context: Context
    ): DataStore<Preferences> = PreferenceDataStoreFactory.create(
        produceFile = {
            context.preferencesDataStoreFile("userData")
        }
    )
}

```

次に、作成した関数loadState()でトークンの有効期限を確認し、失効していたらリフレッシュトークンを用いてトークンを更新するロジックを考えます。

```SampleFragment.kt
viewModel.isTokenExpired(){
    viewModel.loadToken().collect{
        if (it != null) {
            val token = it
            viewModel.refreshToken(token)
        }
    }
}
```

```SampleViewModel.kt
fun refreshToken(token: String){
        viewModelScope.launch {
            // resultは更新後のトークンを含むAPIレスポンス
            val result = userRepository.refreshToken(token)
            result?.let {
                val token = it.token
                val expire = it.expire
                userRepository.saveToken(token)
                userRepository.saveExpire(expire)
            }
        }
    }
```

これで一件落着かとおもったのですが、いざこのプログラムを実行してみると、
APIリクエストが無限に飛んでしまいます。収集しているrefreshToken自体は更新していないのに、いったいなぜでしょうか。

##### 何が原因だったのか？
> Preferences DataStore は、設定が変更されるたびに出力される Flow<Preferences> に保存されたデータを公開します。Preferences オブジェクト全体ではなく、UserPreferences オブジェクトを公開します。そうするには、Flow<Preferences> をマッピングし、目的のブール値を取得し、キーに基づいて UserPreferences オブジェクトを作成する必要があります。

`Preferences DataStore` は、あるデータを更新するとオブジェクト全体に対して更新通知(flow)が送られます。
つまり、viewModel.refreshToken()→userRepository.saveToken()
と実行されると、オブジェクトが更新されるのでviewModel.loadToken().collect{}が再度呼ばれることになります。
よって、無限ループになってしまうわけですね。。。

https://at-sushi.work/blog/24/

>launchInしたタイミングで直近の値が1件流れてきます。
値の設定がvalueで行え、coroutines scopeがなくても問題ありません。
また、同じ値は流れない、連続して値が変更されると、最後の値のみ流れてくる、といった特徴があります。

`StateFlow`の、↑の性質を利用して、`Flow`を`StateFlow`に変換しcollectする方法で解決できます。

```SampleFragment.kt
viewModel.isTokenExpired(){
    viewModel.loadToken().stateIn(lifecycleScope).collect{
        if (it != null) {
            val token = it
            viewModel.refreshToken(token)
        }
    }
}
```
refreshTokenは更新していないため、`StateFlow`の性質から2回目以降の発火が抑制されます。
`Flow`は初心者だと難しい概念が多いですよね。一緒に頑張っていきましょう。