Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

可以增强一下吗?DataSaverMutableListState 有时候在使用的时候并不需要创建默认数据,现在不支持 #7

Open
XingMingYue opened this issue Jul 22, 2024 · 19 comments

Comments

@XingMingYue
Copy link

希望可以使用类创建
比如
var entityList: List by rememberDataSaverListState(key = "main.entity.list", clazz = Entity::class.java)

@FunnySaltyFish
Copy link
Owner

您好,感谢使用,对于这种情况,如果您的 Entity 已经注册,可以手动注解类型+将默认值设置为 emptyList。代码为:

var entityList: List<Entity> by rememberDataSaverListState(key = "main.entity.list", initialValue = emptyList())

在 kotlin 标准库实现中,emptyList 会直接返回一个全局的单例,不会占用额外空间:

public fun <T> emptyList(): List<T> = EmptyList

internal object EmptyList : List<Nothing>, Serializable, RandomAccess {
  // ...
}

不过代码确实写的有问题,比如 List<String> 这种居然不能直接支持,这是很有问题的,将会改进。如果您有更多需求或者场景,欢迎继续讨论

@XingMingYue
Copy link
Author

感谢及时回复,我尝试过你说的这种方式,但是如果在已经保存的有数据的情况下加载数据是报错的
Exception in thread "main" java.lang.IllegalArgumentException: Unable to read [], this type(class kotlin.collections.EmptyList) cannot be read from Properties, call [registerTypeConverters] to support it.
at com.funny.data_saver.core.DataSaverProperties.readData(DataSaverProperties.kt:43)
at MainKt$App$1.invoke(Main.kt:303)
at MainKt$App$1.invoke(Main.kt:35)

@FunnySaltyFish
Copy link
Owner

你是否标注了具体的泛型,比如 List<Entity> ? 如果不标注,直接使用 emptyList,此变量会被推断为 List<Any>,无法正常工作

@XingMingYue
Copy link
Author

下面是我的代码,我在尝试使用自定义类型的时候会报错(添加一点数据,然后再次启动就可以重现),基础类型String不会报错

import AppConfig.dataSaver
import androidx.compose.desktop.ui.tooling.preview.Preview
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.lazy.LazyColumn
import androidx.compose.foundation.lazy.items
import androidx.compose.material3.Button
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.runtime.CompositionLocalProvider
import androidx.compose.ui.window.Window
import androidx.compose.ui.window.application
import cn.hutool.json.JSONUtil
import com.funny.data_saver.core.DataSaverConverter
import com.funny.data_saver.core.DataSaverInterface
import com.funny.data_saver.core.DataSaverProperties
import com.funny.data_saver.core.LocalDataSaver
import com.funny.data_saver.core.rememberDataSaverListState

// 实体
data class Entity(var name: String, var remark: String) {
    init {
        DataSaverConverter.registerTypeConverters<Entity>(
            save = { bean -> JSONUtil.toJsonStr(bean) },
            restore = { str ->
                JSONUtil.toBean(str, Entity::class.java)
            }
        )
    }
}

// 配置
object AppConfig {
    private const val FILE_NAME = "config.properties"
    private val userHome = System.getProperty("user.home")
    private const val PROJECT_NAME = "test-demo"
    val dataSaver: DataSaverInterface = DataSaverProperties("$userHome/$PROJECT_NAME/$FILE_NAME")

    init {
        DataSaverConverter.registerTypeConverters<String>(
            save = { bean -> bean.toString() },
            restore = { str -> str }
        )
    }
}

@Composable
@Preview
fun App() {
    var list: List<Entity> by rememberDataSaverListState(key = "main.entity.list", initialValue = emptyList())
    MaterialTheme {
        Column {
            Button(onClick = {
                list += Entity("new entity ${list.size + 1}", "remark")
            }) {
                Text("add new entity")
            }
            LazyColumn {
                items(list) {
                    Text(it.name)
                }
            }
        }
    }
}

fun main() = application {
    Window(onCloseRequest = ::exitApplication) {
        CompositionLocalProvider(LocalDataSaver provides dataSaver) {
            App()
        }
    }
}

@FunnySaltyFish
Copy link
Owner

感谢反馈,我试了下确实有问题,逻辑有些错误,将会在下一版本进行改正

@XingMingYue
Copy link
Author

非常感谢 ♪(・ω・)ノ,期待新版本

@FunnySaltyFish
Copy link
Owner

连写两天,新的 v1.2.1 已经发布。不过由于代码实现上有些变化,现在没有 DataSaverListState 了,你需要转用:

