LoginSignup
5
1

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

Posted at

はじめに

この記事は Android Advent Calendar 2023 8日目の記事です。

新人プログラマがアプリのリニューアルに伴い、SharedPreferencesからPreferences DataStoreへ移行したお話になります。

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

SharedPreferencesからDataStoreへ移行

公式コードラボに一括移行の実装が載っています。とても便利そうなのですが、実際にはアプリ影響が大きく怖いので一括での移行は行いませんでした。
keysToMigrateで移行するキーを指定できるため、こちらを利用して下記のようにデータごとに移行していきました。

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")
            )
        )
    }
)

DataStoreの保存パスは以下でした。

/data/data/app.package.name/files/datastore/file_name.preferences_p

DataStoreで読み取り

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

このことは公式コードラボに記載がありますが、読み取り時は下記のように例外処理の実装が推奨されています。

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()を用いることで解消していました。

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

以下のような感じです。

移行前
class SampleRepository(private val sharedPreferences: SharedPreferences) {
    fun getExampleKey(): String = sharedPreferences.getString("example_key", null) ?: "defaultValue"
}
移行後
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を立ち上げるように修正しました。
これが至る箇所にあると結構辛いです。。

最終的なもの
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

おわりに

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

5
1
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
5
1