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(...)

ファイルの破損を処理する

DataStore の永続ディスク ファイルが破損することがあります。デフォルトでは、DataStore は破損から自動的に復元されず、読み取りを試みると、システムが CorruptionException をスローします。

DataStore には、このようなシナリオで正常に復元し、例外をスローしないようにする破損ハンドラ API が用意されています。構成すると、破損ハンドラは破損したファイルを、事前定義されたデフォルト値を含む新しいファイルに置き換えます。

このハンドラを設定するには、by dataStore() または DataStoreFactory ファクトリ メソッドで DataStore インスタンスを作成するときに corruptionHandler を指定します。

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

フィードバックを送信

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

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

参考情報

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

サンプル

ブログ

Codelab