Proto DataStoreの使い方を、まとめます。
DataStoreはPreferencesとProto DataStoreの2つがあります。
Proto DataStoreは、データの識別子にカスタムデータクラスのフィールドを使うタイプです。
カスタムデータクラスはprotoスキーマの定義から自動生成される仕組みになっています。
このカスタムデータクラスの作成に一手間かかるため、使い勝手はPreference DataStoreよりも劣ります。
あえて、Proto DataStoreを使う理由が無いのであれば、Preferences DataStoreで十分です。
※環境:Android Studio Hedgehog | 2023.1.1 Patch 2
com.google.protobuf 0.9.5
androidx.datastore:datastore:1.1.7
com.google.protobuf:protobuf-javalite:4.32.0
目次
Proto DataSotre
DataStoreはデータを「識別子と値のペア」で記録します。
Proto DataStoreは、カスタムデータクラスのフィールドを識別子にしたDataStoreです。
フィールドを参照して、データのアクセスを行います。
データはデータクラスのフィールドに格納されています。
環境設定
build.gradleへProtocol Buffersのブラグインとライブラリを指定します。ライブラリはバージョン番号を揃えてください。
※プラグインについては「google/protobuf-gradle-plugin」を参照
※ライブラリについては「Protocol Buffers[Lite]」を参照
また、DataStorのライブラリを指定します。
※ライブラリについては「ライブラリ>DataStore」を参照
plugins { ... id("com.google.protobuf") version "0.9.5" } android { ... } protobuf { protoc { artifact = "com.google.protobuf:protoc:4.32.0" } // Generates the java Protobuf-lite code for the Protobufs in this project. See // https://github.com/google/protobuf-gradle-plugin#customizing-protobuf-compilation // for more information. generateProtoTasks { all().forEach { task -> task.builtins { create("java") { option("lite") } } } } } dependencies { implementation("androidx.datastore:datastore:1.1.7") implementation("com.google.protobuf:protobuf-javalite:4.32.0") // implementation("com.google.protobuf:protobuf-kotlin-lite:4.32.0") ... }
DataSotreの例
アプリの設定(状態)を保存する例です。
ポイントは4つです。
- (1)protoスキーマの定義(***.proto)
- (2)カスタムデータクラスの自動生成
- (3)シリアライザ―の作成
- (4)DataStoreインスタンスの作成
- (5)コルーチンでデータの読み出し
- (6)コルーチンでデータの書き込み
syntax = "proto3"; option java_package = "パッケージ名.datasotre"; option java_multiple_files = true; message Settings { int32 app_state0 = 1; sint32 app_state1 = 2; sfixed32 app_state2 = 3; }
// Generated by the protocol buffer compiler. DO NOT EDIT! // NO CHECKED-IN PROTOBUF GENCODE // source: datasotre.proto // Protobuf Java Version: 4.32.0 package パッケージ名.datasotre; /** * Protobuf type {@code Settings} */ @com.google.protobuf.Generated public final class Settings extends com.google.protobuf.GeneratedMessageLite< Settings, Settings.Builder> implements // @@protoc_insertion_point(message_implements:Settings) SettingsOrBuilder { private Settings() { } public static final int APP_STATE0_FIELD_NUMBER = 1; private int appState0_; /** * <code>int32 app_state0 = 1;</code> * @return The appState0. */ @java.lang.Override public int getAppState0() { return appState0_; } private void setAppState0(int value) { appState0_ = value; } private void clearAppState0() { appState0_ = 0; } ... }
// ↓↓ (4)DataStoreインスタンスの作成 val Context.settingsDataStore: DataStore<Settings> by dataStore( fileName = "settings.pb", serializer = SettingsSerializer ) // ↓↓ (3)シリアライザ―の作成 object SettingsSerializer : Serializer<Settings> { // override val defaultValue: Settings = Settings.getDefaultInstance() override val defaultValue: Settings = Settings.getDefaultInstance().toBuilder() .setAppState0(1000) // 初期値 .setAppState1(1100) // 初期値 .setAppState2(1200) // 初期値 .build() override suspend fun readFrom(input: InputStream): Settings { try { return Settings.parseFrom(input) } catch (exception: InvalidProtocolBufferException) { throw CorruptionException("Cannot read proto.", exception) } } override suspend fun writeTo( t: Settings, output: OutputStream ) = t.writeTo(output) }
private const val UNDEFINE = -1 class MainActivity : ComponentActivity() { private var state0: Int = UNDEFINE private var state1: Int = UNDEFINE private var state2: Int = UNDEFINE override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) enableEdgeToEdge() val state0Flow: Flow<Int> = settingsDataStore.data // (5)データの読み出し .map { settings -> settings.appState0 } // 常に更新、通信経路を維持(スレッドをブロックしない) lifecycleScope.launch { state0Flow.collect { state0 = it } } val state1Flow: Flow<Int> = settingsDataStore.data .map { settings -> settings.appState1 } // 実行で更新(スレッドをブロックしない) lifecycleScope.launch(Dispatchers.Default) { state1 = state1Flow.first() } val state2Flow: Flow<Int> = settingsDataStore.data .map { settings -> settings.appState2 } // 実行で更新(スレッドをブロックする) state2 = runBlocking { state2Flow.first() } setContent { ... } } }
... lifecycleScope.launch(Dispatchers.Default) { // (6)データの書き込み settingsDataStore.updateData { settings -> settings.toBuilder() .setAppState0(_nextState0) .build() } } ... lifecycleScope.launch(Dispatchers.Default) { // 記述はState0と同じ settingsDataStore.updateData { settings -> settings.toBuilder() .setAppState1(_nextState1) .build() } } ... lifecycleScope.launch(Dispatchers.Default) { // 記述はState0と同じ settingsDataStore.updateData { settings -> settings.toBuilder() .setAppState2(_nextState2) .build() } } ...
(1)protoスキーマの定義(***.proto)
Protocol BuffersのIDL(proto3)を使って、protoスキーマの定義を行います。
protoスキーマとは「Proto DataStoreのデータ構造」のことです。
定義は「***.proto」ファイルに記述し、「app/src/main/proto/」に配置する決まりになっています。
syntax = "proto3"; // IDLのリビジョン option java_package = "パッケージ名.datasotre"; // カスタムデータクラスのパッケージ名 option java_multiple_files = true; message Settings { // カスタムデータクラスのひな型 int32 app_state0 = 1; // 右辺はフィールド番号(初期値ではない) sint32 app_state1 = 2; sfixed32 app_state2 = 3; }
※IDL:Interface Description Language
※IDL(proto3)の言語仕様は「Language Guide (proto 3)」を参照
(2)カスタムデータクラスの自動生成
環境設定でプラグインが指定されていると、カスタムデータクラス(サンプルはSettings.java)が自動生成されます。
自動生成されない場合は、ビルドを再実行して下さい。
カスタムデータクラスには、データを格納するフィールド、シリアライザ・デシリアライザの実行ルーチンが含まれます。
// Generated by the protocol buffer compiler. DO NOT EDIT! // NO CHECKED-IN PROTOBUF GENCODE // source: datasotre.proto // Protobuf Java Version: 4.32.0 package パッケージ名.datasotre; /** * Protobuf type {@code Settings} */ @com.google.protobuf.Generated public final class Settings extends com.google.protobuf.GeneratedMessageLite< Settings, Settings.Builder> implements // @@protoc_insertion_point(message_implements:Settings) SettingsOrBuilder { private Settings() { } public static final int APP_STATE0_FIELD_NUMBER = 1; private int appState0_; /** * <code>int32 app_state0 = 1;</code> * @return The appState0. */ @java.lang.Override public int getAppState0() { return appState0_; } private void setAppState0(int value) { appState0_ = value; } private void clearAppState0() { appState0_ = 0; } ... }
(3)シリアライザ―の作成
シリアライザーを作成します。
実装するSerializer<T>インターフェースの「T」を、カスタムデータクラスに置き換えれば良いです。
シリアライズ(writeTo)・デシリアライズ(parseFrom)の実行ルーチンは、カスタムデータクラスが持っています。
// ↓↓ (3)シリアライザ―の作成 object SettingsSerializer : Serializer<Settings> { // override val defaultValue: Settings = Settings.getDefaultInstance() override val defaultValue: Settings = Settings.getDefaultInstance().toBuilder() .setAppState0(1000) // 初期値 .setAppState1(1100) // 初期値 .setAppState2(1200) // 初期値 .build() override suspend fun readFrom(input: InputStream): Settings { try { return Settings.parseFrom(input) } catch (exception: InvalidProtocolBufferException) { throw CorruptionException("Cannot read proto.", exception) } } override suspend fun writeTo( t: Settings, output: OutputStream ) = t.writeTo(output) }
defaultValueはファイルがない場合に、Flowで送信されるデータ(Settingsオブジェクト)です。デフォルトはSettings.getDefaultInstance()になっています。
この部分を、初期値を含むSettingsオブジェクトへ置き換えれば、初期値を定義できます。
(4)DataStoreインスタンスの作成
DataStoreインスタンスを作成します。
この時、データの書き込み先のファイル名とシリアライザーを指定します。
// ↓↓ (4)DataStoreインスタンスの作成 val Context.settingsDataStore: DataStore<Settings> by dataStore( fileName = "settings.pb", serializer = SettingsSerializer )
インスタンスはアプリケーション内に1つだけ作成します。ここでは、Contextインスタンスの拡張プロパティに代入しています。それにより、「アプリケーション内に1つ」を実現しています。
dataStore( )は引数にファイル名とシリアライザーを伴って、DataStoreインスタンスを返します。委譲プロパティなので、プロパティへの代入は初回の参照時です。
ちなみに、書き込み先のファイルは以下の場所にあります。
# pwd /data/user/0/パッケージ名/files/datastore ls -l total 8 -rw------- 1 u0_a174 u0_a174 11 2025-09-08 23:35 settings.pb
(5)コルーチンでデータの読み出し
Flow経由でSettingsを取得し、データの読み出しを行います。
ストリームデータはSettings(カスタムデータクラス)オブジェクトです。
ファイルが更新される度に、データが送信される点に注意して下さい。
受信したSettingsオブジェクトのフィールドを参照して、データを読み出します。
Flow.map(中間関数)を使って、Flowを個別データへ割り振り直すと便利です。
※中間関数については「Coroutine:Flowのストリームデータ変更(中間演算)」を参照
collectを使って
collectを使って受信する例です。
Flowの通信経路は維持されます。受信データが無い場合は、スレッドを休止して待ちます。
val state0Flow: Flow<Int> = settingsDataStore.data // (5)データの読み出し .map { settings -> settings.appState0 } // 常に更新、通信経路を維持(スレッドをブロックしない) lifecycleScope.launch { state0Flow.collect { state0 = it } }
経路が維持される限り、「ファイルの更新⇒データの送信⇒データの受信⇒設定(状態)の更新」を繰り返すので、アプリの設定は常に最新の値になります。
first()を使って
firstを使って受信する例です。
最初のデータを受け取るまで待機し、その後キャンセル信号を送信機へ送ります。
ですので、一回限りの読み出しで、Flowの通信経路は閉じられます。
val state1Flow: Flow<Int> = settingsDataStore.data .map { settings -> settings.appState1 } // 実行で更新(スレッドをブロックしない) lifecycleScope.launch(Dispatchers.Default) { state1 = state1Flow.first() }
ちなみに、Flowはホットストリームなので、Flowの通信経路はfirstを実行する度に作成されます。
runBlockingを使って
runBlockingを使って受信する例です。
スレッドはデータを受信するまでブロックされます。サンプルの場合はMainスレッドがブロックされます。
val state2Flow: Flow<Int> = settingsDataStore.data .map { settings -> settings.appState2 } // 実行で更新(スレッドをブロックする) state2 = runBlocking { state2Flow.first() }
ANR(Application Not Responding)を引き起こす可能性があるので、お勧めしません。
(6)コルーチンでデータの書き込み
updateDataの戻り値に、Settings(カスタムデータクラス)オブジェクトを返すと、データの書き込みが行われます。
このSettingsオブジェクトの作成をラムダ式で行っています。
Setteingsオブジェクトは、ビルダーを介して再作成が可能です。
lifecycleScope.launch(Dispatchers.Default) { // (6)データの書き込み settingsDataStore.updateData { settings -> settings.toBuilder() // ビルダー取り出し .setAppState0(_nextState0) // 値の変更 .build() // ビルド⇒Settingsオブジェクトの再作成 } }
関連記事: