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.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. 同じファイルに対して 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 の実装では、DataStorePreferences クラスを使用して、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,
)

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.exampleCounter
}

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.copy { exampleCounter = exampleCounter + 1 }
    }
}

Compose アプリで DataStore を使用する

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

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

  1. ViewModel を介して DataStore を公開します。リポジトリ(DataStore をラップする)を ViewModel に渡し、 FlowStateFlow に変換して、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>(
                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.filesDir.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 は破損から自動的に復旧しません。読み取りを試みると、システムは 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) }
)

フィードバックを送信

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

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

参考情報

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

サンプル

ブログ

Codelab