var list: List<Entity> = mutableDataSaverState(key = "main.entity.list", initialValue = emptyList())

同时注册 List<Entity> 的类型转换器

更多迁移指南和代码变更见:https://github.com/FunnySaltyFish/ComposeDataSaver/releases/tag/v1.2.1

@XingMingYue
Copy link
Author

你好,我没有找到mutableDataSaverState 我发现了 mutableDataSaverStateOf 并尝试按照我的理解尝试更改之前的Demo,然后我失败了,再次运行的时候没有加载之前保存的数据。
我尝试调试发现虽然我注册了List<Entity>的转换器,但是他并没有使用自定义的转换器,最终使用的是registerDefaultTypeConverters中注册的第一个默认转换器,一下是我在启动之初直接删除掉第一个默认的转换器,他现在正常,但我觉得这不是正常的写法

import AppConfig.dataSaver
import androidx.compose.desktop.ui.tooling.preview.Preview
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.lazy.LazyColumn
import androidx.compose.foundation.lazy.items
import androidx.compose.material3.Button
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.runtime.CompositionLocalProvider
import androidx.compose.ui.window.Window
import androidx.compose.ui.window.application
import cn.hutool.json.JSONUtil
import com.funny.data_saver.core.DataSaverConverter
import com.funny.data_saver.core.DataSaverInterface
import com.funny.data_saver.core.DataSaverProperties
import com.funny.data_saver.core.LocalDataSaver
import com.funny.data_saver.core.mutableDataSaverStateOf

// 实体
data class Entity(var name: String = "", var remark: String = "")

// 配置
object AppConfig {
    private const val FILE_NAME = "config.properties"
    private val userHome = System.getProperty("user.home")
    private const val PROJECT_NAME = "test-demo"
    val dataSaver: DataSaverInterface = DataSaverProperties("$userHome/$PROJECT_NAME/$FILE_NAME")
}

@Composable
@Preview
fun App() {
    var list: List<Entity> by mutableDataSaverStateOf<List<Entity>>(
        dataSaverInterface = dataSaver,
        key = "main.entity.list",
        initialValue = emptyList<Entity>()
    )
    MaterialTheme {
        Column {
            Button(onClick = {
                list += Entity("new entity ${list.size + 1}", "remark")
            }) {
                Text("add new entity")
            }
            LazyColumn {
                items(list) {
                    Text(it.name)
                }
            }
        }
    }
}

fun main() = application {
    // 删除第一个
    DataSaverConverter.typeConverters.removeAt(0)

    // 添加自定义转换
    DataSaverConverter.registerTypeConverters<Entity>(
        save = { bean -> JSONUtil.toJsonStr(bean) },
        restore = { str ->
            JSONUtil.toBean(str, Entity::class.java)
        }
    )
    DataSaverConverter.registerTypeConverters<List<Entity>>(
        save = { bean -> JSONUtil.toJsonStr(bean) },
        restore = { str ->
            JSONUtil.toList<Entity>(str, Entity::class.java)
        }
    )
    Window(onCloseRequest = ::exitApplication) {
        CompositionLocalProvider(LocalDataSaver provides dataSaver) {
            App()
        }
    }
}

@FunnySaltyFish
Copy link
Owner

抱歉让您折腾这么久,确实没有 mutableDataSaverState ,我少打了 Of;另外这确实不是预期行为,理论上应该是 List<Entity> 的转换器工作。我再看看

@FunnySaltyFish
Copy link
Owner

代码写的确实有问题,由于数据在读取之前不知道转换后具体是什么,这个 findRestorer 传入的参数 datainitialValue,判定后进入到针对 emptList 的转换器中了。我将删除画蛇添足的、对于 emptyList/emtptyMap 的两个默认转换器。 新版将很快发布,伴随对其他平台的进一步支持。

此外,顺带一提,由于您的 list 变量是用在 Composable 中的,可以使用 rememberDataSaverState() 方法,其中的 dataSaver 将由 CompositionLocal 隐式传入,类似于:

fun App() {

    CompositionLocalProvider(LocalDataSaver provides AppConfig.dataSaver) {
        MaterialTheme {
            var list: List<Entity> by rememberDataSaverState<List<Entity>>(
                key = "main.entity.list",
                initialValue = emptyList()
            )
            Column {
                Button(onClick = {
                    list += Entity("new entity ${list.size + 1}", "remark")
                }) {
                    Text("add new entity")
                }
                LazyColumn {
                    items(list) {
                        Text(it.name)
                    }
                }
            }
        }
    }
}

@XingMingYue
Copy link
Author

