Skip to content

Navigation Menu

Sign in
Appearance settings

Search code, repositories, users, issues, pull requests...

Provide feedback

We read every piece of feedback, and take your input very seriously.

Saved searches

Use saved searches to filter your results more quickly

Sign up
Appearance settings

在 Compose Multiplatform ( Android / JVM Desktop) 中优雅完成数据持久化 | An elegant way to do data persistence in Compose Multiplatform ( Android / JVM Desktop )

License

Notifications You must be signed in to change notification settings

FunnySaltyFish/ComposeDataSaver

Repository files navigation

ComposeDataSaver

Maven Central License
Android JVM
iOS WASM

| English Version |

英文的 README 是由译站的长文翻译功能直接一键翻译自中文版本的。这是一个强大的翻译应用程序,利用大型语言模型的力量进行翻译,也由我开发。它还是一个开源的Compose跨平台应用,并使用这个库来保存数据。如果你在寻找一个完整的项目,可以去那看看

优雅地在 Compose Multiplatform ( Android / JVM Desktop / iOS / WASM ) 中完成数据持久化

// booleanExample 初始化值为 false
// 之后会自动读取本地数据
var booleanExample by rememberDataSaverState("KEY_BOOLEAN_EXAMPLE", false)
// 直接赋值即可完成持久化
booleanExample = true
// 或者在 ViewModel 中
class MyViewModel: ViewModel() {
 // 当存储一个 int 值且默认值为 0 时,我们建议您显式地标记变量的类型为 `Int` 以避免被默认视为 `Long`
 var intExample by mutableDataSaverStateOf<Int>(
 dataSaverInterface = AppConfig.dataSaver,
 key = "KEY_INT_EXAMPLE",
 initialValue = 0
 )
 fun increment() {
 intExample++ // 当值变化时,数据会自动保存
 }
}
  • 🎉 简洁:近似原生 Compose 函数的写法
  • 🎉 低耦合:抽象接口,不限制底层保存算法实现
  • 🎉 强大:支持基本的数据类型和自定义类型

注:此库是对Compose中使用其他框架(比如 Preference、MMKV、DataStore 等)的封装,不是一个单独的数据保存框架。您可以参考此链接以了解它的设计思想。

Example

您可以点击 这里下载demo体验(Debug 包,相较于 release 包较卡顿)


引入

settings.gradle引入仓库位置

dependencyResolutionManagement {
 repositories {
 mavenCentral()
 }
}

在项目build.gradle引入

dependencies {
 implementation "io.github.funnysaltyfish:data-saver-core:1.2.3"
}

注意:自 v1.2.0 起,仓库转为 Compose Multiplatform,发布至 Maven Central,Group Id 也有改变。从 v1.2.0 之前升级版本时请注意更改

示例代码

以下介绍的示例代码均可在 这里 查看具体实现

配置

项目使用 DataSaverInterface 的实现类来保存数据,因此您需要先提供一个此类对象。

Android

Perference

项目默认包含了使用 Preference 保存数据的实现类 DataSaverPreferences,可如下初始化:

// init preferences
val dataSaverPreferences = DataSaverPreferences(applicationContext)
CompositionLocalProvider(LocalDataSaver provides dataSaverPreferences){
	ExampleComposable()
}

除此之外, 我们也提供了基于 MMKV 或者 DataStorePreference 的简单实现

MMKV

  1. 在上述依赖基础上,额外添加
// if you want to use mmkv
implementation "io.github.funnysaltyfish:data-saver-mmkv:1.2.3"
implementation 'com.tencent:mmkv:1.2.14'
  1. 如下初始化
// 全局初始化 MMKV,比如在 Application 的 onCreate 中
MMKV.initialize(applicationContext)
...
val dataSaverMMKV = DefaultDataSaverMMKV
// DefaultDataSaverMMKV 是我们提供的默认实现,您可以在任何地方使用它,就像一个 MMKVUtils 那样
// 如果有定制 MMKV 的需要,可以选择 DataSaverMMKV(MMKV.defaultMMKV())
CompositionLocalProvider(LocalDataSaver provides dataSaverMMKV){
 // ...
}

DataStorePreference

  1. 在上述依赖基础上,额外添加
