Proto DataStoreで様々なデータを扱う

Kenji Abe
9 min readDec 9, 2022
Photo by Natasya Chen on Unsplash

Proto DataStoreはSharedPreferencesに比べて多くのデータタイプを扱うことができるようになっています。Proto DataStoreでどのようなデータを扱うことができるかを見ていきます。Protocol Buffersの話がメインになります。

また、最後にprotoファイルを変更するときに気をつけることを簡単に書いています。

Scalar Value Type

数値、文字列、booleanなどの基本的な型です。未設定の場合デフォルトで数値は0、文字列は空文字、booleanはfalseを返すようになっています。

message Settings {
int32 id = 1;
string name = 2;
}prot

Kotlinのコードは以下のようになります。

// Write
settings.updateData { currentSettings ->
currentSettings.toBuilder()
.setId(1)
.setName("test")
.build()
}

// Read
val settings = settings.data.first()
val id = settings.id
val name = settings.name

Enum

enumも扱えるようになっています。

enum Theme {
LIGHT = 0;
DARK = 1;
}

message Settings {
int32 id = 1;
string name = 2;
Theme theme = 3;
}

enumの最初の値は必ず0にする必要があります。またこの値が未設定の場合に返却されるデフォルト値になります。

// Write
currentSettings.toBuilder()
.setTheme(Theme.DARK)
.build()

// Read
val settings = settings.data.first()
val theme = settings.theme

Message Type

別のMessage Typeをフィールドの型として定義することが可能になっています。

message Account {
string email = 1;
string phone = 2;
}

message Settings {
// ...
Account account = 4;
}

Kotlinのコードで以下のようになっています。Builderを使って別のMessage Typeを生成してからセットしています。

// Write
currentSettings.toBuilder()
.setAccount(
Account.newBuilder()
.setEmail("protobuf@example.com")
.setPhone("1234")
)
.build()

// Read
val settings = settings.data.first()
val account = settings.account
val email = account.email
val phone = account.phone

未設定の場合の注意ですが、この例の account はnullにはならず、各フィールドがデフォルト値が設定(この場合はemailとphoneが空文字)されたインスタンスが取得されます。

Repeated

repeated を使うとListのような複数の値を扱うことも可能です。

message Settings {
// ...
repeated string tags = 5;
}

フィールド定義の先頭に repeated をつけるだけです。

// Write (既存のデータに追加する)
currentSettings.toBuilder()
.addTags("tag")
.build()

// Write (一度クリアして一度に複数件追加する)
currentSettings.toBuilder()
.clearTags() // クリア
.addAllTags(listOf("tag1", "tag2"))
.build()

// Read
val settings = settings.data.first()
val tags = settings.tagsList
tags.forEach {
// ...
}

書き込む方法は1件ずつ追加していくこともできますし、一度クリアしてから複数件をまとめて追加することもできます。

repeated はScalar Typeだけではなく、Message Typeも使用できるので、以下のような定義も可能です。

message Settings {
// ...
repeated Account accounts = 6;
}

Map

key-valueを扱うことができるものになります。

message Settings {
// ...
map<string, string> string_map = 7;
}

keyに指定できるのは整数ガタかstringのみになりEnumやMessage Typeなどは使用できません。

// Write
currentSettings.toBuilder()
.putStringMap("key1", "value1")
.putStringMap("key2", "value2")
.build()

// Read
val settings = settings.data.first()
val value1 = settings.stringMapMap["key1"]
val value2 = settings.stringMapMap["key2"]

読み込むときに指定のキーが存在しないときはnullが返却されるようになっています。

Oneof

複数のフィールドのうち1つだけが設定できる状態を扱いたいとき使用します。メモリ節約にもなります。

message Settings {
// ...
oneof sampleOneOf {
int32 one_of_int = 8;
string one_of_string = 9;
}
}

oneof と名前を指定して、その中に複数のフィールドを設定します。このどちらかのみが設定でき、どちらかに値をセットしたら自動的に残りの値をクリアされます。

// Write
currentSettings.toBuilder()
.setOneOfInt(1)
.build()

// Read (whenで分岐)
val settings = settings.data.first()
when (settings.sampleOneOfCase) {
Settings.SampleOneOfCase.ONEOFINT -> {
Log.d(TAG, "oneOfInt = ${settings.oneOfInt}")
}
Settings.SampleOneOfCase.ONEOFSTRING -> {
Log.d(TAG, "oneOfString = ${settings.oneOfString}")
}
Settings.SampleOneOfCase.SAMPLEONEOF_NOT_SET -> {
// not set
Log.d(TAG, "Not set")
}
}

// Read (それぞれのフィールドで値がセットされてるかを確認)
val settings = settings.data.first()
if (settings.hasOneOfInt()) {
Log.d(TAG, "oneOfInt = ${settings.oneOfInt}")
}
if (settings.hasOneOfString()) {
Log.d(TAG, "oneOfString = ${settings.oneOfString}")
}

書き込みは特に変わりはないのですが、読み込むときにどのフィールドがセットされてるかを確認してから読み込むことになります。

Optional

Protobufで未設定の場合はデフォルト値が返却されるため、値がセットされたものなのかデフォルト値なのかが分かりません。そういう場合は optional を使用することでセットされたかどうかを判定できるようになります。

message Settings {
// ...
optional int32 code = 10;
}

読み込むときに値がセットされているかどうかを判定することが可能です。

// Write
currentSettings.toBuilder()
.setCode(0)
.build()

// Read
val settings = settings.data.first()
if (settings.hasCode()) {
val code = settings.code
} else {
// Not set
}

hasXxxx というメソッドで値がセットされたかを判定することが可能になります。

protoファイル変更するときの注意

protoファイルを変更するときはいくつか注意があります。

Field Numberは重要なもので、この値は変更しないようにします。

フィールドを削除する場合はもField Numberは再利用しないようにします。 reserved でField Numberを再利用しないように予約してしまうか、フィールドを削除せずに OBSOLETE_ のプレフィックスをつけて定義自体は残しておきます。

フィールドを追加した場合ですが、古いバージョンで保存されたものを読み込んだときも問題なく解析することできます。追加されたフィールドはデフォルト値が返却されるので、そこは注意する必要があります。

詳しくはドキュメントに詳しく書いてあるので、一度読んでおくと良いと思います。

参考

--

--

Kenji Abe

Programmer / Gamer / Google Developers Expert for Android, Kotlin / @STAR_ZERO