Project: /architecture/_project.yaml Book: /architecture/_book.yaml keywords: datastore, architecture, api:JetpackDataStore description: このアプリ アーキテクチャ ガイドはデータレイヤ ライブラリに関する内容となっており、Preferences DataStore、Proto DataStore、セットアップなどの詳細が記載されています。 hide_page_heading: true
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 を正しく使用するには、常に次のルールに準拠してください。
同じプロセス内の所定ファイルに対し
DataStore
の複数のインスタンスを作成しないこと。複数作成すると、DataStore のすべての機能を使用できなくなる可能性があります。同じプロセス内の所定ファイルに対しアクティブな DataStore が複数あると、DataStore はデータのレンダリングまたは更新の際にIllegalStateException
をスローします。DataStore<T>
の汎用型は不変でなければならない。DataStore で使用される型を変えると、DataStore による整合性が無効となり、重大な検出しにくいバグが発生する可能性があります。不変性、明確な API、効率的なシリアル化を保証するプロトコル バッファを使用することをおすすめします。同じファイルに対し
SingleProcessDataStore
とMultiProcessDataStore
を混在させない。複数のプロセスからDataStore
にアクセスする場合は、MultiProcessDataStore
を使用する必要があります。
設定
アプリで Jetpack DataStore を使用するには、使用する実装に応じて Gradle ファイルに以下を追加します。
Preferences DataStore
Groovy
// Preferences DataStore (SharedPreferences like APIs) dependencies { implementation "androidx.datastore:datastore-preferences:1.1.7" // optional - RxJava2 support implementation "androidx.datastore:datastore-preferences-rxjava2:1.1.7" // optional - RxJava3 support implementation "androidx.datastore:datastore-preferences-rxjava3:1.1.7" } // Alternatively - use the following artifact without an Android dependency. dependencies { implementation "androidx.datastore:datastore-preferences-core:1.1.7" }
Kotlin
// Preferences DataStore (SharedPreferences like APIs) dependencies { implementation("androidx.datastore:datastore-preferences:1.1.7") // optional - RxJava2 support implementation("androidx.datastore:datastore-preferences-rxjava2:1.1.7") // optional - RxJava3 support implementation("androidx.datastore:datastore-preferences-rxjava3:1.1.7") } // Alternatively - use the following artifact without an Android dependency. dependencies { implementation("androidx.datastore:datastore-preferences-core:1.1.7") }
Proto DataStore
Groovy
// Typed DataStore (Typed API surface, such as Proto) dependencies { implementation "androidx.datastore:datastore:1.1.7" // optional - RxJava2 support implementation "androidx.datastore:datastore-rxjava2:1.1.7" // optional - RxJava3 support implementation "androidx.datastore:datastore-rxjava3:1.1.7" } // Alternatively - use the following artifact without an Android dependency. dependencies { implementation "androidx.datastore:datastore-core:1.1.7" }
Kotlin
// Typed DataStore (Typed API surface, such as Proto) dependencies { implementation("androidx.datastore:datastore:1.1.7") // optional - RxJava2 support implementation("androidx.datastore:datastore-rxjava2:1.1.7") // optional - RxJava3 support implementation("androidx.datastore:datastore-rxjava3:1.1.7") } // Alternatively - use the following artifact without an Android dependency. dependencies { implementation("androidx.datastore:datastore-core:1.1.7") }
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, "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.proto";
option java_multiple_files = true;
message Settings {
int32 example_counter = 1;
}
Proto DataStore を作成する
型付きオブジェクトを格納する Proto DataStore を作成するには、次の 2 つの手順を行います。
Serializer<T>
を実装するクラスを定義します。ここで、T
は proto ファイルで定義される型です。このシリアライザー クラスは、データ型の読み取り / 書き込みの方法を DataStore に指示します。まだファイルが作成されていない場合は、使用するシリアライザーのデフォルト値を必ず指定してください。dataStore
によって作成されたプロパティ デリゲートを使用して、DataStore<T>
のインスタンスを作成します。ここで、T
は proto ファイルで定義される型です。kotlin ファイルの最上位でインスタンスを 1 回呼び出し、アプリの他の部分でこのプロパティ デリゲートを介してアクセスします。filename
パラメータは、データの保存に使用するファイルを DataStore に指示します。serializer
パラメータは、手順 1 で定義されたシリアライザー クラスの名前を指定します。
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() {
return Settings.getDefaultInstance();
}
@Override
public Settings readFrom(@NotNull InputStream input) {
try {
return Settings.parseFrom(input);
} catch (InvalidProtocolBufferException exception) {
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 は以下の機能を提供します。
- 読み取りでは、ディスクに永続化されたデータのみを返す。
- 書き込み後の読み取りの整合性。
- 書き込みがシリアル化される。
- 読み取りが書き込みでブロックされない。
サービスとアクティビティを含むサンプルアプリがあるとします。
サービスは別のプロセスで実行され、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) } } }
一方、アプリはその変更を収集して 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): Settings =
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 は破損から自動的に復元されません。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) }
)
フィードバックを送信
以下のリソースを通じてフィードバックやアイデアをお寄せください。
- 公開バグトラッカー:
- Google がバグを修正できるよう問題を報告します。
参考情報
Jetpack DataStore の詳細については、以下の参考情報をご覧ください。
サンプル
ブログ
Codelab
あなたへのおすすめ
- 注: JavaScript がオフになっている場合はリンクテキストが表示されます
- ページング データを読み込んで表示する
- LiveData の概要
- レイアウトとバインディング式