// if you want to use DataStore
implementation "io.github.funnysaltyfish:data-saver-data-store-preferences:1.2.3"
def data_store_version = "1.0.0"
implementation "androidx.datastore:datastore:$data_store_version"
implementation "androidx.datastore:datastore-preferences:$data_store_version"
  1. 如下初始化
val Context.dataStore : DataStore<Preferences> by preferencesDataStore("dataStore")
val dataSaverDataStorePreferences = DataSaverDataStorePreferences(applicationContext.dataStore)
CompositionLocalProvider(LocalDataSaver provides dataSaverDataStorePreferences){
 // ...
}

JVM Desktop

默认包含了基于 java.util.Properties 的实现类 DataSaverProperties,您可以如下初始化:

// init properties
val dataSaver = DataSaverProperties("$userHome/$projectName/config.properties")
CompositionLocalProvider(LocalDataSaver provides dataSaver){
 ExampleComposable()
}

如果您需要加密存储,可以使用 DataSaverEncryptedProperties 的实现。它基于 AES 算法加密每一项值,您需要提供一个密钥。

val dataSaver = DataSaverEncryptedProperties("$userHome/$projectName/data_saver_encrypted.properties", "FunnySaltyFish")
CompositionLocalProvider(LocalDataSaver provides dataSaver){
 ExampleComposable()
}

iOS

iOS 平台默认包含了基于 NSUserDefaults 的实现类 DataSaverNSUserDefaults,它提供了增强的数据类型支持和外部变更监听能力。您可以如下初始化:

// 使用默认的 NSUserDefaults 实例
val dataSaver = DefaultDataSaverNSUserDefaults
CompositionLocalProvider(LocalDataSaver provides dataSaver){
 ExampleComposable()
}
// 或者自定义配置
val customDataSaver = DataSaverNSUserDefaults(
 userDefaults = NSUserDefaults.standardUserDefaults,
 senseExternalDataChange = true // 启用外部变更监听
)

iOS 实现支持以下额外特性:

  • 丰富的数据类型: 除基本类型外,还支持 ByteArrayList(Int/String/Boolean)Map(String, Int/String/Boolean)NSDate、URL 等
  • KVO 变更监听: 可监听外部对 NSUserDefaults 的修改
  • 内存管理优化: 使用 autoreleasepool 进行内存管理

WASM

WASM 平台基于浏览器的 localStorage 实现数据持久化,适用于 Web 应用。您可以如下初始化:

// 使用默认配置
val dataSaver = DefaultDataSaverLocalStorage
CompositionLocalProvider(LocalDataSaver provides dataSaver){
 ExampleComposable()
}
// 或者自定义键前缀
val customDataSaver = DataSaverLocalStorage(
 keyPrefix = "MyApp_", // 自定义键前缀,避免冲突
 senseExternalDataChange = false // WASM 暂不支持外部变更监听
)

WASM 实现特点:

  • 轻量级: 基于浏览器原生 localStorage API
  • 键前缀: 支持自定义键前缀,避免与其他应用数据冲突
  • 基本类型: 支持 StringIntLongBooleanFloatDouble 等基本数据类型

几者默认支持的类型如下所示

类型 DataSaverPreference DataSaverMMKV DataSaverDataStorePreferences DataSaverProperties/DataSaverEncryptedProperties DataSaverNSUserDefaults DataSaverLocalStorage
Int Y Y Y Y Y Y
Boolean Y Y Y Y Y Y
String Y Y Y Y Y Y
Long Y Y Y Y Y Y
Float Y Y Y Y Y Y
Double Y Y Y Y Y
Parceable Y
ByteArray Y Y
List Y
Map Y
NSDate Y
URL Y

保存数据

完成了 CompositionLocalProvider 的赋值后,在其子微件内部可使用 getLocalDataSaverInterface() 获取当前 DataSaverInterface 实例

对于基本数据类型(如String/Int/Boolean)等:

// booleanExample 初始化值为 false
// 之后会自动读取本地数据
var booleanExample by rememberDataSaverState("KEY_BOOLEAN_EXAMPLE", false)
// 直接赋值即可完成持久化
booleanExample = true

通过赋值,数据即可自动转换、存于本地。就这么简单!

而对于其他数据类型,您需要自己注册类型转换器,告诉框架如何将您的数据转换为字符串,以及如何从字符串还原:

@Serializable
data class ExampleBean(var id: Int, val label: String)
// ------------ //
// 在初始化时调用registerTypeConverters方法注册对应转换方法
// 该方法接收两个参数:分别用于 转成可序列化类型以保存 和 反序列化为您的Bean
// 此处使用 Json.encodeToString 和 Json.decodeFromString, 您也可以用 Gson、Fastjson 等
registerTypeConverters<ExampleBean>(
	save = { bean -> Json.encodeToString(bean) },
	restore = { str -> Json.decodeFromString(str) }
)
// 或者,如果你只需要对某个 state 编写转换器,可以直接传入 `typeConverter` 参数
// 此参数如有,则其优先级高于 `registerTypeConverters` 方法注册的全局转换器
var array by rememberDataSaverState(
 "custom_type_converter_example",
 intArrayOf(1, 2, 3, 4, 5),
 // 参数类型为 ITypeConverter,这里的 ClassTypeConverter 是基于 type 类型 accept 的子类
 typeConverter = object : ClassTypeConverter(type = typeOf<IntArray>()) {
 override fun save(data: Any?): String {
 return (data as IntArray).joinToString(",")
 }
 override fun restore(str: String): Any {
 return str.split(",").map { it.toInt() }.toIntArray()
 }
 }
)

如果您需要存储可空变量,请使用 registerTypeConverters<ExampleBean?>

请注意,出于代码的实现上的考虑,对于可空类型,设置 state.value = nulldataSaverInterface.saveData(key, null) 实际将调用对应 remove 方法直接移除对应值。这意味着,框架的默认实现没有办法正确的保存 "null" 值。当 state.value = null 设置完且下次重新打开应用后,框架会认为此 key 对应的本地值不存在,会将 value 设为 initialValue
如果您需要真的存储 "null" 且 initialValue != null,请手动处理这部分逻辑。比如,设置一个特殊的值来代表 "null" ,比如 ExampleBean(-1, "null");如果您有更好的方案,欢迎 PR!

自 v1.2.1 起,您除了使用类型信息来注册转换器,也可以自己写上其他判定条件:

inline fun <reified T> registerTypeConverters(
 noinline save: (T) -> String,
 noinline restore: (String) -> T,
 noinline acceptCondition: (T) -> Boolean
)

acceptConditiontrue 时,框架会调用对应 saverestore 方法转换对应数据。

注意:

  1. registerTypeConverters 请在初始化时调用,确保早于使用 rememberDataSaverState("key", ExampleBean()) 之前
  2. 多个类型转换器会按照注册顺序反向依次尝试,直到找到合适的转换器。因此,如果您注册了多个相同类型的转换器,框架会使用最后一个符合条件的转换器。
  3. 您可以通过 DataSaverConverters.typeConverters 获取到注册的全部转换器列表,初始会有默认的一些,如对 String 的支持

在 Composable 函数外使用

有些情况下,您可能需要将 DataSaverState 置于 @Composable 函数外面,比如放在 ViewModel 中。v1.1.0 提供了 mutableDataSavarStateOf 函数用于此用途,该函数将会自动读取并转换已保存的值,并返回 State。

object AppConfig {
 val dataSaver = DataSaverMMKV(...)
}
class MyViewModel: ViewModel() {
 var username: String by mutableDataSavarStateOf(AppConfig.dataSaver, "username", "")
}

使用其他存储框架

如果默认提供的几种实现无法满足您的需求,您也可以自行继承 DataSaverInterface,并重写 saveDatareadData 方法分别用于保存数据和读取数据。对于一些支持协程的框架(如DataStore),您也可以重写 saveDataAsync 以实现异步的保存

abstract class DataSaverInterface(val senseExternalDataChange: Boolean = false) {
 abstract fun <T> saveData(key: String, data: T)
 abstract fun <T> readData(key: String, default: T): T
 open suspend fun <T> saveDataAsync(key: String, data: T) = saveData(key, data)
 abstract fun remove(key: String)
 abstract fun contains(key: String): Boolean
 var externalDataChangedFlow: MutableSharedFlow<Pair<String, Any?>>? =
 if (senseExternalDataChange) MutableSharedFlow(replay = 1) else null
}

然后将 LocalDataSaver 提供的对象更改为您自己的类实例

