【问题标题】:Implementing observable properties that can also serialize in Kotlin实现也可以在 Kotlin 中序列化的可观察属性
【发布时间】:2022-01-02 09:44:21
【问题描述】:

我正在尝试构建一个类,其中某些值是可观察但也可序列化的。

这显然有效并且序列化有效,但是必须为每个字段添加一个 setter 并且必须在每个 setter 中手动调用 change(...),这非常繁琐:

interface Observable {

    fun change(message: String) {
        println("changing $message")
    }
}

@Serializable
class BlahVO : Observable {

    var value2: String = ""
        set(value) {
            field = value
            change("value2")
        }

    fun toJson(): String {
        return Json.encodeToString(serializer(), this)
    }
}

println(BlahVO().apply { value2 = "test2" }) 正确输出

changing value2
{"value2":"test2"}

我尝试过介绍代表:

interface Observable {

    fun change(message: String) {
        println("changing $message")
    }

    
    @Suppress("ClassName")
    class default<T>(defaultValue: T) {

        private var value: T = defaultValue

        operator fun getValue(observable: Observable, property: KProperty<*>): T {
            return value
        }

        operator fun setValue(observable: Observable, property: KProperty<*>, value: T) {
            this.value = value
            observable.change(property.name)
        }

    }

}

@Serializable
class BlahVO : Observable {

    var value1: String by Observable.default("value1")

    fun toJson(): String {
        return Json.encodeToString(serializer(), this)
    }
}

println(BlahVO().apply { value1 = "test1" }) 正确触发更改检测,但它不序列化:

changing value1
{}

如果我从 Observable 转到 ReadWriteProperty,

interface Observable {

    fun change(message: String) {
        println("changing $message")
    }

    fun <T> look(defaultValue: T): ReadWriteProperty<Observable, T> {
        return OP(defaultValue, this)
    }

    class OP<T>(defaultValue: T, val observable: Observable) : ObservableProperty<T>(defaultValue) {
        override fun setValue(thisRef: Any?, property: KProperty<*>, value: T) {
            super.setValue(thisRef, property, value)
            observable.change("blah!")
        }
    }
}

@Serializable
class BlahVO : Observable {

    var value3: String by this.look("value3")

    fun toJson(): String {
        return Json.encodeToString(serializer(), this)
    }
}

结果是一样的:

changing blah!
{}

Delegates.vetoable 也是如此

var value4: String by Delegates.vetoable("value4", {
        property: KProperty<*>, oldstring: String, newString: String ->
    this.change(property.name)
    true
})

输出:

changing value4
{}

代表似乎不适用于 Kotlin 序列化

还有哪些其他选项可以在不破坏其序列化的情况下观察属性的变化,而这些选项也适用于其他平台(KotlinJS、KotlinJVM、Android...)?

【问题讨论】:

  • 您的一个示例是使用 Delegates.vetoable,但您是否尝试过 Delegates.observable?
  • vetoable 和 observable 做同样的事情,我已经对其进行了测试以确认。 Vetoable 只是可观察的,返回 true / false 以查看它是否可以应用更改。此外,我添加的 ReadWriteProperty 示例实际上是一个 Observable。如果我想使用 Delegates,我需要对 Serializer 进行更改

标签: kotlin serialization kotlin-multiplatform kotlin-delegate


【解决方案1】:

截至目前,kotlinx.serialization 不支持 Kotlin 委托的序列化和反序列化。
GitHub 上有一个 open issue #1578 关于此功能。

根据问题,您可以创建一个中间数据传输对象,该对象被序列化而不是原始对象。您还可以编写一个自定义序列化程序来支持 Kotlin Delegates 的序列化,这似乎是更多样板,然后编写自定义 getter 和 setter,如问题中所建议的那样。


数据传输对象

通过将您的原始对象映射到一个没有委托的简单数据传输对象,您可以利用默认的序列化机制。 这还具有很好的副作用,可以从特定于框架的注释中清除数据模型类,例如 @Serializable

class DataModel {
    var observedProperty: String by Delegates.observable("initial") { property, before, after ->
        println("""Hey, I changed "${property.name}" from "$before" to "$after"!""")
    }

    fun toJson(): String {
        return Json.encodeToString(serializer(), this.toDto())
    }
}

fun DataModel.toDto() = DataTransferObject(observedProperty)

@Serializable
class DataTransferObject(val observedProperty: String)

fun main() {
    val data = DataModel()
    println(data.toJson())
    data.observedProperty = "changed"
    println(data.toJson())
}

这会产生以下结果:

{"observedProperty":"initial"}
Hey, I changed "observedProperty" from "initial" to "changed"!
{"observedProperty":"changed"}

自定义数据类型

如果更改数据类型是一种选择,您可以编写一个透明地进行(反)序列化的包装类。以下内容可能会起作用。

@Serializable
class ClassWithMonitoredString(val monitoredProperty: MonitoredString) {
    fun toJson(): String {
        return Json.encodeToString(serializer(), this)
    }
}

fun main() {
    val monitoredString = obs("obsDefault") { before, after ->
        println("""I changed from "$before" to "$after"!""")
    }
    
    val data = ClassWithMonitoredString(monitoredString)
    println(data.toJson())
    data.monitoredProperty.value = "obsChanged"
    println(data.toJson())
}

产生以下结果:

{"monitoredProperty":"obsDefault"}
I changed from "obsDefault" to "obsChanged"!
{"monitoredProperty":"obsChanged"}

但是,您会丢失有关更改了哪个属性的信息,因为您无法轻松访问字段名称。此外,如上所述,您还必须更改数据结构,这可能是不可取的,甚至是不可能的。此外,目前这仅适用于字符串,尽管可能会使它更通用。 此外,这需要大量的样板文件开始。然而,在调用站点上,您只需将实际值包装在对obs 的调用中。 我使用下面的样板来让它工作。

typealias OnChange = (before: String, after: String) -> Unit

@Serializable(with = MonitoredStringSerializer::class)
class MonitoredString(initialValue: String, var onChange: OnChange?) {
    var value: String = initialValue
        set(value) {
            onChange?.invoke(field, value)

            field = value
        }

}

fun obs(value: String, onChange: OnChange? = null) = MonitoredString(value, onChange)

object MonitoredStringSerializer : KSerializer<MonitoredString> {
    override val descriptor: SerialDescriptor = PrimitiveSerialDescriptor("MonitoredString", PrimitiveKind.STRING)

    override fun serialize(encoder: Encoder, value: MonitoredString) {
        encoder.encodeString(value.value)
    }

    override fun deserialize(decoder: Decoder): MonitoredString {
        return MonitoredString(decoder.decodeString(), null)
    }
}

【讨论】:

  • 我目前采用了类似的方法,但感觉可能会更好。我更进一步创建了一个返回 MonitoredString 的方法受监控的字符串,并且由于该函数可以访问它,因此我不必传递 onChange,我可以将其链接到 OnChange 从中。拥有一个可观察的“状态”类和一个可以序列化的数据传输类的缺点是模型字段的重复。似乎实现我想要做的唯一好的解决方案是使用@Something 进行注释,然后使用 KSP 生成样板。
猜你喜欢
  • 2023-04-02
  • 1970-01-01
  • 1970-01-01
  • 2017-09-07
  • 1970-01-01
  • 1970-01-01
  • 2017-05-07
  • 2013-04-26
  • 2015-11-30
相关资源
最近更新 更多