Home > Mobile >  how to properly overload kotlin data class with constructors
how to properly overload kotlin data class with constructors

Time:11-09

I am trying to create a nested data class in kotlin which contain several other data classes. Then access the main data class to get the objects of the nested data class.

Person data class is going to be the standard, but the other 3 data classes are the overloaded ones (not sure if i'm using the term overload correctly here?).

Note: the ide i'm using is intellij

What I mean:

data class User(val person: Person, val transport: Transport)
{
    constructor(person: Person, activity: Activity): this(person, activity)  //ERROR: There's a cycle in the delegation calls chain
    constructor(person: Person, absent: Absent): this(person, absent)  //ERROR: There's a cycle in the delegation calls chain
}

data class Activity(val game: String, val funLevel: Int)
data class Absent(val isSick: Boolean, val funLevel: Int)
data class Transport(val typeOfTransport: String)
data class Person(val name: String, val age: Int)

fun main(){
    val x = User(Person("foo", 5), Activity("basketball", 10))
    val y = User(Person("bar", 6), Absent(true, 0))
    val z = User(Person("loo", 6), Transport("Bus"))
    
    // Ultimately want to access the data objects like this
    println(x.person.name)
    println(x.activity.game)  // not working
    
}

Ultimately, I am trying to be able to call the function easily based like this: enter image description here

Any ideas?

CodePudding user response:

data class User(val person: Person, val transport: Transport)

That bit in the parentheses there is the primary constructor - in fact that's shorthand, you can also write the class declaration like this:

data class User constructor(val person: Person, val transport: Transport)

When you have a primary constructor, any secondary constructors have to call through to the primary one.

// need to somehow create a Transport to call the primary with
constructor(person: Person, activity: Activity): this(person, someTransport)

// can't do this - the primary constructor doesn't take (Person, Activity)
constructor(person: Person, activity: Activity): this(person, activity)

That's because to actually create an instance of a class, you need to call its main constructor at some point - the secondaries can just do other stuff as well, but they need to actually instantiate the object.


You can omit the primary constructor (so the class can be instantiated with no arguments) and then your secondary constructors can do whatever they want:

class User {
    constructor(person: Person, activity: Activity) {
        // initialisation stuff
    }
    constructor(person: Person, absent: Absent) {
        // other initialisation stuff
    }
}

but at the end of the day it's still calling that no-args constructor to actually create the User - it's up to you to do something with those different arguments passed into the different constructors, and create the same type of object no matter which is called.

Do you have all the possible properties, and just leave them null if no value was provided? Do you have a special set of classes representing the different combos of data for each constructor, and assign an instance of one of those to a userData property? You need to work out how to have a single class that can be instantiated with different combinations of data.


Data classes are special, and require a primary constructor. That's actually what defines the data in the class, and all its handy overridden and generated methods (like equals, hashCode, copy) all work with those properties defined in the primary constructor.

The flipside of that is any property not defined in or derived from the primary constructor's parameters is not part of its "data". If you copy a data class, only the properties in the primary constructor are copied. It doesn't know anything about the rest of the class - so if you're relying on any of the data class features, be aware that other properties are not included by default.

So with that in mind, a typical data class approach would be to have all your data in the primary constructor:

data class User(
    val person: Person,
    val transport: Transport? = null,
    val activity: Activity? = null,
    val absent: Absent? = null
) {
    constructor(person: Person, activity: Activity): this(person, null, activity, null)
    constructor(person: Person, absent: Absent): this(person, null, null, absent)
}

That way every secondary constructor calls the main one, and all your data is defined and contained in the primary constructor. Some things just might be missing!


This is a bit awkward though:

this(person, null, activity, null)

We have named arguments, so you could try this:

constructor(person: Person, activity: Activity): this(person, activity = activity)

But that will actually call the same secondary constructor again, because its signature (takes a Person and an Activity) matches the call you're making. (That's why you're getting the cyclic error.) But if we're doing things this way, with default arguments, you can avoid the secondary constructors completely:

data class User(
    val person: Person,
    val transport: Transport? = null,
    val activity: Activity? = null,
    val absent: Absent? = null
)

// create an instance
User(somePerson, absent = someAbsent)

But this way limits your ability to restrict it to certain combinations, like a Person and one of Transport/Activity/Absent. That's a problem with data classes in general - you can make the primary constructor private and force users to go through secondary constructors or other functions that generate an instance, but the copy function allows people to mess with that data however they like.

In this case, you probably want a sealed class like IR42 mentions in the comments - a type which allows for a defined set of completely different subclasses. I just wanted to give an overview of how this all works and why maybe you'd want to try a different approach

  • Related