Proto DataStore

投稿日:  更新日:

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はデータを「識別子と値のペア」で記録します。

DataStoreのデータフォーマット

Proto DataStoreは、カスタムデータクラスのフィールドを識別子にしたDataStoreです。

フィールドを参照して、データのアクセスを行います。

Proto 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)コルーチンでデータの書き込み
XXX.proto(protoスキーマの定義)Settings.java(自動生成される)
↓↓ (1)protoスキーマの定義(***.proto)

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;
}
↓↓ (2)カスタムデータクラスの自動生成

// 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;
}

protoスキーマの定義

※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を取得し、データの読み出しを行います。

Proto DataStoreのFlow

ストリームデータは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オブジェクトの再作成
    }
}
スポンサーリンク

関連記事:

「DataStore」はAndroid Jetpackで提供されているAPIです。 永続的なデータを保存するための仕組みです。 「SharedPreferences」の代替ツールとして登場し、「SharedPreferences」に代わって使用が推奨されています。 ここでは「DataStoreの概要」について、まとめます。 ※環境:Android Studio Hedgehog | 2023.1.1 Patch 2 ...
Preferences DataStoreの使い方を、まとめます。 DataStoreはPreferencesとProto DataStoreの2つがあります。 Preferences DataStoreは、データの識別子にキー(文字列)を使うタイプです。 手軽・簡素で、Proto DataStoreに比べて、使い勝手がよいです。 あえて、Proto DataStoreを使う理由が無いのであれば、Preferences DataStoreで十分です。 ※環境:Android Studio Hedgehog | 2023.1.1 Patch 2     androidx.datastore:datastore-preferences:1.1.7 ...
スポンサーリンク