val dataSaverXXX = DataSaverXXX()
CompositionLocalProvider(LocalDataSaver provides dataSaverXXX){
 ExampleComposable()
}

后续相同使用即可。

感知外部数据变化

自 v1.1.6 起,框架加入了有限的对外部数据变化感知的支持,具体来说,就是当您在外部修改了某个 key 对应的值时,框架会自动感知到并更新对应的 MutableDataSaverState,从而触发 Composable 的更新。

目前,仅有 rememberDataSaverState 支持此功能,您需要设置 senseExternalDataChange 参数为 true。同时,对应的 DataSaverInterface 也需要设置 senseExternalDataChange 为 true

val dataSaverXXX = DataSaverXXX(senseExternalDataChange = true)
CompositionLocalProvider(LocalDataSaver provides dataSaverXXX){
 val stringExample by rememberDataSaverState(
 key = key,
 initialValue = "Hello World(1)",
 senseExternalDataChange = true
 )
 ...
 onClick = {
 // 外部修改了key对应的值,此时Composable会自动更新
 dataSaverXXX.saveData(key, "Hello World(2)")
 }
}

其中,MMKV 本身不支持感知数据变化,因此它的数据变化是 DataSaverMMKV 手动提交的。如果你在使用 MMKV 时需要感知数据变化,那么需要调用 DataSaverMMKV::saveData 来做数据保存才可以;Desktop 的基于 Properties 的实现均不支持感知外部数据变化

请注意,当新数据为 null 时,会有以下情况:

  • 当使用 rememberDataSaverState
    • 如果 T 为可空类型,比如 ExampleBean? ,那么正确的设置为 null
    • 如果 T 为非空类型,比如 ExampleBean ,那么 State 的 value 会重新变为 initialValue

高级设置

控制保存策略

v1.1.0 将原先的 autoSave 升级为了 savePolicy,以控制是否做、什么时候做数据持久化,该值默认为IMEDIATELY

该类目前包含下面三种值:

open class SavePolicy {
 /**
 * 默认模式,每次给state的value赋新值时就做持久化
 */
 object IMMEDIATELY : SavePolicy()
 /**
 * Composable `onDispose` 时做数据持久化,适合数据变动比较频繁、且此Composable会进入onDispose的情况。
 * **慎用此模式,因为有些情况下onDispose不会被回调**
 */
 object DISPOSED: SavePolicy()
 /**
 * 不会自动做持久化操作,请按需自行调用`state.saveData()`。
 * Example: `onClick = { state.saveData() }`
 */
 object NEVER : SavePolicy()
}

设置库参数

目前,库提供了一些可以设置的参数,它们位于DataSaverConfig

/**
 * 1. DEBUG: 是否输出库的调试信息
 */
object DataSaverConfig {
 var DEBUG = true
}

异步保存

v1.1.0 对 DataSaverInterface 新增了 suspend fun saveDataAsync ,用于异步保存。默认情况下,它等同于 saveData。对于支持协程的框架(如DataStore),使用此实现有助于充分利用协程优势(默认给出的DataStorePreference就是如此)。

mutableDataSavarStateOf的函数调用处可以设置async以启用异步保存,默认为true

@Preview 支持

项目自 v1.1.6 起支持了 @Preview。具体来说,由于 @Preview 模式下无法正常使用 CompositionLocalProvider,因此额外实现了 DataSaverInMemory,它使用 HashMap 来存储数据,从而不依赖于本地存储以及 CompositionLocalProvider

@Composable
@ReadOnlyComposable
fun getLocalDataSaverInterface() =
 if (LocalInspectionMode.current) DefaultDataSaverInMemory else LocalDataSaver.current

@Preview 模式下,您可能需要重新调用一遍 registerTypeConverter 以重新注册类型转换器。

使用的项目

目前,此库已在下列项目中使用:

如果您正在使用此项目,也欢迎您告知我以补充。

有任何建议或bug报告,欢迎提交issue。PR 就更好啦。

About

在 Compose Multiplatform ( Android / JVM Desktop) 中优雅完成数据持久化 | An elegant way to do data persistence in Compose Multiplatform ( Android / JVM Desktop )

Topics

Resources

License

Stars

Watchers

Forks

Packages

No packages published

AltStyle によって変換されたページ (->オリジナル) /