search
LoginSignup
0

More than 1 year has passed since last update.

posted at

updated at

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

はじめに

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

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

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

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

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

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

protobufのリファレンス には、

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つを見つけることができました。

どれを使ってもいいのですが、今回は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 クラスだけです。

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

これを、

// 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でやりたいんだけど?
  • Q:ProtoDataStoreじゃなくてPreferenceDataStoreでやりたいんだけど?
    • A:残念ながらPreferenceDataStoreはSerializerインスタンスを渡せないのでできません。 PreferenceDataStoreも結局は こういうprotoファイル を作ってラップしているに過ぎないので、ProtoDataStoreの仕組みを使ってPreferenceDataStoreっぽいものを作るのはそう難しくなさそうです
  • Q:JSONじゃなくてYAMLで保存したいんだけど?
    • A:MoshiでJSON文字列に変換してからJacksonやSnakeYAMLを使うなんてどうでしょうか

おわりに

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

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
What you can do with signing up
0