Preferences DataStoreの無限ループエラー対処
やりたいこと
・Preferences DataStoreに設定値を保存する
・設定値を呼び出し、他の設定値を更新する
躓いたこと
・処理が無限ループになる
同様のエラーで困っている方の助けになれば幸いです。
2022/12現在、Preferences DataStore
はSharedPreferences
に代わるものとして改善された新しいデータ ストレージ ソリューションとされています。
参考
簡単な例としてトークン(String)、リフレッシュトークン(String)、有効期限(Long)の3情報を保存している
DataStore
があるとします。DataStore
を扱うUserLocalDataSource
クラスは以下の実装になります。
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]
}
}
}
@Module
@InstallIn(SingletonComponent::class)
object DataStore {
@Provides
@Singleton
fun providePreferencesDataStore(
@ApplicationContext context: Context
): DataStore<Preferences> = PreferenceDataStoreFactory.create(
produceFile = {
context.preferencesDataStoreFile("userData")
}
)
}
次に、作成した関数loadState()でトークンの有効期限を確認し、失効していたらリフレッシュトークンを用いてトークンを更新するロジックを考えます。
viewModel.isTokenExpired(){
viewModel.loadToken().collect{
if (it != null) {
val token = it
viewModel.refreshToken(token)
}
}
}
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
の、↑の性質を利用して、Flow
をStateFlow
に変換しcollectする方法で解決できます。
viewModel.isTokenExpired(){
viewModel.loadToken().stateIn(lifecycleScope).collect{
if (it != null) {
val token = it
viewModel.refreshToken(token)
}
}
}
refreshTokenは更新していないため、StateFlow
の性質から2回目以降の発火が抑制されます。
Flow
は初心者だと難しい概念が多いですよね。一緒に頑張っていきましょう。