本文译自「Implementing DataStore in Kotlin Multiplatform Projects」,原文链接carrion.dev/en/posts/da…,由 Ignacio Carrión发布于2025年5月9日。
DataStore 是 Google 开发的一种现代数据存储解决方案,用于替代 SharedPreferences。它提供了一个一致、类型安全的 API,用于存储键值对和类型化对象,并支持 Kotlin 协程和 Flow。随着 Kotlin Multiplatform (KMP) 的最新进展,我们现在可以将 DataStore 集成到 KMP 项目中,从而实现跨平台共享偏好设置和数据存储代码。这篇博文探讨了如何在 KMP 环境中配置、实现和优化 DataStore。
理解 Kotlin 多平台环境中的 DataStore
KMP 中的 DataStore 旨在提供跨平台一致的 API,同时利用平台特定的存储机制。DataStore 有两种类型:
- Preferences DataStore:用于存储键值对
- Proto DataStore:用于使用协议缓冲区存储类型化对象
在 KMP 上下文中,DataStore:
- 平台特定的实现提供实际的存储机制
- API 使用协程和 Flow,跨平台保持一致
这种方法使我们能够用通用代码定义数据访问模式,而底层存储操作则由平台特定的实现处理。
// In commonMain - DataStore interface
interface UserPreferences {
val userData: Flow<UserData>
suspend fun updateUsername(name: String)
suspend fun updateEmail(email: String)
suspend fun clearData()
}
// In commonMain - Data model
data class UserData(
val username: String = "",
val email: String = "",
val isLoggedIn: Boolean = false
)
在 KMP 项目中设置数据存储
要将 DataStore 集成到你的 KMP 项目中,你需要正确配置构建文件。以下是分步指南:
1. 在共享模块中配置 build.gradle.kts 文件
plugins {
kotlin("multiplatform")
id("com.android.library")
id("com.google.devtools.ksp") version "2.1.20-2.0.1" // For Proto DataStore
}
kotlin {
androidTarget()
iosX64()
iosArm64()
iosSimulatorArm64()
sourceSets {
val commonMain by getting {
dependencies {
// For Preferences DataStore
implementation("androidx.datastore:datastore-preferences-core:1.1.0")
// For coroutines
implementation("org.jetbrains.kotlinx:kotlinx-coroutines-core:1.7.3")
}
}
}
}
2. 从通用代码创建 DataStore 实例
/**
* 获取单例 DataStore 实例,如有必要则创建它。
*/
fun createDataStore(producePath: () -> String): DataStore<Preferences> =
PreferenceDataStoreFactory.createWithPath(
produceFile = { producePath().toPath() }
)
internal const val dataStoreFileName = "dice.preferences_pb"
特定平台的考虑因素
Android 实现
// shared/src/androidMain/kotlin/DataStore.kt
fun createDataStoreAndroid(context: Context): DataStore<Preferences> = createDataStore(
producePath = { context.filesDir.resolve(dataStoreFileName).absolutePath }
)
iOS 实现
// shared/src/iosMain/kotlin/DataStore.kt
fun createDataStoreIOS(): DataStore<Preferences> = createDataStore(
producePath = {
val documentDirectory: NSURL? = NSFileManager.defaultManager.URLForDirectory(
directory = NSDocumentDirectory,
inDomain = NSUserDomainMask,
appropriateForURL = null,
create = false,
error = null,
)
requireNotNull(documentDirectory).path + "/$dataStoreFileName"
}
)
实际示例:实现用户偏好存储库
为了演示完整的实现,让我们创建一个使用 DataStore 的存储库:
// In commonMain
class UserPreferencesRepository(private val dataStore: PreferencesDataStore) {
//定义preferences的键
private object PreferenceKeys {
val USERNAME = stringPreferencesKey("username")
val EMAIL = stringPreferencesKey("email")
val IS_LOGGED_IN = booleanPreferencesKey("is_logged_in")
}
// Get user data as a Flow
val userData: Flow<UserData> = dataStore.data.map { preferences ->
UserData(
username = preferences[PreferenceKeys.USERNAME] ?: "",
email = preferences[PreferenceKeys.EMAIL] ?: "",
isLoggedIn = preferences[PreferenceKeys.IS_LOGGED_IN] ?: false
)
}
// Update username
suspend fun updateUsername(name: String) {
dataStore.updateData { preferences ->
preferences.toMutablePreferences().apply {
this[PreferenceKeys.USERNAME] = name
}
}
}
// Update email
suspend fun updateEmail(email: String) {
dataStore.updateData { preferences ->
preferences.toMutablePreferences().apply {
this[PreferenceKeys.EMAIL] = email
}
}
}
// Set login status
suspend fun setLoggedIn(isLoggedIn: Boolean) {
dataStore.updateData { preferences ->
preferences.toMutablePreferences().apply {
this[PreferenceKeys.IS_LOGGED_IN] = isLoggedIn
}
}
}
// Clear all data
suspend fun clearData() {
dataStore.updateData { preferences ->
preferences.toMutablePreferences().apply {
remove(PreferenceKeys.USERNAME)
remove(PreferenceKeys.EMAIL)
remove(PreferenceKeys.IS_LOGGED_IN)
}
}
}
}
// In commonMain - ViewModel or Presenter
class UserViewModel(private val userPreferencesRepository: UserPreferencesRepository) {
val userData: Flow<UserData> = userPreferencesRepository.userData
suspend fun updateUserProfile(username: String, email: String) {
if (username.isNotBlank()) {
userPreferencesRepository.updateUsername(username)
}
if (email.isNotBlank()) {
userPreferencesRepository.updateEmail(email)
}
}
suspend fun login() {
userPreferencesRepository.setLoggedIn(true)
}
suspend fun logout() {
userPreferencesRepository.setLoggedIn(false)
}
suspend fun clearUserData() {
userPreferencesRepository.clearData()
}
}
KMP 中的高级数据存储功能
DataStore 提供了几种可在 KMP 环境中利用的高级功能:
1. 用于类型化对象的 Proto DataStore
如果你需要存储复杂对象,Proto DataStore 提供了一个类型安全的解决方案:
// 在 .proto 文件中定义数据结构
syntax = "proto3";
option java_package = "com.example.app";
option java_multiple_files = true;
message UserPreferences {
string username = 1;
string email = 2;
bool is_logged_in = 3;
}
// In commonMain - 创建序列化器
class UserPreferencesSerializer : Serializer<UserPreferences> {
override val defaultValue: UserPreferences = UserPreferences.getDefaultInstance()
override suspend fun readFrom(input: InputStream): UserPreferences {
return UserPreferences.parseFrom(input)
}
override suspend fun writeTo(t: UserPreferences, output: OutputStream) {
t.writeTo(output)
}
}
// Proto DataStore 的平台特定实现
2.数据迁移
// In androidMain - 从 SharedPreferences 迁移到 DataStore
val dataStore = context.createDataStore(
name = "user_preferences",
produceMigrations = { context ->
listOf(
SharedPreferencesMigration(
context = context,
sharedPreferencesName = "legacy_preferences"
)
)
}
)
3.处理异常
// In commonMain - 数据操作过程中的异常处理
val userData = dataStore.data
.catch { exception ->
// 处理异常(例如数据损坏)
if (exception is IOException) {
emit(emptyPreferences())
} else {
throw exception
}
}
.map { preferences ->
// Map preferences to your data model
UserData(
username = preferences[USERNAME] ?: "",
email = preferences[EMAIL] ?: ""
)
}
KMP 中数据存储的最佳实践
- 利用协程和 Flow 进行异步操作
- DataStore 操作本质上是异步的
- 使用 Flow 观察存储数据的变化
- 应用 Map、Filter 和 Combine 等 Flow 操作符进行数据转换
- 创建存储库层
- 将 DataStore 操作抽象到存储库后面
- 这样可以更轻松地根据需要切换实现
- 为你的业务逻辑提供简洁的 API
- 优雅地处理错误
- 使用 catch 操作符处理 Flow 中的异常
- 在无法读取数据时提供回退值
- 考虑为关键操作实现重试机制
- 优化性能
- 最大限度地减少 DataStore 更新次数
- 将相关的更改集中处理
- 使用 distinctUntilChanged() 避免不必要的排放
- 彻底测试你的 DataStore 代码
- 在 commonTest 中为你的存储库编写测试
- 使用测试替身模拟不同的场景
结论
将 DataStore 集成到 Kotlin Multiplatform 项目中,提供了一种现代化、类型安全的跨平台数据存储和访问方法。
本文概述的方法提供了一种实用的方法,可以跨平台共享首选项和数据存储逻辑,并且只需极少的平台特定代码。DataStore 对协程和 Flow 的支持使其与 KMP 项目完美契合,能够通过一致的 API 实现响应式和异步数据操作。
通过遵循本文概述的配置步骤、平台特定注意事项和最佳实践,你可以在 KMP 项目中成功实现 DataStore,并创建稳定、高效的跨平台数据存储解决方案,并且只需极少的平台特定代码。
欢迎搜索并关注 公众号「稀有猿诉」 获取更多的优质文章!
保护原创,请勿转载!