A implementação de propriedades observáveis que também pode serializar em Kotlin

0

Pergunta

Eu estou tentando criar uma classe em que certos valores são Observáveis, mas também Serializable.

Obviamente, isso funciona e a serialização funciona, mas é muito clichê-pesados, tendo para adicionar um setter para cada campo único e manualmente ter que chamar change(...) dentro de cada um setter:

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" }) corretamente saídas

changing value2
{"value2":"test2"}

Eu tentei introdução de Delegados:

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" }) corretamente disparadores de detecção de mudanças, mas não serializar:

changing value1
{}

Se eu for de Observáveis para 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)
    }
}

o resultado é o mesmo:

changing blah!
{}

Da mesma forma para os Delegados.vetoable

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

saídas:

changing value4
{}

Delegados só não parece trabalhar com Kotlin Serialização

Que outras opções existem para observar uma propriedade alterações sem quebrar a sua serialização que também irá funcionar em outras plataformas (KotlinJS, KotlinJVM, Android, ...)?

1

Melhor resposta

2

A serialização e Desserialização de Kotlin Delegados não é suportado pelo kotlinx.serialization a partir de agora.
Não é uma questão aberta #1578 no GitHub sobre este recurso.

De acordo com o problema, você pode criar um intermediário na transferência de dados objeto, que é serializado em vez do objeto original. Você também poderia escrever um serializador personalizado suporte a serialização de Kotlin Delegados, que parece ser ainda mais clichê, então, escrever personalizado getters e setters, como proposto na questão.


Objeto De Transferência De Dados

O mapeamento de seu objeto original para um simples objeto de transferência de dados sem delegados, você pode utilizar o padrão de serialização de mecanismos. Isto também tem o lado bom efeito para limpar o seu modelo de dados de classes do framework específico anotações, como @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())
}

Isso produz o seguinte resultado:

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

Tipo de dados personalizado

Se alterar o tipo de dados é uma opção, você poderia escrever um embrulho de classe que é (de)serializados de forma transparente. Algo ao longo das linhas do seguinte poderia funcionar.

@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())
}

O que produz o seguinte resultado:

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

Você no entanto perder informações sobre a propriedade mudou, como você não tem acesso fácil ao nome do campo. Você também tem que alterar suas estruturas de dados, como mencionado acima, e pode não ser desejável ou mesmo possível. Além disso, este trabalho apenas para Cadeias de caracteres, por agora, ainda que se possa torná-lo mais genérico embora. Também, isso requer um monte de clichê para começar. No site de chamada no entanto, você só precisa quebrar o real valor de uma chamada para obs. Eu usei o seguinte clichê para fazê-lo funcionar.

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)
    }
}
2021-11-24 18:19:41

Eu atualmente seguir uma abordagem semelhante, mas parece que ele poderia ser melhor. Eu tenho ido um passo a mais para a criação de um método monitoredString que retorna um MonitoredString e desde que a função tem acesso a isso, eu não tenho que passar o onChange, eu posso apenas um link para o OnChange a partir deste. A desvantagem de ter um Observável "estado" de classe e, em seguida, uma transferência de dados de classe que pode ser serializado é a duplicação do modelo de campos. Parece ser a única solução boa que alcança o que eu quero fazer, é para anotar com @Algo e, em seguida, gerar o clichê usando KSP.
Jan Vladimir Mostert

Em outros idiomas

Esta página está em outros idiomas

Русский
..................................................................................................................
Italiano
..................................................................................................................
Polski
..................................................................................................................
Română
..................................................................................................................
한국어
..................................................................................................................
हिन्दी
..................................................................................................................
Français
..................................................................................................................
Türk
..................................................................................................................
Česk
..................................................................................................................
ไทย
..................................................................................................................
中文
..................................................................................................................
Español
..................................................................................................................
Slovenský
..................................................................................................................