アプリ アーキテクチャ: データレイヤー - DataStore - デベロッパー向け Android

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 の一部。

Kotlin Multiplatform で試す
Kotlin Multiplatform を使用すると、データレイヤを他のプラットフォームと共有できます。KMP で DataStore を設定して使用する方法について説明します

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

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

DataStore API

DataStore インターフェースは次の API を提供します。

  1. DataStore からデータを読み取るために使用できるフロー

    val data: Flow<T>
    
  2. DataStore のデータを更新する関数

    suspend updateData(transform: suspend (t) -> T)
    

DataStore の構成

キーを使用してデータを保存してアクセスする場合は、定義済みのスキーマを必要とせず、型安全性を備えていない Preferences DataStore 実装を使用します。SharedPreferences のような API を備えていますが、共有設定に関連する欠点はありません。

DataStore を使用すると、カスタムクラスを永続化できます。これを行うには、データのスキーマを定義し、永続化可能な形式に変換する Serializer を指定する必要があります。プロトコル バッファ、JSON、その他のシリアル化戦略を使用できます。

設定

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

Preferences DataStore

gradle ファイルの dependencies 部分に次の行を追加します。

Groovy

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

        // Alternatively - without an Android dependency.
        implementation "androidx.datastore:datastore-preferences-core:1.1.7"
    }
    

Kotlin

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

        // Alternatively - without an Android dependency.
        implementation("androidx.datastore:datastore-preferences-core:1.1.7")
    }
    

オプションの RxJava サポートを追加するには、次の依存関係を追加します。

