Proto DataStoreの使い方を、まとめます。
DataStoreはPreferencesとProto DataStoreの2つがあります。
Proto DataStoreは、データの識別子にカスタムデータクラスのフィールドを使うタイプです。
カスタムデータクラスはprotoスキーマの定義から自動生成される仕組みになっています。
このカスタムデータクラスの作成に一手間かかるため、使い勝手はPreference DataStoreよりも劣ります。
あえて、Proto DataStoreを使う理由が無いのであれば、Preferences DataStoreで十分です。
※環境:Android Studio Narwhal 3 Feature Drop | 2025.1.3
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オブジェクトの再作成
}
}
関連記事:
