DataStore Android Jetpack の一部。

Jetpack DataStore は、プロトコル バッファを使用して Key-Value ペアや型付きオブジェクトを格納できるデータ ストレージ ソリューションです。DataStore は、Kotlin コルーチンと Flow を使用して、データを非同期的に、一貫した形で、トランザクションとして保存します。

現在 SharedPreferences を使用してデータを保存している場合は、DataStore に移行することを検討してください。

Preferences DataStore と Proto DataStore

DataStore には、Preferences DataStore と Proto DataStore の 2 種類があります。

  • Preferences DataStore では、キーを使用してデータの保存およびアクセスを行います。この実装では、定義済みのスキーマは必要ありませんが、タイプセーフではありません。
  • Proto DataStore では、カスタムデータ型のインスタンスとしてデータを保存します。この実装では、プロトコル バッファを使用してスキーマを定義する必要がありますが、タイプセーフです。

DataStore を正しく使用する

DataStore を正しく使用するには、常に次のルールに準拠してください。

  1. 同じプロセス内の所定ファイルに対し DataStore の複数のインスタンスを作成しないこと。複数作成すると、DataStore のすべての機能を使用できなくなる可能性があります。同じプロセス内の所定ファイルに対しアクティブな DataStore が複数あると、DataStore はデータのレンダリングまたは更新の際に IllegalStateException をスローします。

  2. DataStore の汎用型は不変でなければならないDataStore で使用される型を変えると、DataStore による保証が無効となり、重大な検出しにくいバグが発生する可能性があります。不変性が保証され、シンプルな API と効率的なシリアル化を実現するプロトコル バッファを使用することを強くおすすめします。

  3. 同じファイルに対し SingleProcessDataStoreMultiProcessDataStore を混在させない。複数のプロセスから DataStore にアクセスする場合は、常に MultiProcessDataStore を使用します。

セットアップ

アプリで Jetpack DataStore を使用するには、使用する実装に応じて Gradle ファイルに以下を追加します。

Preferences DataStore

Groovy

    // Preferences DataStore (SharedPreferences like APIs)
    dependencies {
        implementation "androidx.datastore:datastore-preferences:1.1.1"

        // optional - RxJava2 support
        implementation "androidx.datastore:datastore-preferences-rxjava2:1.1.1"

        // optional - RxJava3 support
        implementation "androidx.datastore:datastore-preferences-rxjava3:1.1.1"
    }

    // Alternatively - use the following artifact without an Android dependency.
    dependencies {
        implementation "androidx.datastore:datastore-preferences-core:1.1.1"
    }
    

Kotlin

    // Preferences DataStore (SharedPreferences like APIs)
    dependencies {
        implementation("androidx.datastore:datastore-preferences:1.1.1")

        // optional - RxJava2 support
        implementation("androidx.datastore:datastore-preferences-rxjava2:1.1.1")

        // optional - RxJava3 support
        implementation("androidx.datastore:datastore-preferences-rxjava3:1.1.1")
    }

    // Alternatively - use the following artifact without an Android dependency.
    dependencies {
        implementation("androidx.datastore:datastore-preferences-core:1.1.1")
    }
    

Proto DataStore

Groovy

    // Typed DataStore (Typed API surface, such as Proto)
    dependencies {
        implementation "androidx.datastore:datastore:1.1.1"

        // optional - RxJava2 support
        implementation "androidx.datastore:datastore-rxjava2:1.1.1"

        // optional - RxJava3 support
        implementation "androidx.datastore:datastore-rxjava3:1.1.1"
    }

    // Alternatively - use the following artifact without an Android dependency.
    dependencies {
        implementation "androidx.datastore:datastore-core:1.1.1"
    }
    

Kotlin

    // Typed DataStore (Typed API surface, such as Proto)
    dependencies {
        implementation("androidx.datastore:datastore:1.1.1")

        // optional - RxJava2 support
        implementation("androidx.datastore:datastore-rxjava2:1.1.1")

        // optional - RxJava3 support
        implementation("androidx.datastore:datastore-rxjava3:1.1.1")
    }

    // Alternatively - use the following artifact without an Android dependency.
    dependencies {
        implementation("androidx.datastore:datastore-core:1.1.1")
    }
    

Preferences DataStore を使用して Key-Value ペアを保存する

Preferences DataStore の実装では、DataStore クラスと Preferences クラスを使用して、単純な Key-Value ペアをディスクに保持します。

Preferences DataStore を作成する

preferencesDataStore によって作成されたプロパティ デリゲートを使用して Datastore<Preferences> のインスタンスを作成します。kotlin ファイルの最上位でインスタンスを 1 回呼び出し、アプリケーションの他の部分でこのプロパティを介してアクセスします。これにより、DataStore をシングルトンとして簡単に保持できます。ただし RxJava を使用している場合は、RxPreferenceDataStoreBuilder を使用してください。必須の name パラメータは、Preferences DataStore の名前です。

Kotlin

// At the top level of your kotlin file:
val Context.dataStore: DataStore<Preferences> by preferencesDataStore(name = "settings")

Java

RxDataStore<Preferences> dataStore =
  new RxPreferenceDataStoreBuilder(context, /*name=*/ "settings").build();

Preferences DataStore から読み取る

Preference DataStore は定義済みのスキーマを使用しないため、対応するキー型の関数を使用して、DataStore<Preferences> インスタンスに保存する必要がある各値のキーを定義する必要があります。たとえば、int 値のキーを定義するには、intPreferencesKey() を使用します。次に、DataStore.data プロパティを使用し、Flow を使って適切な保存値を公開します。

Kotlin

val EXAMPLE_COUNTER = intPreferencesKey("example_counter")
val exampleCounterFlow: Flow<Int> = context.dataStore.data
  .map { preferences ->
    // No type safety.
    preferences[EXAMPLE_COUNTER] ?: 0
}

Java

Preferences.Key<Integer> EXAMPLE_COUNTER = PreferencesKeys.int("example_counter");

Flowable<Integer> exampleCounterFlow =
  dataStore.data().map(prefs -> prefs.get(EXAMPLE_COUNTER));

Preferences DataStore に書き込む

Preferences DataStore には、DataStore 内のデータをトランザクションとして更新する edit() 関数が用意されています。この関数の transform パラメータは、必要に応じて値を更新できるコードのブロックを受け入れます。変換ブロック内のすべてのコードは単一のトランザクションとして扱われます。

Kotlin

suspend fun incrementCounter() {
  context.dataStore.edit { settings ->
    val currentCounterValue = settings[EXAMPLE_COUNTER] ?: 0
    settings[EXAMPLE_COUNTER] = currentCounterValue + 1
  }
}

Java

Single<Preferences> updateResult =  dataStore.updateDataAsync(prefsIn -> {
  MutablePreferences mutablePreferences = prefsIn.toMutablePreferences();
  Integer currentInt = prefsIn.get(INTEGER_KEY);
  mutablePreferences.set(INTEGER_KEY, currentInt != null ? currentInt + 1 : 1);
  return Single.just(mutablePreferences);
});
// The update is completed once updateResult is completed.

Proto DataStore で型付きオブジェクトを保存する

Proto DataStore 実装では、DataStore とプロトコル バッファを使用して型付きオブジェクトをディスクに保存します。

スキーマを定義する

Proto DataStore では、app/src/main/proto/ ディレクトリの proto ファイル内に定義済みスキーマが必要です。このスキーマは、Proto DataStore で保持するオブジェクトの型を定義します。proto スキーマの定義の詳細については、protobuf 言語ガイドをご覧ください。

syntax = "proto3";

option java_package = "com.example.application";
option java_multiple_files = true;

message Settings {
  int32 example_counter = 1;
}

Proto DataStore を作成する

型付きオブジェクトを格納する Proto DataStore を作成するには、次の 2 つの手順を行います。

  1. Serializer<T> を実装するクラスを定義します。ここで、T は proto ファイルで定義される型です。このシリアライザー クラスは、データ型の読み取り / 書き込みの方法を DataStore に指示します。まだファイルが作成されていない場合は、使用するシリアライザーのデフォルト値を必ず指定してください。
  2. dataStore によって作成されたプロパティ デリゲートを使用して、DataStore<T> のインスタンスを作成します。ここで、T は proto ファイルで定義される型です。kotlin ファイルの最上位でインスタンスを 1 回呼び出し、アプリの他の部分でこのプロパティ デリゲートを介してアクセスします。filename パラメータは、データの保存に使用するファイルを DataStore に指示します。serializer パラメータは、手順 1 で定義されたシリアライザー クラスの名前を DataStore に指示します。

Kotlin

object SettingsSerializer : Serializer<Settings> {
  override val defaultValue: Settings = Settings.getDefaultInstance()

  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)
}

val Context.settingsDataStore: DataStore<Settings> by dataStore(
  fileName = "settings.pb",
  serializer = SettingsSerializer
)

Java

private static class SettingsSerializer implements Serializer<Settings> {
  @Override
  public Settings getDefaultValue() {
    Settings.getDefaultInstance();
  }

  @Override
  public Settings readFrom(@NotNull InputStream input) {
    try {
      return Settings.parseFrom(input);
    } catch (exception: InvalidProtocolBufferException) {
      throw CorruptionException(“Cannot read proto.”, exception);
    }
  }

  @Override
  public void writeTo(Settings t, @NotNull OutputStream output) {
    t.writeTo(output);
  }
}

RxDataStore<Byte> dataStore =
    new RxDataStoreBuilder<Byte>(context, /* fileName= */ "settings.pb", new SettingsSerializer()).build();

Proto DataStore から読み取る

DataStore.data を使用して、保存されたオブジェクトから適切なプロパティの Flow を公開します。

Kotlin

val exampleCounterFlow: Flow<Int> = context.settingsDataStore.data
  .map { settings ->
    // The exampleCounter property is generated from the proto schema.
    settings.exampleCounter
  }

Java

Flowable<Integer> exampleCounterFlow =
  dataStore.data().map(settings -> settings.getExampleCounter());

Proto DataStore に書き込む

