DataStore   Android Jetpack の一部。

Kotlin Multiplatform で試す
Kotlin マルチプラットフォームを使用すると、データレイヤを他のプラットフォームと共有できます。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.2.1"

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

Kotlin

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

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

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

Groovy

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

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

Kotlin

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

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

DataStore

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

Groovy

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

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

Kotlin

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

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

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

Groovy

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

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

Kotlin

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

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

コンテンツをシリアル化するには、プロトコル バッファまたは 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. 同じファイルに対し SingleProcessDataStore と MultiProcessDataStore を混在させない。複数のプロセスから 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.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())
    }
}

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.datastoresampleapp";
option java_multiple_files = true;

message Settings {
  int32 counter = 1;
  bool foo = 2;
}

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 をシングルトンとして簡単に保持できます。必須の 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 パラメータは、以前に定義されたシリアライザー クラスの名前を DataStore に指示します。

val Context.dataStore: DataStore<Settings> by
    dataStore(
        fileName = "settings.json",
        serializer = SettingsSerializer,
        scope = CoroutineScope(Dispatchers.IO + SupervisorJob()),
    )

Proto DataStore

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

collectAsStateWithLifecycle を使用して、コンポーザブル内の ViewModel によって生成された Flow を使用します。これにより、DataStore Flow が再コンポーズをトリガーする Compose State に安全に変換されます。

@Composable
fun SomeScreen(counterFlow: Flow<Int>) {
  val counter by counterFlow.collectAsStateWithLifecycle(initialValue = 0)
  Text(text = "Example counter: ${counter}")
}

collectAsStateWithLifecycle の詳細については、状態と Jetpack Compose をご覧ください。

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.toBuilder().setCounter(settings.counter + 1).build()
    }
}

Compose アプリで DataStore を使用する

Compose アプリで DataStore を使用するには、DataStore オペレーションをデータレイヤ(リポジトリなど)に保持し、ViewModel を介して UI にデータを公開することで、Android アプリ アーキテクチャのガイドラインに沿って実装します。

コンポーザブル関数内で DataStore からの読み取りや DataStore への書き込みを直接行わないでください。

  1. ViewModel を介して DataStore を公開します。次のスニペットに示すように、リポジトリ(DataStore をラップ)を ViewModel に渡し、Flow を StateFlow に変換して、UI がそれを監視できるようにします。

    class SettingsViewModel(
        private val userPreferencesRepository: UserPreferencesRepository
    ) : ViewModel() {
    
        // Expose the DataStore flow as a StateFlow for Compose
        val userSettings: StateFlow<UserSettings> = userPreferencesRepository.userSettingsFlow
            .stateIn(
                scope = viewModelScope,
                started = SharingStarted.WhileSubscribed(5000),
                initialValue = UserSettings.getDefaultInstance()
            )
    
        fun updateCounter(newValue: Int) {
            viewModelScope.launch {
                userPreferencesRepository.updateCounter(newValue)
            }
        }
    }
    
  2. コンポーザブルからオブザーブして書き込みます。次のスニペットに示すように、collectAsStateWithLifecycle を使用して UI で StateFlow を安全に監視し、ViewModel 関数を呼び出して書き込みを処理します。

    @Composable
    fun SettingsScreen(
        viewModel: SettingsViewModel = viewModel()
    ) {
        // Safely collect the state
        val settings by viewModel.userSettings.collectAsStateWithLifecycle()
    
        Column(modifier = Modifier.padding(16.dp)) {
            Text(text = "Current counter: ${settings.counter}")
    
            Spacer(modifier = Modifier.height(8.dp))
    
            Button(onClick = { viewModel.updateCounter(settings.counter + 1) }) {
                Text("Increment Counter")
            }
        }
    }
    

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

1 つのプロセス内の場合と同じデータの整合性プロパティで複数のプロセスから同じデータにアクセスできるよう DataStore を構成することができます。特に、DataStore は次のプロパティを提供します。

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

サービスとアクティビティを含むサンプルアプリがあるとします。サービスは別のプロセスで実行され、DataStore を定期的に更新します。

この例では JSON データストアを使用していますが、Preferences DataStore または Proto DataStore を使用することもできます。

@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.serializer(), 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(Time.serializer(), t).encodeToByteArray())
    }
}

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

val dataStore =
    MultiProcessDataStoreFactory.create(
        serializer = TimeSerializer,
        produceFile = { context.dataStoreFile("time.pb") },
        corruptionHandler = null,
    )

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

<service
    android:name="com.example.datastore.snippets.TimestampUpdateService"
    android:exported="false"
    android:process=":service" />

サービスは定期的に 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 は破損から自動的に復元されず、読み取りを試みるとシステムが CorruptionException をスローします。

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

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

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

デバイスのバックアップと復元

Preferences DataStore ファイル(*.preferences_pb)と Proto DataStore ファイルは、アプリの files/datastore/ ディレクトリに保存されます。デフォルトでは、これらのファイルは Android の自動バックアップとデバイス間(D2D)転送に含まれます。

DataStore のバックアップ ルールを構成する

DataStore に機密データと機密情報以外の設定(ユーザーのテーマやフィーチャー トグルなど)が含まれている場合は、それらを別々の DataStore ファイルに分離して res/xml/data_extraction_rules.xml を構成します。

<data-extraction-rules>
    <cloud-backup>
        <!-- Include general settings -->
        <include domain="file" path="datastore/user_settings.preferences_pb"/>
        <!-- Exclude sensitive local state -->
        <exclude domain="file" path="datastore/secure_state.preferences_pb"/>
    </cloud-backup>
    <device-to-device>
        <!-- Transfer settings during device-to-device transfer -->
        <include domain="file" path="datastore/"/>
    </device-to-device>
</data-extraction-rules>

フィードバックを送信

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

公開バグトラッカー:
Google がバグを修正できるよう問題を報告します。

参考情報

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

サンプル

ブログ

Codelab