1
0

More than 1 year has passed since last update.

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

Last updated at Posted at 2022-12-07

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

やりたいこと

・Preferences DataStoreに設定値を保存する
・設定値を呼び出し、他の設定値を更新する

躓いたこと

・処理が無限ループになる

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

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

参考

簡単な例としてトークン(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 オブジェクト全体ではなく、UserPreferences オブジェクトを公開します。そうするには、Flow をマッピングし、目的のブール値を取得し、キーに基づいて UserPreferences オブジェクトを作成する必要があります。

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

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

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

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

refreshTokenは更新していないため、StateFlowの性質から2回目以降の発火が抑制されます。
Flowは初心者だと難しい概念が多いですよね。一緒に頑張っていきましょう。

1
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
1
0