DataStoreの保存形式をProtocol Bufferを使いつつJSONにする

# はじめに

Jetpack DataStoreはSharedPreferenceに代わるデータ保存フレームワークです。

[公式ページを眺めているだけではわからないJetpack DataStoreのあれこれ](https://qiita.com/que9/items/54c60d9cbca1dfaffdbf)

以前上記の記事を書いたときに、

> 保存形式がprotobufのバイナリーであり、JSONやYAMLのように気軽に人間が読み書きできない（PreferenceDataStoreであっても！）

と書いたのですが、これは正確ではないというかほとんど誤り👿みたいな認識だったので訂正したいと思います。
結論からいうと、 **protobufを使いつつもJSON化は楽にできる** 。

# proto3はJSON変換を保証している

[protobufのリファレンス](https://developers.google.com/protocol-buffers/docs/proto3#json) には、

> Proto3 supports a canonical encoding in JSON, making it easier to share data between systems. 
>
> Proto3はJSONの正規化されたエンコーディングをサポートしており、システム間でのデータ共有を容易にしています。

とあります。
つまり、proto3では **protoファイルで表現できるデータ構造は必ずJSONで表現できることが仕様レベルで保証されている** ということです（proto2ではそういう仕様はなかった）。

したがって、 **proto3の仕様サポートに手厚いライブラリーは、データクラスからJSONへのシリアライズ/JSONからデータクラスへのデシリアライズをサポートしている** のです。
この仕組みを使えば、Proto DataStoreのデータをJSON形式で保存することはprotobufフレームワークの範疇で解決可能です。わざわざ自前でデータクラス⇔JSON変換処理を書き下ろす必要なんてありません。

いやあ……無知すぎて恥ずかしい……(｡>﹏<｡)

# WireでDataStoreの保存形式をJSONにする

## protobufライブラリーの選定

proto3に対応したprotobuf実装ですが、ちょっと探しただけでも以下の4つを見つけることができました。

* [Protocol Buffer（Java版）](https://github.com/protocolbuffers/protobuf/tree/master/java)
* [gRPC-Kotlin/JVM](https://github.com/grpc/grpc-kotlin) （protobufというかgRPCの実装）
* [Wire](https://github.com/square/wire)
* [Kroto+](https://github.com/marcoferrer/kroto-plus) （protocのKotlinクラス生成プラグイン）



どれを使ってもいいのですが、今回はKotlinっぽい素直なデータクラスが生成されるWireを使って実装することにしました。

## セットアップ

プロジェクトレベルのbuild.gradleにGradle用のWireプラグインを追加します。

```プロジェクトレベルのbuild.gradle

buildscript {
    repositories {
        jcenter()
    }
    dependencies {
        ...
+        classpath 'com.squareup.wire:wire-gradle-plugin:3.5.0'
        ...
    }
}

```

モジュールレベルのbuild.gradleにプラグインとライブラリーを追加します。また、Java8対応も行います。

```モジュールレベルのbuild.gradle
...
apply plugin: 'com.squareup.wire'
...

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-alpha05"
+
+    // Proto DataStore
+    implementation "androidx.datastore:datastore-core:1.0.0-alpha05"

+    def wire_grpc_version = '3.5.0'
+    implementation "com.squareup.wire:wire-moshi-adapter:$wire_grpc_version"

   ...
}

+ wire {
+     kotlin {
+         javaInterop = true
+     }
+ }
```

ライブラリー（dependencies）については、DataStoreモジュールに加え、WireのMoshiアダプターモジュール 
`wire-moshi-adapter` を追加しています。
WireではJSON変換にMoshiを使用します。 `wire-moshi-adapter` はMoshiとMoshiのアダプタークラス `WireJsonAdapterFactory` の依存性解決を行ってくれます。

末尾の `wire { kotlin { javaInterop = true} }` がポイントです。
これを付与すると生成されるデータクラス内にクラス内クラス `Builder` が生成されるようになります。
MoshiでJSON変換する場合、この `Builder` を頼りに変換が行われます。
`Builder` がないと例外送出して変換に失敗してしまうのでご注意ください。

上記のとおり設定をしておくとビルド時にprotoファイルからデータクラスが自動生成されるようになります。

## シリアライザーの実装

いよいよ本題です。
今回は [公式ページにあるSettingsクラスの例]()をそのまま使います。

JSONで保存/読み込みを行うにあたって、書き換えるのは `SettingsSerializer` クラスだけです。

```kotlin
// BEFORE
object SettingsSerializer : Serializer<Settings> {
  override val defaultValue: Settings = Settings.getDefaultInstance()

  override fun readFrom(input: InputStream): Settings {
    try {
      return Settings.parseFrom(input)
    } catch (exception: InvalidProtocolBufferException) {
      throw CorruptionException("Cannot read proto.", exception)
    }
  }

  override fun writeTo(
    t: Settings,
    output: OutputStream) = t.writeTo(output)
}
```

これを、

```kotlin
// AFTER
object SettingsSerializer : Serializer<Settings> {
    private val moshi = Moshi.Builder()
        .add(WireJsonAdapterFactory())
        .build()
    private val adapter = moshi.adapter(Settings::class.java)

    override val defaultValue: Settings
        get() = Settings()

    override fun readFrom(input: InputStream): Settings {
        return try {
            input.source().buffer().use { source ->
                adapter.fromJson(source)!!
            }
        } catch (t: Throwable) {
            // failure
            Log.w("readFrom", "readFrom failed.", t)
            Settings()
        }
    }

    override fun writeTo(
        t: Settings,
        output: OutputStream
    ) {
        try {
            output.sink().buffer().use { sink ->
                adapter.toJson(sink, t)
            }
        } catch (t: Throwable) {
            // failure
            Log.e("writeTo", "writeTo failed.", t)
        }
    }
}
```

こうです。むちゃくちゃ簡単やん……😊

ポイントは `moshi` インスタンスの生成時にMoshiのアダプターファクトリーとして `WireJsonAdapterFactory` インスタンスを渡しているところです。
このインスタンスを渡しておくとWireで自動生成されたクラス（より正確には、自動生成されたクラスが継承している `com.squareup.wire.Message` クラス）のJSONシリアライズ/デシリアライズができるようになります。
`readFrom` / `writeTo` ではもはやWireは関係なく、単純にMoshiを使って読み書き処理を行うようにしているだけです。

あとは実際に読み込み/書き換えを行い、Android Studioの `Device File Explorer` 機能を使って `/data/data/＜アプリのパッケージ名＞/files/datastore/settings.pb` ファイルを見てみてください。JSONで書き込まれていることがわかると思います。

# Q&A

* Q：MoshiじゃなくてKotlinx Serializationでやりたいんだけど？
    * A：残念ながらWire 3.5.0ではまだ対応していないようです。ただ [対応のためのコードは作られ始めている](https://github.com/square/wire/pull/1851) ので近い将来に実現できそうです
* Q：ProtoDataStoreじゃなくてPreferenceDataStoreでやりたいんだけど？
    * A：残念ながらPreferenceDataStoreはSerializerインスタンスを渡せないのでできません。
      PreferenceDataStoreも結局は [こういうprotoファイル](https://cs.android.com/androidx/platform/frameworks/support/+/androidx-master-dev:datastore/datastore-preferences-core/datastore-preferences-proto/src/main/proto/preferences.proto) を作ってラップしているに過ぎないので、ProtoDataStoreの仕組みを使ってPreferenceDataStoreっぽいものを作るのはそう難しくなさそうです
* Q：JSONじゃなくてYAMLで保存したいんだけど？
    * A：MoshiでJSON文字列に変換してからJacksonやSnakeYAMLを使うなんてどうでしょうか

# おわりに

* おもいこみはよくない
* けんきょさがだいじ