Share common properties between Kotlin classes - kotlin

I have 2 (data) classes that almost share the same properties:
data class Foo(
val id: FooId,
val name: String,
... 10+ properties
}
data class NewFoo(
val name: String,
... 10+ properties
}
I just want some syntax sugar magic here: not to repeat 10+ properties. I can make a base sealed class, but you would end up writing even more text (for passing arguments to base class ctor), although you are safer from making a mistake.
Yes, I know I could use composition for this, but here I don't want to, as there might be different 'variants' of the same data.
Am I missing something or this is not possible in Kotlin?

You can use an abstract (or sealed) class with abstract params instead and override them in the constructor of your data class (i.e. without additional passing them into the constructor of the base class).
abstract class Base {
// put commons parameter here
// abstract param needs to be initialized in the constructor of data class
abstract val name: String
// you can define some not-abstract params as well
open lateinit var someOtherParam: String
}
data class Foo1(
override val name: String,
val id: Int,
val someAdditionalParam1: String
) : Base()
data class Foo2(
override val name: String,
val someAdditionalParam2: String,
override var someOtherParam: String
) : Base()

Related

Why need the author to add override before val in a data class in order to instance an interface in Kotlin?

The Code A is from the official sample project.
I find override is added before val route in data class TopLevelDestination, but it's hard to understand to instance the interface NiaNavigationDestination in this way.
Is there other way to instance the interface NiaNavigationDestination ?
Code A
data class TopLevelDestination(
override val route: String,
override val destination: String,
val selectedIcon: Icon,
val unselectedIcon: Icon,
val iconTextId: Int
) : NiaNavigationDestination
interface NiaNavigationDestination {
val route: String
val destination: String
}
You can override the vals from the interface in the body of the class, but then they will not participate in equals(), hashcode(), or copy(). A data class only uses its constructor parameters for those functions.
Example:
data class TopLevelDestination(
val selectedIcon: Icon,
val unselectedIcon: Icon,
val iconTextId: Int
) : NiaNavigationDestination {
override val route: String = "something"
override val destination: String = "something else"
}
In this case, it does not seem productive for something like "destination" to not be a part of equals() for a class that has "destination" in it's name!
It also becomes impossible to set these values to anything but a constant or something that depends on the other constructor properties.

Kotlin function for data class