Proto DataStore には、保存されたオブジェクトをトランザクションとして更新する updateData() 関数が用意されています。updateData() は、データ型のインスタンスとしてデータの現在の状態を提供し、アトミックな読み取り - 書き込み - 修正オペレーションでデータをトランザクションとして更新します。

Kotlin

suspend fun incrementCounter() {
  context.settingsDataStore.updateData { currentSettings ->
    currentSettings.toBuilder()
      .setExampleCounter(currentSettings.exampleCounter + 1)
      .build()
    }
}

Java

Single<Settings> updateResult =
  dataStore.updateDataAsync(currentSettings ->
    Single.just(
      currentSettings.toBuilder()
        .setExampleCounter(currentSettings.getExampleCounter() + 1)
        .build()));

同期コードで DataStore を使用する

DataStore の主なメリットの 1 つは非同期 API ですが、コード全体を常に非同期に変更できるわけではありません。これは、同期ディスク I/O を使用する既存のコードベースを利用している場合や、非同期 API を提供しない依存関係がある場合などに問題になります。

Kotlin のコルーチンには、同期コードと非同期コードの間のギャップを埋めるための runBlocking() コルーチン ビルダーが用意されています。runBlocking() を使用すると、DataStore からデータを同期的に読み取ることができます。 RxJava では、Flowable でブロック メソッドが用意されています。次のコードは、DataStore がデータを返すまで呼び出し元のスレッドをブロックします。

Kotlin

val exampleData = runBlocking { context.dataStore.data.first() }

Java

Settings settings = dataStore.data().blockingFirst();

UI スレッドで同期 I/O オペレーションを実行すると、ANR や UI ジャンクが発生することがあります。この問題は、DataStore から非同期でデータをプリロードすることで軽減できます。

Kotlin

override fun onCreate(savedInstanceState: Bundle?) {
    lifecycleScope.launch {
        context.dataStore.data.first()
        // You should also handle IOExceptions here.
    }
}

Java

dataStore.data().first().subscribe();

このようにして、DataStore はデータを非同期的に読み取り、メモリ内にキャッシュします。最初に行われる読み取りがすでに完了している場合は、後で行われる runBlocking() を使用した同期読み取りがより高速になるか、ディスク I/O オペレーションを完全に回避することがあります。

マルチプロセス コードで DataStore を使用する

1 つのプロセス内の場合と同じデータの整合性を保証することで複数のプロセスから同じデータにアクセスできるよう DataStore を構成することができます。特に、DataStore は以下の点を保証します。

  • 読み取りでは、ディスクに永続化されたデータのみを返す。
  • 書き込み後の読み取りの整合性。
  • 書き込みがシリアル化される。
  • 読み取りが書き込みでブロックされない。

サービスとアクティビティを含むサンプルアプリがあるとします。

  1. サービスは別のプロセスで実行され、DataStore を定期的に更新します。

    <service
      android:name=".MyService"
      android:process=":my_process_id" />
    
    override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int {
          scope.launch {
              while(isActive) {
                  dataStore.updateData {
                      Settings(lastUpdate = System.currentTimeMillis())
                  }
                  delay(1000)
              }
          }
    }
    
  2. 一方、アプリはその変更を収集して UI を更新します。

    val settings: Settings by dataStore.data.collectAsState()
    Text(
      text = "Last updated: $${settings.timestamp}",
    )
    

複数のプロセスで DataStore を使用できるようにするには、MultiProcessDataStoreFactory を使用して DataStore オブジェクトを作成する必要があります。

val dataStore: DataStore<Settings> = MultiProcessDataStoreFactory.create(
   serializer = SettingsSerializer(),
   produceFile = {
       File("${context.cacheDir.path}/myapp.preferences_pb")
   }
)

serializer はデータ型の読み取り / 書き込みの方法を DataStore に指示します。まだファイルが作成されていない場合は、使用するシリアライザーのデフォルト値を必ず指定してください。kotlinx.serialization を使用する実装例を以下に示します。

@Serializable
data class Settings(
   val lastUpdate: Long
)

@Singleton
class SettingsSerializer @Inject constructor() : Serializer<Settings> {

   override val defaultValue = Settings(lastUpdate = 0)

   override suspend fun readFrom(input: InputStream): Timer =
       try {
           Json.decodeFromString(
               Settings.serializer(), input.readBytes().decodeToString()
           )
       } catch (serialization: SerializationException) {
           throw CorruptionException("Unable to read Settings", serialization)
       }

   override suspend fun writeTo(t: Settings, output: OutputStream) {
       output.write(
           Json.encodeToString(Settings.serializer(), t)
               .encodeToByteArray()
       )
   }
}

Hilt 依存関係インジェクションを使用して、DataStore のインスタンスがプロセスごとに一意になるようにします。

@Provides
@Singleton
fun provideDataStore(@ApplicationContext context: Context): DataStore<Settings> =
   MultiProcessDataStoreFactory.create(...)

フィードバックを提供する

以下のリソースを通じてフィードバックやアイデアをお寄せください。

Issue Tracker
Google がバグを修正できるよう問題を報告します。

参考情報

Jetpack DataStore の詳細については、以下の参考情報をご覧ください。

サンプル

ブログ

Codelab