2
2

More than 1 year has passed since last update.

SharedPreferencesをJetpack DataStoreに置き換えて、HiltでDIする

Posted at

この記事について

HiltでProvideしていた「SharedPreferences」をJetpackの「Preferences DataStore」に置き換えようという内容です。
「Proto DataStore」には触れていません。

モジュール構成

今までの構成

Repository(自作)
 -> SharedPref(自作)
 -> SharedPreferences
 -> デバイスのファイル「/data/data/{package-name}/shared_prefs/forecast_pref.xml」

これを

Repository(自作)
 -> SharedPref(自作)
 -> DataStore<Preferences>
 -> デバイスのファイル「/data/data/{package-name}/files/forecast_pref_datastore.xml」

に変えていきます。

dependencies

依存関係に、以下を足します。

app/build.gradle
dependencies {
    implementation "androidx.datastore:datastore-preferences:1.0.0"
}

HiltのModule定義なし(@Provide定義しない場合)

SharedPrefを以下の様に改造しました。
元々は、ModuleクラスでSharedPrefをProvideしていましたが、これをやめて自動でProvideされる様にしています。

SharedPref.kt
+ @Singleton
class SharedPref @Inject constructor(
-     private val sharedPreferences: SharedPreferences,
+     @ApplicationContext private val context: Context,
) {
+     // ① DataStoreのインスタンスを取得する
+     private val Context.dataStore: DataStore<Preferences> by preferencesDataStore(name = "forecast_pref_datastore")

+     // ② DataStoreのキーを生成する
+     private val keyAreaDataUpdatedAt = longPreferencesKey(SharedPrefKey.AreaDataUpdatedAt.name)

-     var areaDataUpdatedAt: Long
-         get() = sharedPreferences.getLong(SharedPrefKey.AreaDataUpdatedAt.name, 0L)
-         set(value) = sharedPreferences.edit {
-             putLong(SharedPrefKey.AreaDataUpdatedAt.name, value)
-         }

+     // ③ 値を取得する
+     val areaDataUpdatedAt: Flow<Long> = context.dataStore.data.map { preferences ->
+         preferences[keyAreaDataUpdatedAt] ?: 0L
+     }

+     // ④ 値を更新する
+     suspend fun editAreaDataUpdatedAt(value: Long) {
+         context.dataStore.edit { preferences ->
+             preferences[keyAreaDataUpdatedAt] = value
+         }
+     }
}

使う側はこんな感じです。

AreaRepositoryImpl.kt
    // 値を取得する
    @OptIn(ExperimentalCoroutinesApi::class)
    override fun getArea(): Flow<Future<Area>> {
-       return flow {
-           val areaDataUpdatedAt = sharedPref.areaDataUpdatedAt
-           emit(areaDataUpdatedAt.isPassedTime(minutes = 10))
+       return sharedPref.areaDataUpdatedAt
+           .map { areaDataUpdatedAt ->
+               areaDataUpdatedAt.isPassedTime(minutes = 10)
        }.flatMapConcat { needServerData ->
            if (needServerData) {
                getAreaFromServer()
            } else {
                getAreaFromLocal()
            }
        }.onStart {
            emit(Future.Proceeding)
        }.catch { cause ->
            emit(Future.Error(cause))
        }.flowOn(dispatchers)
    }

    // 値を設定する
    private fun saveArea(area: Area): Flow<Future<Area>> {
        return flow<Future<Area>> {
            /* いろいろ処理 */
-           sharedPref.areaDataUpdatedAt = System.currentTimeMillis()
+           sharedPref.editAreaDataUpdatedAt(System.currentTimeMillis())
            emit(Future.Success(area))
        }.catch { cause ->
            emit(Future.Error(cause))
        }
    }

HiltのModule定義あり(@Provideする場合)

テストやなんかで、DataStore自体をSharedPrefとかの自作クラスで直接インスタンス化したくない場合は、Module定義で以下の様に書くことができます。

preferencesDataStoreは、Module定義なしの時は「by」で入れていましたが、関数の時はgetValueを読んでいます。
getValueの第2引数は使用されませんので、dummyPropを定義してそれを渡すようにしています。

InfrastructureModule.kt
@Module
@InstallIn(SingletonComponent::class)
object InfrastructureModule {
    private val dummyProp: Nothing? = null

    @Provides
    fun providePrefDataStore(
        @ApplicationContext context: Context,
    ): DataStore<Preferences> = 
        preferencesDataStore(name = "forecast_pref_datasource")
            .getValue(context, InfrastructureModule::dummyProp)
}

完成

ソースはこちらです。
Module定義なし
Module定義あり

2
2
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
2
2