What is the best way in Kotlin to write a function that can be used by a data class?
For example, say I have a data class and I need a function where the result is based on the value of a field from that data class:
data class Person(
val dateOfBirth: String
)
How would I go about writing an 'age' function for the Person object?
The same way you would write it for a non-data class!
You could add a method within the class:
data class Person(val dateOfBirth: String) {
fun age() = // …
}
Or you could add an extension method outside it:
data class Person(val dateOfBirth: String)
fun Person.age() = //…
(A method within the class is usually a better option if you can modify the class, and it belongs conceptually to the class. An extension method is useful if you don't have access to the class, or if it's specific to some particular usage or has a dependency on something unrelated to the class.)
Of course, you can always write a simple, old-style function:
fun calculateAge(person: Person) = // …
…but an extension method is clearer, reads better, and your IDE will suggest it.
In this case (where the age is quick to calculate, doesn't change the object's visible state, and won't throw an exception), a property or extension property might be more natural:
data class Person(val dateOfBirth: String) {
val age get() = // …
}
Or:
data class Person(val dateOfBirth: String)
val Person.age get() = //…
Then you can access it simply as myPerson.age.
The same way you would for any other class:
data class Person(val dateOfBirth: String) {
fun age(): Int {
// Use dateOfBirth here to compute the age.
}
}

can I use kotlinx serializer with multiple sealed class levels as parents and a nested invocation?

I am trying to use kotlinx #Serializable and Ive faced this issue:
I have the following classes:
#Serializable
sealed class GrandParent
a second one:
#Serializable
sealed class Parent() : GrandParent() {
abstract val id: String
}
and a third one
#Serializable
data class Child(
override val id: String, ....
): Parent()
I'm needing of grandparent since I use it as a generic type in another class, which happen to also have a reference to the GrandParent class
#Serializable
data class MyContent(
override val id: String,
....
val data: GrandParent, <- so it has a self reference to hold nested levels
...): Parent()
Every time I try to run this I get an error...
Class 'MyContent' is not registered for polymorphic serialization in the scope of 'GrandParent'.
Mark the base class as 'sealed' or register the serializer explicitly.
I am using ktor as wrapper, kotlin 1.5.10. I did this based on https://github.com/Kotlin/kotlinx.serialization/blob/master/docs/polymorphism.md#registered-subclasses
Any ideas?
You should serialize and deserialize using your sealed class in order for kotlin serialization to "know" to add a discriminator with the right implementation. By default it search for type in the json but you can change it with JsonBuilder:
Json {
classDiscriminator = "class"
}
Here is an example:
#Serializable
sealed class GrandParent
#Serializable
sealed class Parent : GrandParent() {
abstract val id: String,
}
#Serializable
data class Child(
override val id: String,
): Parent()
#Serializable
data class MyContent(
override val id: String,
val data: GrandParent,
): Parent()
fun main() {
val test = MyContent(id = "test", data = Child(id = "child"))
val jsonStr = Json.encodeToString(GrandParent.serializer(), test)
println("Json string: $jsonStr")
val decoded = Json.decodeFromString(GrandParent.serializer(), jsonStr)
println("Decoded object: $decoded")
}
Result in console:
Json string: {"type":"MyContent","id":"test","data":{"type":"Child","id":"child"}}
Decoded object: MyContent(id=test, data=Child(id=child))
encode and decode can also be written like this (but behind the scenes it will use reflections):
val jsonStr = Json.encodeToString<GrandParent>(test)
println("Json string: $jsonStr")
val decoded = Json.decodeFromString<GrandParent>(jsonStr)
println("Decoded object: $decoded")

Is there any way to transform the value of a property at data class construction time?

When creating a data class I frequently find that I want to transform one of the properties, usually to normalize it or to make a defensive copy. For example, here I want productCode to always be lowercase:
data class Product(val productCode: String)
I've tried adding an init block, in the hopes that Kotlin would be smart enough to let me manually deal with the assignment of the constructor parameter to the property:
data class Product(val productCode: String) {
init {
this.productCode = productCode.toLowerCase()
}
}
but it treats this as a reassignment.
I'd rather not have to write equals/hashCode/toString/copy by hand and IDE generated methods aren't really much better.
Is there any way to transform constructor parameters in a data class?
No. For equality and toString to work, the properties need to be in the primary constructor.
What you can do however, is create a factory method:
data class Product private constructor(val productCode: String) {
companion object Factory {
fun create(productCode: String) : Product {
return Product(productCode.toLowerCase())
}
}
}
By making the constructor private you force usage of this create method.
If you want to get 'hacky', you can pretend you're still calling the constructor, by renaming create to invoke and making it an operator function:
data class Product private constructor(val productCode: String) {
companion object {
operator fun invoke(productCode: String): Product {
return Product(productCode.toLowerCase())
}
}
}
Calling Product("foo") will call the invoke method.
Note: the constructor is still exposed through the copy method, see https://youtrack.jetbrains.com/issue/KT-11914
What about
sealed class Product {
abstract val productCode: String
private data class Product(override val productCode: String) : your.package.Product()
companion object {
operator fun invoke(productCode: String): your.package.Product =
Product(productCode.toLowerCase())
}
}
All the advantages of data class without exposing copy. A negative is having to repeat property names an extra time.

Define common properties without inheritance

Is there a way of defining common properties without using inheritance in Kotlin?
For example
If I have two classes that both require an "id" property.
class Dog() {
var id: UUID?
}
class Cat() {
var id: UUID?
}
The general JAVA way to solve this is introduce a super class
class Animal() {
var id: UUID?
}
class Dog: Animal()
class Cat: Animal()
But now "Dog" and "Cat" are of type "Animal". What if I introduce a "Chair" class that also requires a unique identifier.
Essentially what I want to the ability to create a set of properties I can include in a number of different classes for programming convenience only. I don't want all the problems associated with inheritance.
You can, of course, use an interface instead of a base class:
interface HasId {
val id: UUID
}
data class Dog(override val id: UUID) : HasId
data class Cat(override val id: UUID) : HasId
However, the above is still using inheritance. If you have more common properties that would be used in multiple classes it may be a sign that they should be grouped together to form a separate value object e.g.
data class Address(val city: String, val street: String, val country: String)
class Person(val name: String, val address: Address)
class School(val name: String, val address: Address, val studentsCount: Int)
And if you want to treat Person and School uniformly with regards to address property you can still use the interface to denote the common attribute:
interface HasAddress {
val address: Address
}
class Person(val name: String,
override val address: Address) : HasAddress
class School(val name: String,
override val address: Address,
val studentsCount: Int) : HasAddress
It might be possible that delegation will suit your needs:
interface WithId {
var id: Int
}
class IdStorage : WithId {
override var id: Int = 0
}
class Dog(withId: WithId) : WithId by withId {
constructor() : this(IdStorage()) {}
}
class Cat(withId: WithId) : WithId by withId {
constructor() : this(IdStorage()) {}
}
This code is rather verbose, but what it allows you to do is:
Avoid using superclass just for the sake of having id property, which allows you to extend other classes if you need
Usage of interface, which guarantees other pieces of code that your class has id
Allows to move implementation of your properties (or functions) to separate class, hence no need for duplicate code in case of complex property/function implementation
Allows implementing multiple properties/functions in a separate class
As was mentioned in the comments:
interface Animal {
var id: UUID?
}
class Dog: Animal
class Cat: Animal