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

https://developer.android.com/topic/libraries/architecture/datastore?hl=ja#kotlin

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

## モジュール構成

今までの構成

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

これを

Repository（自作）
　->　SharedPref（自作）
　->　<font color="red">DataStore&lt;Preferences&gt;</font>
　->　<font color="red">デバイスのファイル「/data/data/{package-name}/files/forecast_pref_datastore.xml」</font>

に変えていきます。

## dependencies
依存関係に、以下を足します。
```gradle:app/build.gradle
dependencies {
    implementation "androidx.datastore:datastore-preferences:1.0.0"
}
```

## HiltのModule定義なし（@Provide定義しない場合）
SharedPrefを以下の様に改造しました。
元々は、ModuleクラスでSharedPrefをProvideしていましたが、これをやめて自動でProvideされる様にしています。

```diff_kotlin: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
+         }
+     }
}
```

使う側はこんな感じです。
```diff_kotlin: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を定義してそれを渡すようにしています。

```diff_kotlin: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定義なし](https://github.com/jozuko/WeatherForecast/tree/a9964edca159a13b28b88ebc0f7d5e58dbe069a2)
[Module定義あり](https://github.com/jozuko/WeatherForecast/tree/1fa1e801d70f7bbbf60fd701b7758f4de7488f68)