diff --git a/README.md b/README.md index da2b85d..d9ea496 100644 --- a/README.md +++ b/README.md @@ -4,26 +4,23 @@ |---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| ------------------------------------------------------------ | | [English Version](README_en.md) | +> 英文的 README 是由[译站](https://github.com/FunnySaltyFish/Transtation-KMP)的长文翻译功能直接一键翻译自中文版本的。这是一个强大的翻译应用程序,利用大型语言模型的力量进行翻译,也由我开发。它还是一个**开源的Compose跨平台应用,并使用这个库来保存数据**。如果你在寻找一个完整的项目,可以去那看看 优雅地在 Compose Multiplatform ( Android / JVM Desktop ) 中完成数据持久化 ```kotlin -// booleanExample 初始化值为false +// booleanExample 初始化值为 false // 之后会自动读取本地数据 -var booleanExample by rememberDataSaverState(KEY_BOOLEAN_EXAMPLE, false) +var booleanExample by rememberDataSaverState("KEY_BOOLEAN_EXAMPLE", false) // 直接赋值即可完成持久化 booleanExample = true ``` - -- :tada: 简洁:近似原生Compose的写法 +- :tada: 简洁:近似原生 Compose 函数的写法 - :tada: 低耦合:抽象接口,不限制底层保存算法实现 -- :tada: 轻巧:默认不引入除Compose外任何第三方库(主体jar包约**10kb**,可选的实现仅**1kb**) -- :tada: 强大:支持基本的数据类型和自定义类型、支持List类型 - -**注:此库是对Compose中使用其他框架(比如 Preference、MMKV、DataStore)的封装,不是一个单独的数据保存框架**。您可以参考[此链接](https://juejin.cn/post/7144750071156834312)以了解它的设计思想。 - +- :tada: 强大:支持基本的数据类型和自定义类型 +**注:此库是对Compose中使用其他框架(比如 Preference、MMKV、DataStore 等)的封装,不是一个单独的数据保存框架**。您可以参考[此链接](https://juejin.cn/post/7144750071156834312)以了解它的设计思想。 Example @@ -56,12 +53,13 @@ dependencies { ## 示例代码 以下介绍的示例代码均可在 [这里](composeApp/src/commonMain/kotlin/com/funny/data_saver/ui/ExampleComposables.kt) 查看具体实现 -## 基本使用 +## 配置 -项目使用`DataSaverInterface`的实现类来保存数据,因此您需要先提供一个此类对象。 +项目使用 `DataSaverInterface` 的实现类来保存数据,因此**您需要先提供一个此类对象。** ### Android -项目默认包含了使用`Preference`保存数据的实现类`DataSaverPreferences`,可如下初始化: +#### Perference +项目默认包含了使用 `Preference` 保存数据的实现类 `DataSaverPreferences`,可如下初始化: ```kotlin // init preferences @@ -71,76 +69,9 @@ CompositionLocalProvider(LocalDataSaver provides dataSaverPreferences){ } ``` -### JVM Desktop -默认包含了基于 `java.util.Properties` 的实现类 `DataSaverProperties`,您可以如下初始化: - -```kotlin -// init properties -val dataSaver = DataSaverProperties("$userHome/$projectName/$filename") -CompositionLocalProvider(LocalDataSaver provides dataSaver){ - ExampleComposable() -} -``` - -此后在`ExampleComposable`及其子微件内部可使用`LocalDataSaver.current`获取当前实例 - -对于基本数据类型(如String/Int/Boolean): - -```kotlin -// booleanExample 初始化值为false -// 之后会自动读取本地数据 -var booleanExample by rememberDataSaverState(KEY_BOOLEAN_EXAMPLE, false) -// 直接赋值即可完成持久化 -booleanExample = true -``` - -v1.1.0 起新增了对列表的支持,使用方式为: - -```kotlin -var listExample by rememberDataSaverListState(key = "key_list_example", default = listOf(...)) -// 删除一个 -onClick = { listExample = listExample.dropLast(1) } -``` - -通过赋值,数据即可自动转换、存于本地。就这么简单! - - -## 在Composable函数外使用 - -有些情况下,您可能需要将 `DataSaverState` 置于 `@Composable` 函数外面,比如放在 `ViewModel` 中。v1.1.0提供了 `mutableDataSavarStateOf` 函数用于此用途,该函数将会自动读取并转换已保存的值,并返回State。 - -此函数签名如下: - -```Kotlin -/** - * This function READ AND CONVERT the saved data and return a [DataSaverMutableState]. - * Check the example in `README.md` to see how to use it. - * - * 此函数 **读取并转换** 已保存的数据,返回 [DataSaverMutableState] - * - * @param key String 键 - * @param initialValue T 如果本地还没保存过值,此值将作为初始值;其他情况下会读取已保存值 - * @param savePolicy 管理是否。何时做持久化操作,见 [SavePolicy] - * @param async 是否异步做持久化 - * @return DataSaverMutableState - * - * @see DataSaverMutableState - */ -inline fun mutableDataSaverStateOf( - dataSaverInterface: DataSaverInterface, - key: String, - initialValue: T, - savePolicy: SavePolicy = SavePolicy.IMMEDIATELY, - async: Boolean = true -): DataSaverMutableState -``` - - -## 自定义存储框架 - -我们提供了基于 [MMKV](https://github.com/Tencent/MMKV) 或者 [DataStorePreference](https://developer.android.google.cn/jetpack/androidx/releases/datastore) 的简单实现 +除此之外, 我们也提供了基于 [MMKV](https://github.com/Tencent/MMKV) 或者 [DataStorePreference](https://developer.android.google.cn/jetpack/androidx/releases/datastore) 的简单实现 -### MMKV +#### MMKV 1. 在上述依赖基础上,额外添加 @@ -153,6 +84,7 @@ implementation 'com.tencent:mmkv:1.2.14' 2. 如下初始化 ```kotlin +// 全局初始化 MMKV,比如在 Application 的 onCreate 中 MMKV.initialize(applicationContext) ... @@ -167,7 +99,7 @@ CompositionLocalProvider(LocalDataSaver provides dataSaverMMKV){ --- -### DataStorePreference +#### DataStorePreference 1. 在上述依赖基础上,额外添加 @@ -191,71 +123,64 @@ CompositionLocalProvider(LocalDataSaver provides dataSaverDataStorePreferences){ ``` - -四者默认支持的类型如下所示 - -| 类型 | DataSaverPreference | DataSaverMMKV | DataSaverDataStorePreferences | DataSaverProperties | -|:---------:|:-------------------:|:-------------:|:-----------------------------:|:-------------------:| -| Int | Y | Y | Y | Y | -| Boolean | Y | Y | Y | Y | -| String | Y | Y | Y | Y | -| Long | Y | Y | Y | Y | -| Float | Y | Y | Y | Y | -| Double | | Y | Y | Y | -| Parceable | | Y | | | -| ByteArray | | Y | | | - - -更多类型的支持请参见 [保存自定义类型](#保存自定义类型) - ---- - -### 使用其他存储框架 - -只需要实现`DataSaverInterface`类,并重写`saveData`和`readData`方法分别用于保存数据和读取数据。对于一些支持协程的框架(如DataStore),您也可以重写`saveDataAsync`以实现异步的保存 +### JVM Desktop +默认包含了基于 `java.util.Properties` 的实现类 `DataSaverProperties`,您可以如下初始化: ```kotlin -abstract class DataSaverInterface(val senseExternalDataChange: Boolean = false) { - abstract fun saveData(key: String, data: T) - abstract fun readData(key: String, default: T): T - open suspend fun saveDataAsync(key: String, data: T) = saveData(key, data) - abstract fun remove(key: String) - abstract fun contains(key: String): Boolean - - var externalDataChangedFlow: MutableSharedFlow>? = - if (senseExternalDataChange) MutableSharedFlow(replay = 1) else null +// init properties +val dataSaver = DataSaverProperties("$userHome/$projectName/config.properties") +CompositionLocalProvider(LocalDataSaver provides dataSaver){ + ExampleComposable() } ``` -然后将LocalDataSaver提供的对象更改为您自己的类实例 +如果您需要加密存储,可以使用 `DataSaverEncryptedProperties` 的实现。它基于 AES 算法加密每一项值,您需要提供一个密钥。 ```kotlin -val dataSaverXXX = DataSaverXXX() -CompositionLocalProvider(LocalDataSaver provides dataSaverXXX){ +val dataSaver = DataSaverEncryptedProperties("$userHome/$projectName/data_saver_encrypted.properties", "FunnySaltyFish") +CompositionLocalProvider(LocalDataSaver provides dataSaver){ ExampleComposable() } ``` -后续相同使用即可。 +几者默认支持的类型如下所示 + +| 类型 | DataSaverPreference | DataSaverMMKV | DataSaverDataStorePreferences | DataSaverProperties/DataSaverEncryptedProperties | +| :-------: | :-----------------: | :-----------: | :---------------------------: | :----------------------------------------------: | +| Int | Y | Y | Y | Y | +| Boolean | Y | Y | Y | Y | +| String | Y | Y | Y | Y | +| Long | Y | Y | Y | Y | +| Float | Y | Y | Y | Y | +| Double | | Y | Y | Y | +| Parceable | | Y | | | +| ByteArray | | Y | | | + -## 保存自定义类型 +## 保存数据 -自`1.1.0`起,对自定义类型提供了更完善的支持。 +完成了 CompositionLocalProvider 的赋值后,在其子微件内部可使用 `getLocalDataSaverInterface()` 获取当前 `DataSaverInterface` 实例 -因为默认的`DataSaverPreferences`并不提供自定义类型的保存(当尝试这样做时会报错),所以您可以从以下两种方式中**任选其一**以保存自定义数据。 +对于基本数据类型(如String/Int/Boolean)等: +```kotlin +// booleanExample 初始化值为 false +// 之后会自动读取本地数据 +var booleanExample by rememberDataSaverState("KEY_BOOLEAN_EXAMPLE", false) +// 直接赋值即可完成持久化 +booleanExample = true +``` -1. 通过`DataSaverConverter.registerTypeConverters`将实体类序列化为 String 再储存 -2. 重写自己的`DataSaverInterface`实现类(见上)并实现相关的保存方法 +通过赋值,数据即可自动转换、存于本地。就这么简单! -对于第一种方式,您需要为对应实体类添加转换器,以实现保存时自动转换为String、并从String还原。方法如下: +而对于其他数据类型,您需要自己注册类型转换器,告诉框架如何将您的数据转换为字符串,以及如何从字符串还原: ```kotlin @Serializable -data class ExampleBean(var id:Int, val label:String) +data class ExampleBean(var id: Int, val label: String) // ------------ // // 在初始化时调用registerTypeConverters方法注册对应转换方法 @@ -267,16 +192,80 @@ registerTypeConverters( ) ``` -通过注册类型转换器,框架即可在remember和save时自动尝试转换。甚至,如果您为 `ExampleBean` 注册了转换器,那么 `List` **也将自动得到支持**(通过 `rememberDataSaverListState` ) +如果您需要存储可空变量,请使用 `registerTypeConverters`。 + +> 请注意,出于代码的实现上的考虑,对于可空类型,设置 `state.value = null` 或 `dataSaverInterface.saveData(key, null)` 实际**将调用对应 `remove` 方法直接移除对应值**。这意味着,框架的默认实现没有办法正确的保存 “null” 值。当 `state.value = null` 设置完且下次重新打开应用后,**框架会认为此 `key` 对应的本地值不存在,会将 value 设为 initialValue**。 +> 如果您需要真的存储 “null” 且 `initialValue != null`,请手动处理这部分逻辑。比如,设置一个特殊的值来代表 “null” ,比如 `ExampleBean(-1, "null")`;如果您有更好的方案,欢迎 PR! + + +自 v1.2.1 起,您除了使用类型信息来注册转换器,也可以自己写上其他判定条件: + +```kotlin +inline fun registerTypeConverters( + noinline save: (T) -> String, + noinline restore: (String) -> T, + noinline acceptCondition: (T) -> Boolean +) +``` + +当 `acceptCondition` 为 `true` 时,框架会调用对应 `save` 和 `restore` 方法转换对应数据。 + +> **注意:** +> +> 1. registerTypeConverters 请在初始化时调用,确保早于使用 `rememberDataSaverState("key", ExampleBean())` 之前 +> 2. 多个类型转换器会按照注册顺序依次尝试,直到找到合适的转换器。因此,如果您注册了多个相同类型的转换器,框架会使用第一个注册的转换器。 +> 3. 您可以通过 `DataSaverConverters.typeConverters` 获取到注册的全部转换器列表,初始会有默认的一些,如对 `String`、`emptyList`、`emptyMap` 的支持 + + + +## 在 Composable 函数外使用 + +有些情况下,您可能需要将 `DataSaverState` 置于 `@Composable` 函数外面,比如放在 `ViewModel` 中。v1.1.0 提供了 `mutableDataSavarStateOf` 函数用于此用途,该函数将会自动读取并转换已保存的值,并返回 State。 + +```Kotlin +object AppConfig { + val dataSaver = DataSaverMMKV(...) +} + +class MyViewModel: ViewModel() { + var username: String by mutableDataSavarStateOf(AppConfig.dataSaver, "username", "") +} +``` + + +## 使用其他存储框架 + +如果默认提供的几种实现无法满足您的需求,您也可以自行继承 `DataSaverInterface`,并重写 `saveData` 和 `readData` 方法分别用于保存数据和读取数据。对于一些支持协程的框架(如DataStore),您也可以重写 `saveDataAsync` 以实现异步的保存 + +```kotlin +abstract class DataSaverInterface(val senseExternalDataChange: Boolean = false) { + abstract fun saveData(key: String, data: T) + abstract fun readData(key: String, default: T): T + open suspend fun saveDataAsync(key: String, data: T) = saveData(key, data) + abstract fun remove(key: String) + abstract fun contains(key: String): Boolean + + var externalDataChangedFlow: MutableSharedFlow>? = + if (senseExternalDataChange) MutableSharedFlow(replay = 1) else null +} +``` + +然后将 LocalDataSaver 提供的对象更改为您自己的类实例 +```kotlin +val dataSaverXXX = DataSaverXXX() +CompositionLocalProvider(LocalDataSaver provides dataSaverXXX){ + ExampleComposable() +} +``` +后续相同使用即可。 -完整例子见 [示例项目](composeApp/src/commonMain/kotlin/com/funny/data_saver/AppConfig.kt) ## 感知外部数据变化 -自 v1.1.6 起,框架加入了**有限的对外部数据变化感知的支持**,具体来说,就是当您在外部修改了某个key对应的值时,框架会自动感知到并更新对应的`MutableDataSaverState`,从而触发Composable的更新。 +自 v1.1.6 起,框架加入了**有限的对外部数据变化感知的支持**,具体来说,就是当您在外部修改了某个 key 对应的值时,框架会自动感知到并更新对应的 `MutableDataSaverState`,从而触发 Composable 的更新。 -目前,仅有 `rememberDataSaverState` 和 `rememberDataSaverListState` 支持此功能,您需要设置 `senseExternalDataChange` 参数为 `true`。同时,对应的 `DataSaverInterface` 也需要设置 `senseExternalDataChange` 为 true +目前,仅有 `rememberDataSaverState` 支持此功能,您需要设置 `senseExternalDataChange` 参数为 `true`。同时,对应的 `DataSaverInterface` 也需要设置 `senseExternalDataChange` 为 true ```kotlin val dataSaverXXX = DataSaverXXX(senseExternalDataChange = true) @@ -293,20 +282,18 @@ CompositionLocalProvider(LocalDataSaver provides dataSaverXXX){ } } ``` -其中,MMKV 本身不支持感知数据变化,因此它的数据变化是 `DataSaverMMKV` 手动提交的。如果你在使用 MMKV 时需要感知数据变化,那么需要调用 `DataSaverMMKV::saveData` 来做数据保存才可以。 +其中,MMKV 本身不支持感知数据变化,因此它的数据变化是 `DataSaverMMKV` 手动提交的。如果你在使用 MMKV 时需要感知数据变化,那么需要调用 `DataSaverMMKV::saveData` 来做数据保存才可以;Desktop 的基于 Properties 的实现均不支持感知外部数据变化 请注意,当新数据为 null 时,会有以下情况: - 当使用 `rememberDataSaverState` 时 - 如果 T 为可空类型,比如 ExampleBean? ,那么正确的设置为 null - 如果 T 为非空类型,比如 ExampleBean ,那么 State 的 value 会重新变为 initialValue -- 当使用 `rememberDataSaverListState` 时 - - State 的 value 会重新变为 initialValue ## 高级设置 ### 控制保存策略 -v1.1.0 将原先的 `autoSave` 升级为了 `savePolicy`,以控制是否做、什么时候做数据持久化。`mutableDataSaverStateOf`、`rememberDataSaverState` 均包含此参数,默认为`IMEDIATELY` +v1.1.0 将原先的 `autoSave` 升级为了 `savePolicy`,以控制是否做、什么时候做数据持久化,该值默认为`IMEDIATELY` 该类目前包含下面三种值: @@ -331,8 +318,6 @@ open class SavePolicy { } ``` - - ### 设置库参数 目前,库提供了一些可以设置的参数,它们位于`DataSaverConfig`下 @@ -340,43 +325,17 @@ open class SavePolicy { ```Kotlin /** * 1. DEBUG: 是否输出库的调试信息 - * 2. LIST_SEPARATOR: 内置的 列表转字符串 使用的分隔符,默认为'#@#'。(**请不要使用 ',',因为单个bean 序列化后的json中会包含它** ) */ object DataSaverConfig { var DEBUG = true - var LIST_SEPARATOR = "#@#" } ``` - - ### 异步保存 -v1.1.0 对`DataSaverInterface` 新增了 `suspend fun saveDataAsync` ,用于异步保存。默认情况下,它等同于 `saveData`。对于支持协程的框架(如`DataStore`),使用此实现有助于充分利用协程优势(默认给出的`DataStorePreference`就是如此)。 - -在`mutableDataSavarStateOf` 和 `rememberMutableDataSavarState` 函数调用处可以设置`async`以启用异步保存,默认为`true`。 - - - -### Null 支持 - -v1.1.4 放宽了自定义类型时为 `null` 的情况,可以通过 - -```kotlin -registerTypeConverters( - save = { bean -> Json.encodeToString(bean) }, - restore = { str -> Json.decodeFromString(str) } -) -``` - -来支持 `null` 作默认值 - -```kotlin -val nullableCustomBeanState: DataSaverMutableState = rememberDataSaverState(key = "nullable_bean", initialValue = null) -``` +v1.1.0 对 `DataSaverInterface` 新增了 `suspend fun saveDataAsync` ,用于异步保存。默认情况下,它等同于 `saveData`。对于支持协程的框架(如`DataStore`),使用此实现有助于充分利用协程优势(默认给出的`DataStorePreference`就是如此)。 -请注意,出于代码的实现上的考虑,设置 `state.value = null` 或 `dataSaverInterface.saveData(key, null)` 实际**将调用对应 `remove` 方法直接移除对应值**。这意味着,框架的默认实现没有办法正确的保存 “null” 值。当 `state.value = null` 设置完且下次重新打开应用后,**框架会认为此 `key` 对应的本地值不存在,会将 value 设为 initialValue**。 -如果您需要真的存储 “null” 且 `initialValue != null`,请手动处理这部分逻辑。比如,设置一个特殊的值来代表 “null” ,比如 `ExampleBean(-1, "null")`;如果您有更好的方案,欢迎 PR! +在`mutableDataSavarStateOf`的函数调用处可以设置`async`以启用异步保存,默认为`true`。 ### @Preview 支持 @@ -395,7 +354,8 @@ fun getLocalDataSaverInterface() = 目前,此库已在下列项目中使用: -- [译站:基于 KMP + CMP 实现的 AI 翻译软件](https://github.com/FunnySaltyFish/Transtation-KMP) +- [译站:基于 KMP + CMP 实现的 AI 翻译软件 | 总 Star 380+](https://github.com/FunnySaltyFish/Transtation-KMP) +- [tts-server-android | Star 3k+ ](https://github.com/jing332/tts-server-android) - [Github 中搜索](https://github.com/search?q=mutableDataSaverStateOf&type=code) 如果您正在使用此项目,也欢迎您告知我以补充。 diff --git a/README_en.md b/README_en.md index 0d881eb..6a4e752 100644 --- a/README_en.md +++ b/README_en.md @@ -1,37 +1,35 @@ # ComposeDataSaver | [![Maven Central](https://img.shields.io/maven-central/v/io.github.FunnySaltyFish/data-saver-core)](https://central.sonatype.com/artifact/io.github.FunnySaltyFish/data-saver-core) | [![License](https://img.shields.io/badge/License-Apache%202.0-blue.svg)](http://www.apache.org/licenses/LICENSE-2.0) | -|---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| ------------------------------------------------------------ | +| ------------------------------------------------------------ | ------------------------------------------------------------ | -An elegant way to do data persistence in Compose Multiplatform ( Android / JVM Desktop ). +> The English README is translated from the Chinese version by [Transtation](https://github.com/FunnySaltyFish/Transtation-KMP). It is an powerful translation app which leverages the power of Large Language Models to do translation, developed by me as well. It is also an **Open Source Compose Multiplatform Application, and uses this library to save data**. If you're looking for a complete project, you can go there. + +Elegantly accomplish data persistence in Compose Multiplatform (Android/JVM Desktop) ```kotlin -// booleanExample will be initialized to false -// and will be automatically read from local storage later -var booleanExample by rememberDataSaverState(KEY_BOOLEAN_EXAMPLE, false) -Switch(checked = booleanExample, onCheckedChange = { - // Persistence can be easily completed by assignment. - booleanExample = it -}) +// booleanExample is initialized with false +// Local data will be automatically loaded later +var booleanExample by rememberDataSaverState("KEY_BOOLEAN_EXAMPLE", false) +// Direct assignment completes persistence +booleanExample = true ``` +- :tada: Concise: Writing style similar to native Compose functions +- :tada: Loose Coupling: Abstract interface, not limiting the underlying storage algorithm implementation +- :tada: Powerful: Supports basic data types and custom types -- :tada: Brevity: a nearly native style of coding -- :tada: Low coupling: using abstract interface that does not restrict the implementation of the underlying persistence framework -- :tada: Lightweight: no third-party libraries other than Compose are included by default. (the size of `sources-jar` is only about **10kb** for `core` and **1kb** per optional implementation) -- :tada: Flexibility: both basic data types and custom beans are supported +**Note: This library is a wrapper for using other frameworks (such as Preference, MMKV, DataStore, etc.) in Compose, not a standalone data storage framework**. You can refer to [this link](https://juejin.cn/post/7144750071156834312) to understand its design concepts. +Example -
-Example
- -You can download the demo [here](demo.apk). +You can click [here to download the demo for experience](demo.apk) (Debug package, may be slower compared to the release package) --- -## Implementation +## Introduction -Add maven central url in `settings.gradle` +Include the repository location in `settings.gradle` ```bash dependencyResolutionManagement { @@ -41,158 +39,192 @@ dependencyResolutionManagement { } ``` -add implementation in module's `build.gradle` +Include in project `build.gradle` ```bash dependencies { implementation "io.github.FunnySaltyFish:data-saver-core:{version}" } ``` +> Note: Starting from v1.2.0, the repository has been migrated to Compose Multiplatform and released to Maven Central. The Group Id has also been changed. When upgrading from versions before v1.2.0, please remember to make the necessary changes. -> NOTICE: the group id had been changed since v1.2.0 from `com.funny.ComposedDataSaver` to `io.github.FunnySaltyFish` - -## Basic Usage +## Sample Code +The sample codes described below can be viewed in detail [here](composeApp/src/commonMain/kotlin/com/funny/data_saver/ui/ExampleComposables.kt). -This library uses classes which implements interface `DataSaverInterface` to save data,thus you need to provide an instance of it. +## Configuration -This library includes a default implementation class for each platform, there are: +The project uses an implementation class of `DataSaverInterface` to save data, so **you need to provide an object of this class first.** -- Android: using `Preference` to save data, the class is `DataSaverPreferences`. You can initialize it like this: +### Android +#### Preference +The project comes with a default implementation class `DataSaverPreferences` that uses `Preference` to save data, which can be initialized as follows: ```kotlin -// init preferences -val dataSaverPreferences = DataSaverPreferences().apply { - setContext(context = applicationContext) -} +// Init preferences +val dataSaverPreferences = DataSaverPreferences(applicationContext) CompositionLocalProvider(LocalDataSaver provides dataSaverPreferences){ - ExampleComposable() + ExampleComposable() } ``` -- JVM Desktop: using `java.util.Properties` to save data, the class is `DataSaverProperties`. You can initialize it like this: - -```kotlin -// init properties -val dataSaver = DataSaverProperties("$userHome/$projectName/$filename") -CompositionLocalProvider(LocalDataSaver provides dataSaver){ - ExampleComposable() -} -``` - -After that, you can use `getLocalDataSaverInterface()` to access to the instance inside `ExampleComposable` and its children. - -For basic data types like String/Int/Boolean : +Additionally, we also provide simple implementations based on [MMKV](https://github.com/Tencent/MMKV) or [DataStorePreference](https://developer.android.google.cn/jetpack/androidx/releases/datastore). -```kotlin -// booleanExample is initialized as false -// later it will read local data automatically -var booleanExample by rememberDataSaverState(KEY_BOOLEAN_EXAMPLE, false) -// Persistence can be easily completed by direct assignment. -booleanExample = true -``` - -After v1.1.0, we also support list type: - -```kotlin -var listExample by rememberDataSaverListState(key = "key_list_example", default = listOf(...)) -// drop one item -onClick = { listExample = listExample.dropLast(1) } -``` - -## Use It Outside Composable Function -Some times, you may want to use it outside composable function, like in `ViewModel` or `object`. You can use `mutableDataSavarStateOf` to create a `DataSaverMutableState`. The function will read the saved value and convert it automatically (if that type is supported). - -The signature of `mutableDataSavarStateOf` is: - -```kotlin -/** - * This function READ AND CONVERT the saved data and return a remembered [DataSaverMutableState]. - - * @param key String - * @param initialValue T default value if it is initialized the first time - * @param savePolicy how and when to save data, see [SavePolicy] - * @param async whether to save data asynchronously - * @return DataSaverMutableState - * - * @see DataSaverMutableState - */ -@Composable -inline fun rememberDataSaverState( - key: String, - initialValue: T, - savePolicy: SavePolicy = SavePolicy.IMMEDIATELY, - async: Boolean = true -): DataSaverMutableState -``` +#### MMKV +1. In addition to the above dependencies, add the following: -## Custom storage framework -We provide the basic implementations of using [MMKV](https://github.com/Tencent/MMKV) or [DataStorePreference](https://developer.android.google.cn/jetpack/androidx/releases/datastore) on Android platform. +```bash +// If you want to use MMKV +The following: -### MMKV -1. Add extra implementations as below: ```bash -// if you want to use mmkv -implementation "io.github.FunnySaltyFish:data-saver-mmkv:{version}" -implementation 'com.tencent:mmkv:1.2.12' +// If you want to use MMKV +implementation "io.github.FunnySaltyFish:data-saver-mmkv:{tag}" +implementation 'com.tencent:mmkv:1.2.14' ``` -2. Initialize it as below: + +2. Initialize as follows ```kotlin +// Global initialization of MMKV, for example in the onCreate of the Application MMKV.initialize(applicationContext) -val dataSaverMMKV = DataSaverMMKV().apply { - setKV(newKV = MMKV.defaultMMKV()) -} +... + +val dataSaverMMKV = DefaultDataSaverMMKV +// DefaultDataSaverMMKV is our default implementation provided, you can use it anywhere, just like MMKVUtils +// If you need to customize MMKV, you can choose DataSaverMMKV(MMKV.defaultMMKV()) CompositionLocalProvider(LocalDataSaver provides dataSaverMMKV){ // ... } ``` + --- -### DataStorePreference +#### DataStorePreference + +1. Besides the above dependencies, add the following: -1. Add extra implementations as below: ```bash // if you want to use DataStore -implementation "io.github.FunnySaltyFish:data-saver-data-store-preferences:{version}" +implementation "io.github.FunnySaltyFish:data-saver-data-store-preferences:{tag}" def data_store_version = "1.0.0" implementation "androidx.datastore:datastore:$data_store_version" implementation "androidx.datastore:datastore-preferences:$data_store_version" ``` -2. Initialize it as below: + +2. Initialization as follows ```kotlin val Context.dataStore : DataStore by preferencesDataStore("dataStore") -val dataSaverDataStorePreferences = DataSaverDataStorePreferences().apply { - setDataStorePreferences(applicationContext.dataStore) -} +val dataSaverDataStorePreferences = DataSaverDataStorePreferences(applicationContext.dataStore) CompositionLocalProvider(LocalDataSaver provides dataSaverDataStorePreferences){ // ... } ``` -The default data types that supported by these four are as follows: +### JVM Desktop +By default, it includes an implementation class based on `java.util.Properties` called `DataSaverProperties`, which you can initialize as follows: -| Type | DataSaverPreference | DataSaverMMKV | DataSaverDataStorePreferences | DataSaverProperties | -|:---------:|:-------------------:|:-------------:|:-----------------------------:|:-------------------:| -| Int | Y | Y | Y | Y | -| Boolean | Y | Y | Y | Y | -| String | Y | Y | Y | Y | -| Long | Y | Y | Y | Y | -| Float | Y | Y | Y | Y | -| Double | | Y | Y | Y | -| Parceable | | Y | | | -| ByteArray | | Y | | | +```kotlin +// init properties +val dataSaver = DataSaverProperties("$userHome/$projectName/config.properties") +CompositionLocalProvider(LocalDataSaver provides dataSaver){ + ExampleComposable() +} +``` ---- +If you need encrypted storage, you can use the implementation of `DataSaverEncryptedProperties`. It encrypts each value based on the AES algorithm, and you need to provide a key. + +The supported types by each of them are as follows: + + +| Type | DataSaverPreference | DataSaverMMKV | DataSaverDataStorePreferences | DataSaverProperties/DataSaverEncryptedProperties | +| :-------: | :-----------------: | :-----------: | :---------------------------: | :----------------------------------------------: | +| Int | Y | Y | Y | Y | +| Boolean | Y | Y | Y | Y | +| String | Y | Y | Y | Y | +| Long | Y | Y | Y | Y | +| Float | Y | Y | Y | Y | +| Double | | Y | Y | Y | +| Parceable | | Y | | | +| ByteArray | | Y | | | + + +## Save Data + +After assigning the `CompositionLocalProvider`, you can use `getLocalDataSaverInterface()` inside its sub-widgets to get the current `DataSaverInterface` instance. + +For basic data types (such as String/Int/Boolean), etc.: + +```kotlin +// booleanExample is initialized as false +// It will automatically read local data later +var booleanExample by rememberDataSaverState("KEY_BOOLEAN_EXAMPLE", false) +// Simply assign a value to achieve persistence +booleanExample = true +``` -### Others +By assigning, the data can be automatically converted and stored locally. It's as simple as that! -Your class just needs to implement the interface`DataSaverInterface` and override the required methods. -For some frameworks that support `Coroutine`, you can override `saveDataAsync` to save data asynchronously. +For other data types, you need to register type converters yourself to tell the framework how to convert your data to a string and how to restore it from a string: + +```kotlin +@Serializable +data class ExampleBean(var id: Int, val label: String) +// ------------ // + +// Call registerTypeConverters method to register corresponding conversion methods during initialization +// This method accepts two parameters: for converting to a serializable type for saving and deserializing to your Bean +// Here, we use Json.encodeToString and Json.decodeFromString, you can also use Gson, Fastjson, etc. +registerTypeConverters( + save = { bean -> Json.encodeToString(bean) }, + restore = { str -> Json.decodeFromString(str) } +) +``` + +If you need to store nullable variables, please use `registerTypeConverters`. + +> Please note that, for implementation reasons, when dealing with nullable types, setting `state.value = null` or `dataSaverInterface.saveData(key, null)` will actually **call the corresponding `remove` method to remove the corresponding value directly**. This means that the default implementation of the framework cannot correctly save "null" values. When `state.value = null` is set and the application is reopened, **the framework will consider that the local value corresponding to this `key` does not exist, and will set the value to `initialValue`**. +> If you really need to store "null" and `initialValue != null`, please handle this part of the logic manually. For example, set a special value to represent "null", such as `ExampleBean(-1, "null")`; if you have a better solution, feel free to submit a pull request! + +Since v1.2.1, besides using type information to register converters, you can also write additional conditional statements: + +```kotlin +inline fun registerTypeConverters( + noinline save: (T) -> String, + noinline restore: (String) -> T, + noinline acceptCondition: (T) -> Boolean +) +``` + +The framework will call the corresponding `save` and `restore` methods to convert the corresponding data when `acceptCondition` is `true`. + +> **Note:** + +> 1. Please call `registerTypeConverters` during initialization to ensure it is called before using `rememberDataSaverState("key", ExampleBean())`. +> 2. Multiple type converters will be tried in the order of registration until a suitable converter is found. Therefore, if you register multiple converters of the same type, the framework will use the first registered converter. +> 3. You can access the list of all registered converters through `DataSaverConverters.typeConverters`. Some default converters are initially provided, such as support for `String`, `emptyList`, and `emptyMap`. + +## Using Outside of Composable Functions + +In some cases, you may need to place `DataSaverState` outside of a `@Composable` function, such as in a `ViewModel`. Starting from v1.1.0, the `mutableDataSavarStateOf` function is provided for this purpose, which will automatically read and convert the saved values, returning a State. + +```Kotlin +object AppConfig { + val dataSaver = DataSaverMMKV(...) +} + +class MyViewModel: ViewModel() { + var username: String by mutableDataSavarStateOf(AppConfig.dataSaver, "username", "") +} +``` + +## Using Other Storage Frameworks + +If the default provided implementations do not meet your needs, you can inherit from the `DataSaverInterface` yourself and override the `saveData` and `readData` methods respectively for saving data and reading data. For some coroutine-enabled frameworks (such as `DataStore`), you can also override `saveDataAsync` to achieve asynchronous saving. ```kotlin abstract class DataSaverInterface(val senseExternalDataChange: Boolean = false) { @@ -207,7 +239,7 @@ abstract class DataSaverInterface(val senseExternalDataChange: Boolean = false) } ``` -Then change the object provided by `LocalDataSaver` to your own class instance +Then change the object provided by `LocalDataSaver` to an instance of your own class. ```kotlin val dataSaverXXX = DataSaverXXX() @@ -216,99 +248,108 @@ CompositionLocalProvider(LocalDataSaver provides dataSaverXXX){ } ``` -Other usages remain unchanged. - +You can continue using it in the same way. +## Perceive External Data Changes +Since v1.1.6, the framework has added **limited support for perceiving external data changes**. Specifically, when you modify the value corresponding to a key externally, the framework will automatically perceive it and update the corresponding `MutableDataSaverState`, triggering the update of the Composable. -## Save Entity Classes - -The default `DataSaverPreferences` does not provide custom type saving (an error will be threw when trying to do so). So you can choose **one** of the following ways to achieve this goal. - -1. Convert the custom type to a `String` by using function `registerTypeConverters`. -2. Implement your `DataSaverInterface` class(see content above)and override required methods - -For the first method, you need to add a converter for the corresponding entity class to automatically convert it to String when saving and load it from String. The method is as follows: +Currently, only `rememberDataSaverState` supports this functionality, and you need to set the `senseExternalDataChange` parameter to `true`. Additionally, the corresponding `DataSaverInterface` also needs to set `senseExternalDataChange` to true. ```kotlin -@Serializable -data class ExampleBean(var id:Int, val label:String) -// ------------ // - -// call the method [registerTypeConverters] to register the corresponding conversion method during initialization (BEFORE CALLING `rememberDataSaverState`). -// this method receives two lambda functions, which are used to convert the entity class to String and read it from String respectively. -// we use `Json` here, you can use other frameworks(Gson/Fastjson/...). -registerTypeConverters( - save = { bean -> Json.encodeToString(bean) }, - restore = { str -> Json.decodeFromString(str) } -) +val dataSaverXXX = DataSaverXXX(senseExternalDataChange = true) +CompositionLocalProvider(LocalDataSaver provides dataSaverXXX){ + val stringExample by rememberDataSaverState( + key = key, + initialValue = "Hello World(1)", + senseExternalDataChange = true + ) + ... + onClick = { + // The value corresponding to the key is modified externally, and the Composable will automatically update at this point + dataSaverXXX.saveData(key, "Hello World(2)") + } +} ``` +Regarding MMKV itself, it does not support perceiving data changes, so its data changes are manually submitted by `DataSaverMMKV`.If you need to perceive data changes when using MMKV, you need to call `DataSaverMMKV::saveData` to save the data; the desktop-based Properties implementation does not support perceiving external data changes. -By doing this, you can use `rememberDataSaverState` to save and read the entity directly. Even more, you can use `rememberDataSaverListState` to save and read the list of corresponding class's entity without any additional code. - +Please note that when the new data is null, the following situations may occur: +- When using `rememberDataSaverState` + - If T is a nullable type, such as `ExampleBean?`, then it should be set to null. + - If T is a non-nullable type, such as `ExampleBean`, then the value of State will be reset to the `initialValue`. -To check the full code, see [example](composeApp/src/commonMain/kotlin/com/funny/data_saver/AppConfig.kt) +## Advanced Settings +### Controlling the Save Policy -## Advanced Settings +In v1.1.0, the original `autoSave` has been upgraded to `savePolicy` to control whether and when data persistence is done. The default value is `IMMEDIATELY`. -### SavePolicy -Since v1.1.0, we change the original `autoSave` to `savePolicy`. The default value is `SavePolicy.IMMEDIATELY`, which means that the data will be saved immediately after the value is changed. You can also set it to other values: +This class currently includes the following three values: -```kotlin -/** - * Controls whether and when to do data persistence. Includes [IMMEDIATELY], [DISPOSED] and [NEVER] by default. - * - */ +```Kotlin open class SavePolicy { /** - * Default mode, do data persistence every time you assign a new value to the state. + * Default mode, performs data persistence each time a new value is assigned to the state's value. */ object IMMEDIATELY : SavePolicy() /** - * do data persistence when the Composable enters `onDispose`. NOTE: USE THIS MODE CAREFULLY, BECAUSE SOMETIME - * `onDispose` will not be called + * Performs data persistence on Composable `onDispose`, suitable for cases where data changes frequently and this Composable will enter onDispose. + * **Use with caution, as onDispose may not be called in some situations.** */ object DISPOSED: SavePolicy() /** - * NEVER do data persistence automatically. Please call `state.saveData()` manually. - * + * Will not perform automatic persistence operations, please call `state.saveData()` as needed. * Example: `onClick = { state.saveData() }` */ object NEVER : SavePolicy() } ``` -### Configs -We provide some configurations for you to customize some behaviors of the library. +### Setting Library Parameters + +Currently, the library provides some parameters that can be set under `DataSaverConfig`. ```kotlin /** - * Some config that you can set: - * 1. DEBUG: whether to output some debug info - * 2. LIST_SEPARATOR: the separator used to convert a list into string, '#@#' by default (**don't use ',' which will occurs in json itself** ) + * 1. DEBUG: Whether to output debug information of the library */ object DataSaverConfig { var DEBUG = true - var LIST_SEPARATOR = "#@#" } ``` -## Async Saving -Since v1.1.0, we provide the ability to save data asynchronously. You can use `rememberDataSaverState` or `rememberDataSaverListState` to save data asynchronously by setting the `async` parameter to `true`. +### Asynchronous Saving + +In version 1.1.0, `suspend fun saveDataAsync` was added to `DataSaverInterface` for asynchronous saving. By default, it is equivalent to `saveData`. For frameworks that support coroutines (such as `DataStore`), using this implementation helps to fully leverage the advantages of coroutines (the default `DataStorePreference` does this). + +At the function call of `mutableDataSavarStateOf`, you can set `async` to enable asynchronous saving, which defaults to `true`. + +### @Preview Support +Starting from version 1.1.6, the project supports @Preview. Specifically, as `CompositionLocalProvider` cannot be used normally in @Preview mode, `DataSaverInMemory` was additionally implemented. It uses a `HashMap` to store data, eliminating the reliance on local storage and `CompositionLocalProvider`. + +```kotlin +@Composable +@ReadOnlyComposable +fun getLocalDataSaverInterface() = + if (LocalInspectionMode.current) DefaultDataSaverInMemory else LocalDataSaver.current +``` + +When using the @Preview mode, you may need to re-register the type converter by calling `registerTypeConverter` again. + +## Projects in use -The implementaion using `DataStorePreference` supports this feature well by defualt. +Currently, this library has been used in the following projects: -## Projects using this library -The library has been used in the following projects: +- [Transtation: AI translation software based on KMP + CMP | Total Stars 380+](https://github.com/FunnySaltyFish/Transtation-KMP) +- [tts-server-android | Star 3k+ ](https://github.com/jing332/tts-server-android) +- [Search on Github](https://github.com/search?q=mutableDataSaverStateOf&type=code) -- [A translation app on Android/Desktop built by Kotlin Multiplatform + Compose Multiplatform, enjoy amazing experience with LLMs' support](https://github.com/FunnySaltyFish/Transtation-KMP) -- [Search in Github](https://github.com/search?q=mutableDataSaverStateOf&type=code) +If you are using this project, feel free to let me know to include it here. -If you are using this library in your project, please let me know and I will add it to the list. +For any suggestions or bug reports, please submit an issue. Pull requests are even better. -If you have any questions, please feel free to create an issue. PRs are also welcome. -If this library helps, **a star** will be appreciated. Thanks! +-------------------- +This result is generated by Transtation App\'s Long Text Translation function with GPT-3.5-Turbo by one-click