Groovy

    dependencies {
        // optional - RxJava2 support
        implementation "androidx.datastore:datastore-preferences-rxjava2:1.1.7"

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

Kotlin

    dependencies {
        // optional - RxJava2 support
        implementation("androidx.datastore:datastore-preferences-rxjava2:1.1.7")

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

DataStore

gradle ファイルの dependencies 部分に次の行を追加します。

Groovy

    dependencies {
        // Typed DataStore for custom data objects (for example, using Proto or JSON).
        implementation "androidx.datastore:datastore:1.1.7"

        // Alternatively - without an Android dependency.
        implementation "androidx.datastore:datastore-core:1.1.7"
    }
    

Kotlin

    dependencies {
        // Typed DataStore for custom data objects (for example, using Proto or JSON).
        implementation("androidx.datastore:datastore:1.1.7")

        // Alternatively - without an Android dependency.
        implementation("androidx.datastore:datastore-core:1.1.7")
    }
    

RxJava のサポート用に次のオプションの依存関係を追加します。

Groovy

    dependencies {
        // optional - RxJava2 support
        implementation "androidx.datastore:datastore-rxjava2:1.1.7"

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

Kotlin

    dependencies {
        // optional - RxJava2 support
        implementation("androidx.datastore:datastore-rxjava2:1.1.7")

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

コンテンツをシリアル化するには、プロトコル バッファまたは JSON シリアル化のいずれかの依存関係を追加します。

JSON シリアル化

JSON シリアル化を使用するには、Gradle ファイルに以下を追加します。

Groovy

    plugins {
        id("org.jetbrains.kotlin.plugin.serialization") version "2.2.20"
    }

    dependencies {
        implementation "org.jetbrains.kotlinx:kotlinx-serialization-json:1.9.0"
    }
    

Kotlin

    plugins {
        id("org.jetbrains.kotlin.plugin.serialization") version "2.2.20"
    }

    dependencies {
        implementation("org.jetbrains.kotlinx:kotlinx-serialization-json:1.9.0")
    }
    

Protobuf のシリアル化

Protobuf シリアル化を使用するには、Gradle ファイルに以下を追加します。

Groovy

    plugins {
        id("com.google.protobuf") version "0.9.5"
    }
    dependencies {
        implementation "com.google.protobuf:protobuf-kotlin-lite:4.32.1"

    }

    protobuf {
        protoc {
            artifact = "com.google.protobuf:protoc:4.32.1"
        }
        generateProtoTasks {
            all().forEach { task ->
                task.builtins {
                    create("java") {
                        option("lite")
                    }
                    create("kotlin")
                }
            }
        }
    }
    

Kotlin

    plugins {
        id("com.google.protobuf") version "0.9.5"
    }
    dependencies {
        implementation("com.google.protobuf:protobuf-kotlin-lite:4.32.1")
    }

    protobuf {
        protoc {
            artifact = "com.google.protobuf:protoc:4.32.1"
        }
        generateProtoTasks {
            all().forEach { task ->
                task.builtins {
                    create("java") {
                        option("lite")
                    }
                    create("kotlin")
                }
            }
        }
    }
    

DataStore を正しく使用する

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

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

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

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

データ定義

Preferences DataStore

データをディスクに永続化するために使用されるキーを定義します。

val EXAMPLE_COUNTER = intPreferencesKey("example_counter")

JSON DataStore

JSON データストアの場合は、永続化するデータに @Serialization アノテーションを追加します。

@Serializable
data class Settings(
    val exampleCounter: Int
)

Serializer<T> を実装するクラスを定義します。ここで、T は、先ほどアノテーションを追加したクラスの型です。まだファイルが作成されていない場合は、使用するシリアライザーのデフォルト値を必ず指定してください。

object SettingsSerializer : Serializer<Settings> {

    override val defaultValue: Settings = Settings(exampleCounter = 0)

    override suspend fun readFrom(input: InputStream): Settings =
        try {
            Json.decodeFromString<Settings>(
                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(t)
                .encodeToByteArray()
        )
    }
}

Proto DataStore

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

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

src/main/proto フォルダ内に settings.proto というファイルを追加します。

syntax = "proto3";

option java_package = "com.example.datastore.snippets.proto";
option java_multiple_files = true;

message Settings {
  int32 example_counter = 1;
}

Serializer<T> を実装するクラスを定義します。ここで、T は proto ファイルで定義される型です。このシリアライザー クラスは、データ型の読み取り / 書き込みの方法を DataStore に指示します。まだファイルが作成されていない場合は、使用するシリアライザーのデフォルト値を必ず指定してください。

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) {
        return t.writeTo(output)
    }
}

DataStore を作成する

データを永続化するために使用するファイルの名前を指定する必要があります。

Preferences DataStore

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

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

JSON DataStore

dataStore によって作成されたプロパティ デリゲートを使用して、DataStore<T> のインスタンスを作成します。ここで、T はシリアル化可能なデータクラスです。kotlin ファイルの最上位でインスタンスを 1 回呼び出し、アプリの他の部分でこのプロパティ デリゲートを介してアクセスします。fileName パラメータは、データの保存に使用するファイルを DataStore に指示します。serializer パラメータは、手順 1 で定義されたシリアライザー クラスの名前を DataStore に指示します。

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

Proto DataStore

dataStore によって作成されたプロパティ デリゲートを使用して、DataStore<T> のインスタンスを作成します。ここで、T は proto ファイルで定義される型です。kotlin ファイルの最上位でインスタンスを 1 回呼び出し、アプリの他の部分でこのプロパティ デリゲートを介してアクセスします。fileName パラメータは、データの保存に使用するファイルを DataStore に指示します。serializer パラメータは、手順 1 で定義されたシリアライザー クラスの名前を DataStore に指示します。

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

DataStore から読み取る

データを永続化するために使用するファイルの名前を指定する必要があります。

Preferences DataStore

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

fun counterFlow(): Flow<Int> = context.dataStore.data.map { preferences ->
    preferences[EXAMPLE_COUNTER] ?: 0
}

JSON DataStore

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

fun counterFlow(): Flow<Int> = context.dataStore.data.map { settings ->
    settings.exampleCounter
}

Proto DataStore

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

fun counterFlow(): Flow<Int> = context.dataStore.data.map { settings ->
    settings.exampleCounter
}

DataStore に書き込む

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

Preferences DataStore

suspend fun incrementCounter() {
    context.dataStore.updateData {
        it.toMutablePreferences().also { preferences ->
            preferences[EXAMPLE_COUNTER] = (preferences[EXAMPLE_COUNTER] ?: 0) + 1
        }
    }
}

JSON DataStore

suspend fun incrementCounter() {
    context.dataStore.updateData { settings ->
        settings.copy(exampleCounter = settings.exampleCounter + 1)
    }
}

Proto DataStore

suspend fun incrementCounter() {
    context.dataStore.updateData { settings ->
        settings.copy { exampleCounter = exampleCounter + 1 }
    }
}

Compose サンプル

これらの関数をクラスにまとめて、Compose アプリで使用できます。

Preferences DataStore

これらの関数を PreferencesDataStore というクラスに配置し、Compose アプリで使用できるようになりました。

val context = LocalContext.current
val coroutineScope = rememberCoroutineScope()
val preferencesDataStore = remember(context) { PreferencesDataStore(context) }

// Display counter value.
val exampleCounter by preferencesDataStore.counterFlow()
    .collectAsState(initial = 0, coroutineScope.coroutineContext)
Text(
    text = "Counter $exampleCounter",
    fontSize = 25.sp
)

// Update the counter.
Button(
    onClick = {
        coroutineScope.launch { preferencesDataStore.incrementCounter() }
    }
) {
    Text("increment")
}

JSON DataStore

これらの関数を JSONDataStore というクラスに配置して、Compose アプリで使用できるようになりました。

val context = LocalContext.current
val coroutineScope = rememberCoroutineScope()
val jsonDataStore = remember(context) { JsonDataStore(context) }

// Display counter value.
val exampleCounter by jsonDataStore.counterFlow()
    .collectAsState(initial = 0, coroutineScope.coroutineContext)
Text(
    text = "Counter $exampleCounter",
    fontSize = 25.sp
)

// Update the counter.
Button(onClick = { coroutineScope.launch { jsonDataStore.incrementCounter() } }) {
    Text("increment")
}

Proto DataStore

これらの関数を ProtoDataStore というクラスに配置して、Compose アプリで使用できるようになりました。

val context = LocalContext.current
val coroutineScope = rememberCoroutineScope()
val protoDataStore = remember(context) { ProtoDataStore(context) }

// Display counter value.
val exampleCounter by protoDataStore.counterFlow()
    .collectAsState(initial = 0, coroutineScope.coroutineContext)
Text(
    text = "Counter $exampleCounter",
    fontSize = 25.sp
)

// Update the counter.
Button(onClick = { coroutineScope.launch { protoDataStore.incrementCounter() } }) {
    Text("increment")
}

同期コードで 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 を定期的に更新します。

この例では JSON データストアを使用していますが、設定データストアや proto データストアを使用することもできます。

@Serializable
data class Time(
    val lastUpdateMillis: Long
)

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

object TimeSerializer : Serializer<Time> {

    override val defaultValue: Time = Time(lastUpdateMillis = 0L)

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

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

複数のプロセスで DataStore を使用できるようにするには、アプリとサービスの両方のコードで MultiProcessDataStoreFactory を使用して DataStore オブジェクトを作成する必要があります。

val dataStore = MultiProcessDataStoreFactory.create(
    serializer = TimeSerializer,
    produceFile = {
        File("${context.cacheDir.path}/time.pb")
    },
    corruptionHandler = null
)

AndroidManifiest.xml に次の行を追加します。

<service
    android:name=".TimestampUpdateService"
    android:process=":my_process_id" />

サービスは定期的に updateLastUpdateTime() を呼び出し、updateData を使用してデータストアに書き込みます。

suspend fun updateLastUpdateTime() {
    dataStore.updateData { time ->
        time.copy(lastUpdateMillis = System.currentTimeMillis())
    }
}

アプリは、次のデータフローを使用して、サービスによって書き込まれた値を読み取ります。

fun timeFlow(): Flow<Long> = dataStore.data.map { time ->
    time.lastUpdateMillis
}

これで、これらの関数をすべて MultiProcessDataStore というクラスにまとめ、アプリで使用できるようになりました。

サービスコードは次のとおりです。

class TimestampUpdateService : Service() {
    val serviceScope = CoroutineScope(SupervisorJob() + Dispatchers.IO)
    val multiProcessDataStore by lazy { MultiProcessDataStore(applicationContext) }


    override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int {
        serviceScope.launch {
            while (true) {
                multiProcessDataStore.updateLastUpdateTime()
                delay(1000)
            }
        }
        return START_NOT_STICKY
    }

    override fun onDestroy() {
        super.onDestroy()
        serviceScope.cancel()
    }
}

アプリコードは次のとおりです。

val context = LocalContext.current
val coroutineScope = rememberCoroutineScope()
val multiProcessDataStore = remember(context) { MultiProcessDataStore(context) }

// Display time written by other process.
val lastUpdateTime by multiProcessDataStore.timeFlow()
    .collectAsState(initial = 0, coroutineScope.coroutineContext)
Text(
    text = "Last updated: $lastUpdateTime",
    fontSize = 25.sp
)

DisposableEffect(context) {
    val serviceIntent = Intent(context, TimestampUpdateService::class.java)
    context.startService(serviceIntent)
    onDispose {
        context.stopService(serviceIntent)
    }
}

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