SharedPreferencesからDataStore移行で気を付けること

## はじめに
この記事は [Android Advent Calendar 2023](https://qiita.com/advent-calendar/2023/android) 8日目の記事です。

新人プログラマがアプリのリニューアルに伴い、SharedPreferencesから[Preferences DataStore](https://developer.android.com/topic/libraries/architecture/datastore)へ移行したお話になります。

:::note warn
本記事のスニペットの動作保証はしていません。サンプルとして記載しています。
:::

## SharedPreferencesからDataStoreへ移行

[公式コードラボ](https://developer.android.com/codelabs/android-preferences-datastore#6)に一括移行の実装が載っています。とても便利そうなのですが、実際にはアプリ影響が大きく怖いので一括での移行は行いませんでした。
`keysToMigrate`で移行するキーを指定できるため、こちらを利用して下記のようにデータごとに移行していきました。

```kotlin
private val Context.dataStore: DataStore<Preferences> by preferencesDataStore(
    name = "file_name",
    produceMigrations = { context ->
        listOf(
            SharedPreferencesMigration(
                context = context,
                sharedPreferencesName = "shared_prefs_name",
                keysToMigrate = setOf("example_key")
            )
        )
    }
)
```

:::note info
DataStoreの保存パスは以下でした。
```
/data/data/app.package.name/files/datastore/file_name.preferences_p
```
:::

## DataStoreで読み取り
DataStoreの読み取り、書き込みはFlowを用いて非同期で行われます。
またSharedPreferencesと異なりDataStoreは値の読み取りに失敗した場合、IOExceptionを投げてくれます。

このことは[公式コードラボ](https://developer.android.com/codelabs/android-preferences-datastore#5)に記載がありますが、読み取り時は下記のように例外処理の実装が推奨されています。
```kotlin
val userPreferencesFlow: Flow<String> = dataStore.data
    .catch { exception ->
        if (exception is IOException) {
            emit(emptyPreferences())
        } else {
            throw exception
        }
    }.map { preferences ->
        preferences[PreferencesKeys.EXAMPLE_KEY] ?: "defaultValue"
    }
```

> As DataStore reads data from a file, IOExceptions are thrown when an error occurs while reading data. We can handle these by using the catch() Flow operator before map() and emitting emptyPreferences() in case the exception thrown was an IOException.

以下翻訳

> DataStore がファイルからデータを読み取るとき、データの読み取り中にエラーが発生すると IOExceptions がスローされます。これらは、map() の前に catch() フロー演算子を使用し、スローされた例外が IOException だった場合に emptyPreferences() を発行することで処理できます。

出典：https://developer.android.com/codelabs/android-preferences-datastore#5


## 同期的処理での注意点
前述した通りDataStoreの読み取り、書き込みはFlowを用いて非同期で行われます。

一方SharedPreferencesは同期的に扱えるため、元々は同期前提での実装になっている方が多いのではないでしょうか？

僕は当初なるべく呼び出し元を変更しないで同期的に扱えるように`runBlocking()`を用いることで解消していました。

ここが気を付けるべきポイントです。

以下のような感じです。

```kotlin:移行前
class SampleRepository(private val sharedPreferences: SharedPreferences) {
    fun getExampleKey(): String = sharedPreferences.getString("example_key", null) ?: "defaultValue"
}
```

```kotlin:移行後
class SampleRepository(private val dataStore: DataStore<Preferences>) {
    fun getExampleKey(): String = runBlocking {
        dataStore.data.map { preferences ->
            preferences[EXAMPLE_KEY] ?: "defaultValue"
        }.first()
    }
}
```

こうすることでANRが発生するようになってしまいました。。

`runBlocking()`はUIスレッドをブロックしますが、当時正しく理解できておらず安易に利用してしまっていました。
同期的な処理については公式ドキュメントに下記の注意書きがありました。

> 注意: 可能な限り、DataStore のデータ読み取りでスレッドがブロックされないようにしてください。UI スレッドをブロックすると、ANR や UI ジャンクが発生する可能性があります。また、他のスレッドをブロックすると、デッドロックが発生する可能性があります。

出典：https://developer.android.com/topic/libraries/architecture/datastore?hl=ja#synchronous


結局下記のようにsuspend関数に置き換え、呼び出し元でCoroutineScopeを立ち上げるように修正しました。
これが至る箇所にあると結構辛いです。。

```kotlin:最終的なもの
class SampleRepository(private val dataStore: DataStore<Preferences>, private val ioDispatcher: CoroutineDispatcher) {
    suspend fun getExampleKey(): String = withContext(ioDispatcher) {
        dataStore.data.map { preferences ->
            preferences[EXAMPLE_KEY] ?: "defaultValue"
        }.first()
    }
}

// 呼び出し元
scope.launch {
    sampleRepository.getExampleKey()
    // DataStorから読み取った値で何かする
}
```

## 所感
公式ドキュメントではDataStoreへの移行を検討するよう促されているため、今後SharedPreferencesがサポートされなくなる可能性もあるのかと考えると移行してしまえば安心かと思います。

ただ、移行対応をしている途中から思っていたのですが、アプリそのものとしてはSharedPrefrencesでも提供できる機能とは変わりないんですよね。。

移行作業自体も影響が大きいので、通常は正式に非推奨になるなどなにかしらきっかけがないと、移行するモチベーションを持つのは中々難しいのかな、と思ったりしました。

> 現在 SharedPreferences を使用してデータを保存している場合は、DataStore に移行することを検討してください。

出典：https://developer.android.com/topic/libraries/architecture/datastore?hl=ja

## おわりに
アドカレ初参加だったのですが、書くきっかけが出来て素敵なイベントだと思いました。
来年は有用な記事が書けるように頑張りたいです。
ありがとうございました。