稀有猿诉

十年磨一剑,历炼出锋芒,说话千百句,不如码二行。

在Kotlin Multiplatform项目中使用DataStore

本文译自「Implementing DataStore in Kotlin Multiplatform Projects」,原文链接https://carrion.dev/en/posts/datastore-in-kmp/,由 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 有两种类型:

  1. Preferences DataStore:用于存储键值对
  2. Proto DataStore:用于使用协议缓冲区存储类型化对象

在 KMP 上下文中,DataStore:

  1. 平台特定的实现提供实际的存储机制
  2. API 使用协程和 Flow,跨平台保持一致

这种方法使我们能够用通用代码定义数据访问模式,而底层存储操作则由平台特定的实现处理。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
// 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 文件

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
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 实例

1
2
3
4
5
6
7
8
9
/**
 * 获取单例 DataStore 实例,如有必要则创建它。
 */
fun createDataStore(producePath: () -> String): DataStore<Preferences> =
   PreferenceDataStoreFactory.createWithPath(
      produceFile = { producePath().toPath() }
   )

internal const val dataStoreFileName = "dice.preferences_pb"

特定平台的考虑因素

Android 实现

1
2
3
4
5
// shared/src/androidMain/kotlin/DataStore.kt

fun createDataStoreAndroid(context: Context): DataStore<Preferences> = createDataStore(
   producePath = { context.filesDir.resolve(dataStoreFileName).absolutePath }
)

iOS 实现

1
2
3
4
5
6
7
8
9
10
11
12
13
14
// 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 的存储库:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
// 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 提供了一个类型安全的解决方案:

1
2
3
4
5
6
7
8
9
10
11
// 在 .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;
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
// 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.数据迁移

1
2
3
4
5
6
7
8
9
10
11
12
// In androidMain - 从 SharedPreferences 迁移到 DataStore
val dataStore = context.createDataStore(
    name = "user_preferences",
    produceMigrations = { context ->
        listOf(
            SharedPreferencesMigration(
                context = context,
                sharedPreferencesName = "legacy_preferences"
            )
        )
    }
)

3.处理异常

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
// 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 中数据存储的最佳实践

  1. 利用协程和 Flow 进行异步操作
    • DataStore 操作本质上是异步的
    • 使用 Flow 观察存储数据的变化
    • 应用 Map、Filter 和 Combine 等 Flow 操作符进行数据转换
  2. 创建存储库层
    • 将 DataStore 操作抽象到存储库后面
    • 这样可以更轻松地根据需要切换实现
    • 为你的业务逻辑提供简洁的 API
  3. 优雅地处理错误
    • 使用 catch 操作符处理 Flow 中的异常
    • 在无法读取数据时提供回退值
    • 考虑为关键操作实现重试机制
  4. 优化性能
    • 最大限度地减少 DataStore 更新次数
    • 将相关的更改集中处理
    • 使用 distinctUntilChanged() 避免不必要的排放
  5. 彻底测试你的 DataStore 代码
    • 在 commonTest 中为你的存储库编写测试
    • 使用测试替身模拟不同的场景

结论

将 DataStore 集成到 Kotlin Multiplatform 项目中,提供了一种现代化、类型安全的跨平台数据存储和访问方法。

本文概述的方法提供了一种实用的方法,可以跨平台共享首选项和数据存储逻辑,并且只需极少的平台特定代码。DataStore 对协程和 Flow 的支持使其与 KMP 项目完美契合,能够通过一致的 API 实现响应式和异步数据操作。

通过遵循本文概述的配置步骤、平台特定注意事项和最佳实践,你可以在 KMP 项目中成功实现 DataStore,并创建稳定、高效的跨平台数据存储解决方案,并且只需极少的平台特定代码。

Comments