はじめに
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つを見つけることができました。
- Protocol Buffer(Java版)
- gRPC-Kotlin/JVM (protobufというかgRPCの実装)
- Wire
- Kroto+ (protocのKotlinクラス生成プラグイン)
どれを使ってもいいのですが、今回はKotlinっぽい素直なデータクラスが生成されるWireを使って実装することにしました。
セットアップ
プロジェクトレベルのbuild.gradleにGradle用のWireプラグインを追加します。
buildscript {
repositories {
jcenter()
}
dependencies {
...
+ classpath 'com.squareup.wire:wire-gradle-plugin:3.5.0'
...
}
}
モジュールレベルのbuild.gradleにプラグインとライブラリーを追加します。また、Java8対応も行います。
...
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でやりたいんだけど?
- A:残念ながらWire 3.5.0ではまだ対応していないようです。ただ 対応のためのコードは作られ始めている ので近い将来に実現できそうです
- Q:ProtoDataStoreじゃなくてPreferenceDataStoreでやりたいんだけど?
- A:残念ながらPreferenceDataStoreはSerializerインスタンスを渡せないのでできません。
PreferenceDataStoreも結局は こういうprotoファイル を作ってラップしているに過ぎないので、ProtoDataStoreの仕組みを使ってPreferenceDataStoreっぽいものを作るのはそう難しくなさそうです
- A:残念ながらPreferenceDataStoreはSerializerインスタンスを渡せないのでできません。
- Q:JSONじゃなくてYAMLで保存したいんだけど?
- A:MoshiでJSON文字列に変換してからJacksonやSnakeYAMLを使うなんてどうでしょうか
おわりに
- おもいこみはよくない
- けんきょさがだいじ