Categories
Architecture Kotlin

Kotlin Hydra constructors

After 4 years of Kotlin experience, one of the confusing parts still are the constructors. It appears like every time they seem figured out, you still find yourself googling for a solution in the next project.

Compared to Java’s relatively straightforward definitions, Kotlin constructors are like Hydra’s heads. After understanding one concept about them, another 2 will pop up to confuse you more.

When looking at the bigger picture, there are 2 types of constructors. Primary and Secondary.

Primary constructor

Primary constructor can be used as a compact way to describe the class’s properties.

class WebService(url: String, apiVersion: String)

If the class has no logic, the brackets { } can be omitted for fewer lines of code.

Val and not val

When using the val keyword, the property converts into a field and can be used throughout the class’s instance.

class WebService(val url: String, apiVersion: String) {
  fun getUsers() {
    // the primary constructor's val is available throughout the class
    val requestUrl = "$url/users"
}

In this case, the field is written only in the primary constructor.

If the input is not a val, it can only be used in the initialisation phase of the class. Either inline in the field definition, or in the init { } block

// apiVersion is not a `val` property
class WebService(val url: String, apiVersion: String) {
  val completeUrl = "$url/$apiVersion"

  init {
      println("Initialised with api version: $apiVersion")
  }

  fun getUsers() {
    // ERROR: apiVersion is not a val and cannot be used in functions
    val requestUrl = "$url/$apiVersion/users"
  }

In this case, there are fields both in the primary constructor and in the class content.

source

Default value

A default value can be set to any of the constructor fields, which will be used if there is no input

class WebService(val url: String, apiVersion: String = "2")

// initialise WebService with default apiVersion: 2
val webService = WebService("<https://tonisives.com>")
// --- output
> Initialised with api version: 2

source

Secondary constructors

Any number of secondary constructors can be defined. They have to call the primary constructor explicitly if there are any properties. If there are none, the primary constructor, field initialisers and init {} blocks will be called automatically before the secondary constructor.

For our WebService use case, a secondary constructor could be written to manage the WebService URLs.

constructor(environment: Environment) : this(getUrl(environment))

enum class Environment {
    PROD, TEST
}

companion object {
    private fun getUrl(environment: Environment): String {
        return when (environment) {
            Environment.PROD -> "<https://tonisives.com>"
            Environment.TEST -> "<https://tonisives.com/test>"
        }
    }
}

This way the WebService can be initialised with an enum value describing the web server’s environment. The global getUrl() method will be used to retrieve the URL for the given environment.

WebService(WebService.Environment.TEST)
// --- output
> url: <https://tonisives.com/test/2>

source

Secondary constructor could also be used to input an optional field, like a custom Logger.

var logger: Kermit? = null

constructor(environment: Environment, logger: Kermit) : this(getUrl(environment)) {
    this.logger = logger
}

init {
  logger?.d { "hello from logger" }
}

What would be the output when using this constructor? One would think a log from the Kermit logger. However, since all init blocks are ran before secondary constructors, the output is

// --- output
> 

empty.

This means the custom logger call needs to be added to the secondary constructor, not to the init { } block.

constructor(environment: Environment, logger: Kermit) : this(getUrl(environment)) {
    this.logger = logger
    logger?.d { "hello from logger" }
}

// --- output
> Debug: (Kermit) hello from logger

source

Constructor call order

The constructors are called in the following order:

With this in mind, the code needs to be carefully validated. The following:

open class Parent {
    init {
        println("0")
    }
}

class Child : Parent {
    val abc = "1".also(::println)

    constructor() : super() { // secondary constructor will print the last
        println("3")
    }

    init {
        println("2")
    }
}

will print numbers from 0 to 3. Not in the order they are written.

Inheritance 👶🏽

Important part about constructors is how they act in the case of inheritance.

‘super’ is not an expression

When calling super() in a secondary constructor, the compiler fails with an error.

open class Parent

class Child : Parent {
    constructor(input:Int) {
	// ERROR: 'super' is not an expression, it can only be used on the 
	//  left-hand side of a dot ('.')
        super()
	println("Child class $input initialised")
    }
}

Calling a super constructor would be an obvious step in Java. However, in Kotlin, the call to super needs to be in the header itself.

constructor(input:Int) : super()

After writing this, the code turns out to be superfluous. It should be converted into a primary constructor and an init { } block instead.

class Child(input: Int) : Parent() {
    init {
        println("Child class $input initialised")
    }
}

This is one demonstration of the consequences of cutting off a Hydra’s head. New constructor concepts come to light immediately.

source

Parent with properties

If the parent’s primary constructor has properties, they have to be satisfied when calling super in the child class

open class Parent(input: Int)

class Child : Parent {
    // ERROR: No value passed for parameter 'input'
    constructor() : super()
}

Here, secondary constructor doesn’t satisfy the parent’s header. It can be resolved by either adding input to the super() call:

constructor() : super(1)

Or, by adding a new primary and secondary constructors to the Parent,

// () defines a primary constructor without parameters
open class Parent() {
    constructor(input:Int) : this()
}

class Child : Parent {
    constructor() : super() // works

which will generate a hidden primary constructor without parameters. And the code will compile again.

Parent with properties is one example where a clearer distinction between primary and secondary constructors in could have been helpful. It can take considerable effort to decipher the hidden, secondary and inherited constructors.

Conclusion

Besides the Kotlin’s simplistic data class single line headers lie nested concepts about primary and secondary constructors. With their non-linear call order and compact thus hard to read writing style, one can find them puzzling at times.

To not feel discouraged, reading through the official documentation and trying out the examples by oneself can be recommended.

Leave a Reply