脱SharedPreferences、DataStoreに移行してみよう

# はじめに
Android開発について学んでいる時、SharedPreferencesは序盤に触れるものではないでしょうか？
つい最近その代替となるライブラリがリリースされたので紹介したいと思います。

# Jetpack DataStoreとは
[Jetpack DataStore](https://developer.android.com/topic/libraries/architecture/datastore)は[Shared Preferences](https://developer.android.com/reference/android/content/SharedPreferences?hl=ja)の代わりに利用できる新しいライブラリです。Kotlin CoroutineとFlowを使っているので、非同期の読み取りや書き込みを簡単に行うことができます。(執筆時点の最新版は1.0.0-alpha02です)

現状まだ解説されているページが少なく、いろいろな情報が断片的に散らばっているような状況なので調べたことを整理しつつ、纏めたいと思います。

# Preferences DataStoreとProto DataStore
DataStoreにはPreferences DataStoreとProto DataStoreの2種類が存在します。
すごく簡単に説明するとPreferences DataStoreはSharedPreferencesを置き換えるもの、Proto DataStoreはそれよりもっと自由に、型には厳格に値を保存したい時に使えます。

この記事では主にPreferences DataStoreについて詳しく解説します。

### 注意点
部分的な更新や参照整合性を必要とするデータを永続化したい場合はDataStoreではなく、Roomを使用してください。
また、SharedPreferencesを利用していた際と同様に大規模なデータを保存したい場合にもRoomを使用しましょう。

## 比較表
[Prefer Storing Data with Jetpack DataStore](https://android-developers.googleblog.com/2020/09/prefer-storing-data-with-jetpack.html)より抜粋

| Feature                                                    | SharedPreferences                                       | PreferencesDataStore                                          | ProtoDataStore                                                | 
| ---------------------------------------------------------- | ------------------------------------------------------- | ------------------------------------------------------------- | ------------------------------------------------------------- | 
| Async API                                                  | ✅ (only for reading changed values, via&nbsp;listener) | ✅ (via Flow)                                                 | ✅ (via Flow)                                                 | 
| Synchronous API                                            | ✅ (but not safe to call on UI thread)                  | ❌                                                            | ❌                                                            | 
| Safe to call on UI thread                                  | ❌*                                                     | ✅ (work is moved to&nbsp;Dispatchers.IO&nbsp;under the hood) | ✅ (work is moved to&nbsp;Dispatchers.IO&nbsp;under the hood) | 
| Can signal errors                                          | ❌                                                      | ✅                                                            | ✅                                                            | 
| Safe from runtime exceptions                               | ❌**                                                    | ✅                                                            | ✅                                                            | 
| Has a transactional API with strong consistency guarantees | ❌                                                      | ✅                                                            | ✅                                                            | 
| Handles data migration                                     | ❌                                                      | ✅ (from SharedPreferences)                                   | ✅ (from SharedPreferences)                                   | 
| Type safety                                                | ❌                                                      | ❌                                                            | ✅ with&nbsp;Protocol Buffers                                 | 

\*SharedPreferencesは、UIスレッド上で呼び出しても安全に見える同期APIを持っていますが、実際にはディスクI/O操作を行います。さらに、apply() は fsync() で UI スレッドをブロックします。保留中の fsync() コールは、サービスが開始または停止するたびに、またアプリケーションのどこかでアクティビティが開始または停止するたびにトリガーされます。UI スレッドは apply() によってスケジュールされた保留中の fsync() コールでブロックされ、しばしば[ANR](https://developer.android.com/topic/performance/vitals/anr)の原因となります。
**SharedPreferencesは、実行時例外として解析エラーをスローします。
(よく分からなかったので機械翻訳そのままです、おかしな点があったらコメントで教えてください)

### 簡単な説明
表の中でのSharedPreferencesとの差について簡単に書きます、特に指定がない場合はPreferences DataStore、Proto DataStore共通です。

* 非同期処理を[Flow](https://developer.android.com/reference/java/util/concurrent/Flow)を用いて行う
* UIスレッドで呼び出した場合I/O処理用に予約されたスレッド(Dispatchers.IO[^1])で実行される
* エラーハンドリングが出来る
* 実行時例外を安全に防ぐことができる
* 強力な一貫性が保証されたトランザクションAPIを持っている
* SharedPreferencesからのデータ移行に対応している
* 型安全(Proto DataStoreのみ)

というように、SharedPreferencesと比較した際に異なる点が多くあることが分かります。
これらの変更点は、現在のAndroid開発においてはメリットとして働くことが多いと思います。

# Preferences DataStore
前述したように、Preferences DataStoreはSharedPreferences を完全に置き換えるものです。
SharedPreferencesと同様にスキーマを事前に定義せずに、キーに基づいてデータにアクセスします。

Preferences DataStoreを使う際の流れを以下で解説します。

## 保存できる型
Preferences DataStoreで保存できる型はSharedPreferencesと同様で

* int
* long
* float
* boolean
* String
* Set\<String>

となります。

## 依存関係の追加
Preferences DataStoreを利用するため下記を追加します。

```gradle:build.gradle
implementation "androidx.datastore:datastore-preferences:1.0.0-alpha02"
```
これだけだと``Adding support for Java 8 language features could solve this issue.``のエラーが出てしまうため

```gradle:build.gradle
kotlinOptions {
    jvmTarget = '1.8'
}
```
も追加します、公式ページには書かれていないので注意が必要です。

##　値の保存
SharedPreferencesとは異なり、事前にデータ型とキー名を使用してオブジェクトを作成する必要があります。

```Kotlin:Key
private object PreferenceKeys {
    val ID = preferencesKey<Int>("id")
    val NAME = preferencesKey<String>("name")
}
```

次に、DataStoreオブジェクトを作成します。

```Kotlin:DataStoreObject
private  val dataStore : DataStore <Preferences> by lazy {
    createDataStore(name = "example")
}
```

editを利用して値を書き込みます。

```Kotlin:updateName
private suspend fun updateName(name: String) {
    dataStore.edit {
        it[PreferenceKeys.NAME] = name
    }
}
```
一連の利用方法はたったこれだけです。

内容は``/data/app/PACKAGE_NAME/files/datastore``に保存されます。

## SharedPreferencesからの移行
現状SharedPreferencesを利用しているアプリからPreferences DataStoreに移行したいという場面は多くあると思います。
その場合も簡単に移行することができるようになっています。

```Kotlin:DataStoreObject
private val dataStore: DataStore<Preferences> = 
    createDataStore(
        name = "example",
        migrations = listOf(SharedPreferencesMigration(this, "shared_preferences_name"))
    )
```
createDataStoreを行う際に移行元のSharedPreferencesをmigrationsに指定するだけで簡単に移行ができるようになっています。
キーは一度だけSharedPreferencesから移行されるので、DataStoreに移行後にSharedPreferencesを使用しないよう注意してください。

## 値の読み出し
値の読み出しは

```Kotlin:read
val preferencesFlow: Flow<String> = dataStore.data
    .map { preferences ->
        preferences[PreferenceKeys.NAME] ?: "Not Found"
    }
}
```
のように行うことができます、しかしこれは例外が発生した際に捕捉することができません。これではせっかくのDataStoreの利点が活かせないので例外を捕捉するように書き換えましょう。


## 例外処理
SharedPreferencesではファイルの入出力が失敗しても例外を吐いてくれませんでした。
この問題に遭遇した際のトレースには困った方が多いと思いますが、DataStoreではしっかりと解決されています。

[Working with Preferences DataStore](https://developer.android.com/codelabs/android-preferences-datastore#5)によると
>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. If a different type of exception was thrown, prefer re-throwing it.
>> DataStoreはファイルからデータを読み込みます。そのため、データの読み込み中にエラーが発生するとIOExceptionがスローされます。
投げられた例外がIOExceptionであった場合は、map() の前にcatch()を使用し、emptyPreferences()を発行することで対処できます。別のタイプの例外がスローされた場合は、再スローすることをお勧めします。

とあります。
例外をしっかりと捕捉できるよう改善されていますね。具体的にコードにすると以下のような感じです。

```Kotlin:Exception
val preferencesFlow: Flow<String> = dataStore.data
    .catch { exception ->
        if (exception is IOException) {
            emit(emptyPreferences())
        } else {
            throw exception
        }
    }.map { preferences ->
        preferences[PreferenceKeys.NAME] ?: "Not Found"
    }
```
これでPreferences DataStoreの簡単な使い方は理解できたのではないでしょうか？
DataStoreのもう一つであるProto DataStoreについても簡単に説明したいと思います。


# Proto DataStore
Proto DataStoreについては大まかな流れだけを解説します。利用する機会があれば後日まとめて別記事として書きたいと思っています。

Proto DataStoreはprotobufをベースとした型安全で高度なオブジェクトデータストレージを利用するためのスキーマを作成できます。
[Working with Proto DataStore](https://developer.android.com/codelabs/android-proto-datastore#4)によると
> One of the downsides of SharedPreferences and Preferences DataStore is that there is no way to define a schema or to ensure that keys are accessed with the correct type. Proto DataStore addresses this problem by using Protocol buffers to define the schema. Using protos DataStore knows what types are stored and will just provide them, removing the need for using keys.
>>SharedPreferencesとPreferences DataStoreの欠点の一つは、スキーマを定義したり、キーが正しい型でアクセスされることを保証する方法がないことです。Proto DataStoreは、スキーマを定義するためにプロトコルバッファを使用することで、この問題に対処します。Proto DataStoreを使用すると、どのような型が保存されているかを知っていて、それを提供するだけなので、キーを使用する必要はありません。

とあり、Preferences DataStoreとどちらを利用するかは型安全が必要であるかが選ぶ要素になりそうです。

## 保存できる型
Proto DataStoreはProtocol Bufferを使っているため、.protoファイルが扱える型 = Proto DataStoreで利用できる型となります。
詳しく書くと非常に長くなるため、詳しくは[Language Guide (proto3)](https://developers.google.com/protocol-buffers/docs/proto3)を確認してください。

## 使用の流れ
大まかな流れとして

* プロトファイルの作成
* シリアライザーの作成
* データストアの作成
* データの書き込み、読み取り

となります。
データ型とキー名を使用してオブジェクトを作成する工程が不要になった代わりにプロトファイルの作成、シリアライザーの作成という過程が増えるため、Preferences DataStoreとは前半部分の書き方がかなり変わります。
しかし、達成したい目標は基本的には同じです(Preferences DataStoreの[Codelab](https://developer.android.com/codelabs/android-preferences-datastore)と比較しながら、Proto DataStoreの[Codelab](https://developer.android.com/codelabs/android-proto-datastore)を行うと理解がスムーズに行えると思います)。

# まとめ
まだalpha版で今後の大きな変更があり得ることも考え今すぐに急いで移行するべき！というものではないですが、個人的にはエラーハンドリングがしやすい点だけでも移行する価値はあるのかなーと考えています。
質問や間違っている点などは気軽にコメント、編集リクエストでご指摘お願いします。
# 参考サイト
[Jetpack DataStore](https://developer.android.com/topic/libraries/architecture/datastore)
[Shared Preferences](https://developer.android.com/reference/android/content/SharedPreferences?hl=ja)
[Prefer Storing Data with Jetpack DataStore](https://android-developers.googleblog.com/2020/09/prefer-storing-data-with-jetpack.html)
[Android 開発の最新情報をご紹介する「Now in Android」#25](https://developers-jp.googleblog.com/2020/09/now-in-android-25.html)
[Working with Preferences DataStore](https://developer.android.com/codelabs/android-preferences-datastore)
[Working with Proto DataStore](https://developer.android.com/codelabs/android-proto-datastore)

[^1]: ディスクおよびネットワークIOに最適化されたスレッド