应用架构:数据层 - DataStore - Android 开发者

Project: /architecture/_project.yaml Book: /architecture/_book.yaml keywords: datastore, architecture, api:JetpackDataStore description: 浏览这份有关数据层库的应用架构指南,了解 Preferences DataStore 和 Proto DataStore、Setup 等。 hide_page_heading: true

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 以将其转换为可持久保存的格式。您可以选择使用 Protocol Buffers、JSON 或任何其他序列化策略。

设置

如需在您的应用中使用 Jetpack DataStore,请根据您要使用的实现向 Gradle 文件添加以下内容:

Preferences DataStore

将以下代码行添加到 Gradle 文件的依赖项部分:

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

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

如需序列化内容,请添加 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 保留为单例。 如果您使用的是 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 文件顶层调用该实例一次,便可在应用的所有其余部分通过此属性委托访问该实例。fileName 参数会告知 DataStore 使用哪个文件存储数据,而 serializer 参数会告知 DataStore 在第 1 步中定义的序列化器类的名称。

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

Proto DataStore

使用 dataStore 所创建的属性委托来创建 DataStore<T> 实例,其中 T 是在 proto 文件中定义的类型。在您的 Kotlin 文件顶层调用该实例一次,便可在应用的所有其余部分通过此属性委托访问该实例。fileName 参数会告知 DataStore 使用哪个文件存储数据,而 serializer 参数会告知 DataStore 在第 1 步中定义的序列化器类的名称。

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 的主要优势之一是异步 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();

对界面线程执行同步 I/O 操作可能会导致 ANR 或界面无响应。您可以通过从 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

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

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

假设有一个包含一项服务和一个 activity 的示例应用,其中服务在单独的进程中运行,并会定期更新 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 不会自动从损坏中恢复,尝试从中读取数据会导致系统抛出 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) }
)

提供反馈

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

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

其他资源

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

示例

博客

Codelab