3
1

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 3 years have passed since last update.

Android #2Advent Calendar 2020

Day 22

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

Last updated at Posted at 2020-12-21

はじめに

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を使うなんてどうでしょうか

おわりに

  • おもいこみはよくない
  • けんきょさがだいじ
3
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
3
1

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?