DataStore   Android Jetpack 的一部分。

试用 Kotlin Multiplatform
Kotlin Multiplatform 允许与其他平台共享数据层。了解如何在 KMP 中设置和使用 DataStore

Jetpack DataStore 是一种数据存储解决方案,让您可以使用 协议缓冲区存储键值对或类型化对象。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 文件的依赖项部分:

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 文件的依赖项部分:

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")
    }
    

如需序列化内容,请添加 Protocol Buffers 或 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 提供的一致性失效,并且可能会造成严重的、难以发现的 bug。我们建议您使用协议缓冲区,这有助于确保不可变性、清晰的 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 类将键值对保留在磁盘上。您可以使用 preferencesDataStore所创建的属性委托来创建 DataStore<Preferences>实例。在您的 Kotlin 文件顶层调用该实例一次。便可在应用的所有其余部分通过此属性访问 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 文件顶层调用该实例一次,便可在应用的所有其余部分通过此属性委托访问该实例。fileName 参数会告知 DataStore 使用哪个文件存储数据,而 serializer 参数会告知 DataStore 之前定义的序列化器类的名称。

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

Proto DataStore

您可以使用 dataStore 所创建的属性委托来创建 DataStore<T> 实例,其中 T 是 proto 文件中定义的类型。在您的 Kotlin 文件顶层调用该实例一次,便可在应用的所有其余部分通过此属性委托访问该实例。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 状态。

@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 将数据公开给界面。

避免在可组合函数中直接读取或写入 DataStore。

  1. 通过 ViewModel 公开 DataStore。将您的存储库(封装了 DataStore)传递到您的 ViewModel 中,并将 Flow 转换为 StateFlow,以便界面可以轻松观察它,如以下代码段所示:

    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 安全地观察界面中的 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

您可以对 DataStore 进行配置,使其在不同进程中访问相同数据时确保实现与在单个进程中访问数据时相同的数据一致性。具体而言,DataStore 提供以下属性:

  • 读取仅返回已持久存储到磁盘的数据。
  • 写后读一致性。
  • 写入会序列化。
  • 写入绝不会阻塞读取。

假设有一个包含一项服务和一个 activity 的示例应用,其中服务在单独的进程中运行,并会定期更新 DataStore。

此示例使用 JSON 数据存储区,但您也可以使用 Preferences 或 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) }
)

提供反馈

通过以下资源与我们分享您的反馈和想法:

问题跟踪器
报告问题,以便我们修复 bug。

其他资源

如需详细了解 Jetpack DataStore,请参阅下列其他资源:

示例

博客

Codelab