公式ページを眺めているだけではわからないJetpack DataStoreのあれこれ

# はじめに

Android Jetpackファミリーに [DataStoreの1.0.0-alpha01が追加されました](https://developer.android.com/topic/libraries/architecture/datastore)。

[公式ページ](https://developer.android.com/topic/libraries/architecture/datastore)を抜粋しますと、

```
Jetpack DataStore is a data storage solution that allows you to store key-value 
pairs or typed objects with protocol buffers. DataStore uses Kotlin coroutines and
Flow to store data asynchronously, consistently, and transactionally.

If you're currently using SharedPreferences to store data, consider migrating to 
DataStore instead.

Jetpack DataStoreはデータストレージソリューションの一種で、キー-値のペア方式で保存したり、
ProtocolBufferを使った型情報付きのデータの保存ができたりするよ。DataStoreはKotlinコルーチンと
Flowを使っていて、データの保存をトランザクション的に一貫性を保ちながら非同期に行うこともできるんだ。

もしいま君がSharedPreferenceを使ってデータを保存しているならDataStoreへの移行を検討してみてよ。

```
とのことす（適当な位置での改行＆クソ邦訳：筆者）。

というわけで、少し触ってみましたのでその気づきメモです。

なるべく上記のページに書いていないことを書きます。

# 誤記・欠落

[公式ページ](https://developer.android.com/topic/libraries/architecture/datastore)にいくつか誤記・欠落があった（2020/09/04時点）ので補足します。

## セットアップの欠落

### Preferences DataStoreだけ使う場合

公式ページではモジュール階層の `build.gradle` の `dependencies` にライブラリーを追加するだけしか書いていませんが、実際にはJavaのバージョン指定も要るようです。

```build.gradle（モジュール階層）
android {
  ...

  compileOptions {
      sourceCompatibility JavaVersion.VERSION_1_8
      targetCompatibility JavaVersion.VERSION_1_8
  }
  kotlinOptions.jvmTarget = "1.8"

  ...
}

dependencies {
  ...
  // Preferences DataStore
  implementation "androidx.datastore:datastore-preferences:1.0.0-alpha01"

  // Proto DataStore
  implementation "androidx.datastore:datastore-core:1.0.0-alpha01"
  ...
}

```

### Proto DataStoreだけ使う場合

前節の内容に加え、Protocol Buffer関係のモジュールの追加が必要です。
これらがないとprotoファイルからクラスが自動生成されません。

今回はProtocol Buffer公式のJava実装およびそのGradleプラグインを使ってクラスを自動生成するようにします。

最初に、プロジェクト階層のbuild.gradleです。

```プロジェクト階層のbuild.gradle
buildscript {
    ...
    dependencies {
        ...
+        classpath 'com.google.protobuf:protobuf-gradle-plugin:0.8.13'
        ...
    }
    ...
}
...
```

つぎに、モジュール階層のbuild.gradleです。

```モジュール階層のbuild.gradle
...
+ apply plugin: 'com.google.protobuf'

android {
  ...

+    compileOptions {
+        sourceCompatibility JavaVersion.VERSION_1_8
+        targetCompatibility JavaVersion.VERSION_1_8
+    }
+    kotlinOptions.jvmTarget = "1.8"

  ...
}

dependencies {
  ...
+     // Preferences DataStore
+     implementation "androidx.datastore:datastore-preferences:1.0.0-alpha01"

+     // Proto DataStore
+     implementation "androidx.datastore:datastore-core:1.0.0-alpha01"
  ...
}

```

## サンプルのprotoの誤記

`int` -> `int32` ですね。

```settings.proto

syntax = "proto3";

option java_package = "com.example.application";
option java_multiple_files = true;

message Settings {
+ //int example_counter = 1;
+   int32 example_counter = 1;
}
```

# 設計思想

DataStore自体は任意のファイル形式・任意のデータ形式に対応した設定管理モジュールとして振る舞えるよう設計されています。

1.0.0-alpha01では、スキーマレス形式の代表兼SharedPreferenceの代替として `PreferenceDataStore` を、 スキーマフル形式の代表として `ProtoDataStore` が用意されています。

# 関数

`SharedPreferences` の `edit` と違い、`DataStore` の`edit` は `suspend function` です。
よって呼び出す側も `suspend function` またはコルーチンコンテキストで実行を強制されます。
メインスレッドで叩いてしまうのをコンパイラレベルで防ぐことができるというわけですね。

# 保存できる型

## `PreferenceDataStore`

デフォルトのシリアライザーで対応している型はSharedPreferencesと同じ。つまり、

* `Boolean`
* `Float`
* `Int`
* `Long`
* `String`
* `Set<String>`

の6つとなります。Double、List、Map、Parcelableは保存できない一方、整数型はIntとLongの2種類に分かれています。
また、nullは保存できません。

**2020/11/27追記**

1.0.0-alpha03でDouble型が追加されました。
https://developer.android.com/jetpack/androidx/releases/datastore#1.0.0-alpha03

## `ProtoDataStore`

Protocol Buffer(protobuf) を使っているので、IDLであるprotoが許す限り自由な型を扱えます。

## ほかの形式

シリアライザーはインタフェース化されており、DataStoreの生成時に引数として指定することができます。
シリアライザーさえ書けば任意の型・任意の形式で読み書きできます（後述）。

# ファイル仕様

## 読み書き形式

デフォルトでは、protobuf（バイナリー）形式で読み書きされます。
これはPreferenceDataStoreであっても同じです。
~~「protobufだったらIDLが必要なんじゃないの？」と思いましたが、保存時にMap変数が抱えている値の型を調べ、protoファイル相当のスキーマデータを作っています。これによりスキーマレスのように扱えるという仕組みです。~~ 嘘でした。
内部実装として [このようなprotoファイル](https://cs.android.com/androidx/platform/frameworks/support/+/androidx-main:datastore/datastore-preferences-core/datastore-preferences-proto/src/main/proto/preferences.proto;l=1?q=datastore%2Fdatastore-preferences-core%2Fdatastore-preferences-proto%2Fsrc%2Fmain%2Fproto%2Fpreferences.proto&sq=&ss=androidx%2Fplatform%2Fframeworks%2Fsupport) およびそこから生成されたクラスを持っており、これを使ってシリアライズ/デシリアライズを行っています。

前述のとおり、シリアライザーを入れ替えれば任意の形式で読み書きすることができます。

## 保存パス

### PreferenceDataStore

デフォルトでは `<filesDir>/datastore/<name>.preferences_pb` に保存されます（nameは引数で指定する値）。

例：パッケージ名が `com.example.hoge`、nameに `settings` を指定した場合、 `/data/data/com.example.hoge/files/datastore/settings.preferences_pb` 。

保存する場所やファイル名は引数で変更できますが、拡張子は `.preferences_pb` でないと例外送出されます。

### ProtoDataStore

デフォルトでは `<filesDir>/datastore/<fileName>` に保存されます（fileNameは引数で指定する値）。

例：パッケージ名が `com.example.hoge`、nameに `settings.pb` を指定した場合、 `/data/data/com.example.hoge/files/datastore/settings.pb` 。

PreferenceDataStoreと違い、こちらは拡張子まで含めて指定するようです。

# 感想

* 筆者の感じたメリット
    * Flowでデータを扱えるのでRxプログラミングしやすい
    * ProtoDataStoreは、設定値もスキーマを使って厳密に扱いたいけどRoom（SQLite）を使うほどでもないという「帯に短し襷に長し」の問題にちょうどいいかもしれない
* 筆者の感じたデメリット
    * ~~保存形式がprotobufのバイナリーであり、JSONやYAMLのように気軽に人間が読み書きできない（PreferenceDataStoreであっても！）~~ -> **デメリットというほどの問題ではありませんでした。詳しくは [DataStoreの保存形式をProtocol Bufferを使いつつJSONにする](https://qiita.com/que9/items/c3f181ade4bba148dcab) をご覧ください**
    * Jetpack Securityが対応していない＝SharedPreferenceのように暗号化して管理できない
    * PreferenceDataStoreで保存可能な型がSharedPreference同等なせいでListもMapもParcelableも保存できない。
      かといってProtoDataStoreを使うにはprotobuf関係のモジュールのセットアップ、protoファイルの管理など面倒くさすぎる

例えばいま書いてるアプリではSharedPreferenceの値をFlowで扱わなきゃいけないような大仰な状況には直面していないので、SharedPreferenceから入れ替えるメリットはないかなーといった感じです。
PreferenceDataStoreがList・Map・Parcelableを扱えるならワンチャンあったんですけどもね。
かといってProtoDataStoreはちょっとヘビーですし、ProtoDataStoreより軽くとなるとKotlinx SerializationやMoshiを使って自分でシリアライザーを書き下ろす必要があるんですが、いやそれなら最初からJSONで読み書きすればええやん！となるのでやはり必要なさそう。

とはいえ、まだアルファ版でこれから機能が充実していくライブラリーです。
筆者が感じたような問題も当然解決されていくでしょうししばらく動向を見守りたいと思います。