Hi I am a newbie in the Kotlin world. I like what I see so far and started to think to convert some of our libraries we use in our application from Java to Kotlin.
T
I would say the pattern and implementation stays pretty much the same in Kotlin. You can sometimes skip it thanks to default values, but for more complicated object creation, builders are still a useful tool that can't be omitted.
I was working on a Kotlin project that exposed an API consumed by Java clients (which can't take advantage of the Kotlin language constructs). We had to add builders to make them usable in Java, so I created an @Builder annotation: https://github.com/ThinkingLogic/kotlin-builder-annotation - it's basically a replacement for the Lombok @Builder annotation for Kotlin.
First and foremost, in most cases you don't need to use builders in Kotlin because we have default and named arguments. This enables you to write
class Car(val model: String? = null, val year: Int = 0)
and use it like so:
val car = Car(model = "X")
If you absolutely want to use builders, here's how you could do it:
Making the Builder a companion object
doesn't make sense because object
s are singletons. Instead declare it as an nested class (which is static by default in Kotlin).
Move the properties to the constructor so the object can also be instantiated the regular way (make the constructor private if it shouldn't) and use a secondary constructor that takes a builder and delegates to the primary constructor. The code will look as follow:
class Car( //add private constructor if necessary
val model: String?,
val year: Int
) {
private constructor(builder: Builder) : this(builder.model, builder.year)
class Builder {
var model: String? = null
private set
var year: Int = 0
private set
fun model(model: String) = apply { this.model = model }
fun year(year: Int) = apply { this.year = year }
fun build() = Car(this)
}
}
Usage: val car = Car.Builder().model("X").build()
This code can be shortened additionally by using a builder DSL:
class Car (
val model: String?,
val year: Int
) {
private constructor(builder: Builder) : this(builder.model, builder.year)
companion object {
inline fun build(block: Builder.() -> Unit) = Builder().apply(block).build()
}
class Builder {
var model: String? = null
var year: Int = 0
fun build() = Car(this)
}
}
Usage: val car = Car.build { model = "X" }
If some values are required and don't have default values, you need to put them in the constructor of the builder and also in the build
method we just defined:
class Car (
val model: String?,
val year: Int,
val required: String
) {
private constructor(builder: Builder) : this(builder.model, builder.year, builder.required)
companion object {
inline fun build(required: String, block: Builder.() -> Unit) = Builder(required).apply(block).build()
}
class Builder(
val required: String
) {
var model: String? = null
var year: Int = 0
fun build() = Car(this)
}
}
Usage: val car = Car.build(required = "requiredValue") { model = "X" }
I have seen many examples that declare extra funs as builders. I personally like this approach. Save effort to write builders.
package android.zeroarst.lab.koltinlab
import kotlin.properties.Delegates
class Lab {
companion object {
@JvmStatic fun main(args: Array<String>) {
val roy = Person {
name = "Roy"
age = 33
height = 173
single = true
car {
brand = "Tesla"
model = "Model X"
year = 2017
}
car {
brand = "Tesla"
model = "Model S"
year = 2018
}
}
println(roy)
}
class Person() {
constructor(init: Person.() -> Unit) : this() {
this.init()
}
var name: String by Delegates.notNull()
var age: Int by Delegates.notNull()
var height: Int by Delegates.notNull()
var single: Boolean by Delegates.notNull()
val cars: MutableList<Car> by lazy { arrayListOf<Car>() }
override fun toString(): String {
return "name=$name, age=$age, " +
"height=$height, " +
"single=${when (single) {
true -> "looking for a girl friend T___T"
false -> "Happy!!"
}}\nCars: $cars"
}
}
class Car() {
var brand: String by Delegates.notNull()
var model: String by Delegates.notNull()
var year: Int by Delegates.notNull()
override fun toString(): String {
return "(brand=$brand, model=$model, year=$year)"
}
}
fun Person.car(init: Car.() -> Unit): Unit {
cars.add(Car().apply(init))
}
}
}
I have not yet found a way that can force some fields to be initialized in DSL like showing errors instead of throwing exceptions. Let me know if anyone knows.
Because I'm using Jackson library for parsing objects from JSON, I need to have an empty constructor and I can't have optional fields. Also all fields have to be mutable. Then I can use this nice syntax which does the same thing as Builder pattern:
val car = Car().apply{ model = "Ford"; year = 2000 }
People nowdays should check Kotlin's Type-Safe Builders.
Using said way of object creation will look something like this:
html {
head {
title {+"XML encoding with Kotlin"}
}
// ...
}
A nice 'in-action' usage example is the vaadin-on-kotlin framework, which utilizes typesafe builders to assemble views and components.