😄我看到了,但是对于mutableDataSaverStateOf方法你需要再处理下才可以,因为dataSaverInterface是必传的

inline fun <reified T> mutableDataSaverStateOf(
    dataSaverInterface: DataSaverInterface,
    key: String,
    initialValue: T,
    savePolicy: SavePolicy = SavePolicy.IMMEDIATELY,
    async: Boolean = true,
    coroutineScope: CoroutineScope? = null
): DataSaverMutableState<T> {
    val data = try {
        if (!dataSaverInterface.contains(key)) initialValue
        else dataSaverInterface.readData(key, initialValue)
    } catch (e: Exception) {
        val restore = DataSaverConverter.findRestorer<T>(initialValue)
        restore ?: throw e
        runCatching {
            restore(dataSaverInterface.readData(key, "")) as T
        }.onFailure {
            DataSaverLogger.e("error while restoring data(key=$key), set to default. StackTrace:\n${it.stackTraceToString()}")
        }.getOrDefault(initialValue)
    }
    return DataSaverMutableState(dataSaverInterface, key, data, savePolicy, async, coroutineScope)
}

@FunnySaltyFish
Copy link
Owner

嗯嗯,这个方法是必传的,因为没有 @composable 修饰(可以用在 ViewModel 之中或其他全局单例),没法从 CompositionLocal 中获取到当前 DataSaverInterface 实例。我刚刚的回复是在 Composable 中,可以用 rememberDataSaverState,那个默认从 CompositionLocal 中获取。

@XingMingYue
Copy link
Author

懂你的意思,非常感谢。(^▽^)

@XingMingYue
Copy link
Author

XingMingYue commented Jul 26, 2024

哈喽,我又来了
使用默认DataSaverPropertiesrememberDataSaverState 设置 senseExternalDataChange=true
提示
ComposeDataSaver: to enable senseExternalDataChange, you should set senseExternalDataChange to true in DataSaverInterface 但是默认提供的DataSaverProperties并没有senseExternalDataChange参数

    var list: MutableList<Entity> by rememberDataSaverState<MutableList<Entity>>(
        key = "main.entity.list",
        initialValue = mutableListOf<Entity>(),
        senseExternalDataChange = true
    )

@FunnySaltyFish
Copy link
Owner

Sorry这两天有点别的事,回的晚了些。Properties 确实不支持感知外部数据变化,它自己没这个能力。要想支持,除非我手动在 saveData 时提交变更

@XingMingYue
Copy link
Author

好的,有没有考虑过对mutableListOf进行支持。目前看添加后页面没有刷新(迷),数据没有保存

    var list: MutableList<Entity> by rememberDataSaverState<MutableList<Entity>>(
        key = "main.entity.list",
        initialValue = mutableListOf<Entity>()
    )
    list.add(Entity("new entity ${list.size + 1}", "remark"))

@FunnySaltyFish
Copy link
Owner

很难支持,因为我没有办法监听到 mutableList 内部的更改,比如 .add/.remove 这些方法。这也是之前的 DataSaverListState 诞生的原本目的,但后来发现除非我自己实现 SnapshotStateList,否则没法做到。你只能重新修改 list 这个对象为其他引用,才能触发保存。

var list: MutableList<Entity> by rememberDataSaverState<MutableList<Entity>>(
    key = "main.entity.list",
    initialValue = mutableListOf<Entity>()
)
list = mutableListOf<Entity>().apply { 
    addAll(list)
    add(Entity("new entity ${list.size + 1}", "remark"))
}

或者直接用 List,然后 += 运算符,就像示例那样。这内部也会创建一个新的 List

public operator fun <T> Collection<T>.plus(element: T): List<T> {
    val result = ArrayList<T>(size + 1)
    result.addAll(this)
    result.add(element)
    return result
}

var list: List<Entity> by rememberDataSaverState(
    key = "main.entity.list",
    initialValue = emptyList<Entity>()
)
list += Entity("new entity ${list.size + 1}", "remark")

@XingMingYue
Copy link
Author

好的,大概什么时候发布新版本呐?

@FunnySaltyFish
Copy link
Owner

好的,大概什么时候发布新版本呐?

抱歉才回复,这两天事情有点多。二是我发现了个小问题,如果注册了多个 List<xxx>,比如 List<Student>, List<Animal> 的类型转换器,然后两个的初始值又都是 emptyList,那么被选到的转换器就不对(目前的代码是,总是第一个注册的,也就是 List<Student>),进而导致反序列化出问题,我也在想该怎么解决这种问题。

我会尽量找时间维护的

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

2 participants