Skip to main content

Kotlin Type Safe Builders

Type safe builders are one of Kotlin's most powerful features, allowing you to create readable and type-safe domain-specific languages (DSLs). In this tutorial, you'll learn what type safe builders are, how they work, and how to create your own.

Introduction to Type Safe Builders

Type safe builders are a design pattern in Kotlin that allows you to create structured data in a declarative way. Unlike traditional builder patterns in other languages, Kotlin's type safe builders leverage language features like lambda expressions with receivers, extension functions, and operator overloading to create code that is:

  • Readable and declarative
  • Type-safe and checked at compile time
  • Structured hierarchically to mirror the data it's building

You might have already encountered DSLs in Kotlin if you've worked with frameworks like:

  • Jetpack Compose for Android UI
  • Kotlin HTML builders
  • Gradle build scripts written in Kotlin

How Type Safe Builders Work

At the core of type safe builders are two important Kotlin features:

  1. Lambda with receiver: A lambda function that can access methods and properties of a specific object (the receiver) without any qualifiers
  2. Extension functions: Functions that extend a class without modifying its source code

Let's explore how these combine to create type safe builders:

Basic Building Blocks

kotlin
// This is the base concept of a lambda with receiver
fun example(block: StringBuilder.() -> Unit): String {
val stringBuilder = StringBuilder()
stringBuilder.block() // The StringBuilder is the receiver for the lambda
return stringBuilder.toString()
}

// Usage
val result = example {
append("Hello") // 'this' is implicitly the StringBuilder
append(" ")
append("World")
}

println(result) // Output: Hello World

Creating a Simple HTML Builder

Let's create a simple HTML builder to understand type safe builders better:

kotlin
class TagBuilder(private val tagName: String) {
private val children = mutableListOf<String>()
private val attributes = mutableMapOf<String, String>()

fun attribute(name: String, value: String) {
attributes[name] = value
}

fun text(value: String) {
children.add(value)
}

fun tag(name: String, init: TagBuilder.() -> Unit): TagBuilder {
val tag = TagBuilder(name)
tag.init()
children.add(tag.toString())
return tag
}

override fun toString(): String {
val attrString = attributes.entries.joinToString(" ") {
"${it.key}=\"${it.value}\""
}

val openTag = if (attrString.isEmpty())
"<$tagName>"
else
"<$tagName $attrString>"

val closeTag = "</$tagName>"
val content = children.joinToString("")

return openTag + content + closeTag
}
}

fun html(init: TagBuilder.() -> Unit): String {
val html = TagBuilder("html")
html.init()
return html.toString()
}

fun TagBuilder.head(init: TagBuilder.() -> Unit) = tag("head", init)
fun TagBuilder.body(init: TagBuilder.() -> Unit) = tag("body", init)
fun TagBuilder.title(init: TagBuilder.() -> Unit) = tag("title", init)
fun TagBuilder.h1(init: TagBuilder.() -> Unit) = tag("h1", init)
fun TagBuilder.p(init: TagBuilder.() -> Unit) = tag("p", init)
fun TagBuilder.a(href: String, init: TagBuilder.() -> Unit) {
val link = tag("a", init)
link.attribute("href", href)
}

Now let's see how we can use this builder:

kotlin
val webpage = html {
head {
title {
text("My First Kotlin DSL")
}
}
body {
h1 {
text("Welcome to Kotlin Type Safe Builders")
}
p {
text("This is a paragraph built with Kotlin DSL.")
}
p {
text("Check out more at ")
a(href = "https://kotlinlang.org") {
text("Kotlin's official website")
}
}
}
}

println(webpage)

Output:

html
<html><head><title>My First Kotlin DSL</title></head><body><h1>Welcome to Kotlin Type Safe Builders</h1><p>This is a paragraph built with Kotlin DSL.</p><p>Check out more at <a href="https://kotlinlang.org">Kotlin's official website</a></p></body></html>

Real-World Example: UI Builder

Let's create a simplified UI builder that mimics how modern UI frameworks like Jetpack Compose work:

kotlin
// UI Component interface
interface Component {
fun render(): String
}

// Box component
class Box(private val init: BoxScope.() -> Unit) : Component {
private val children = mutableListOf<Component>()
private var backgroundColor: String = "transparent"
private var padding: Int = 0

inner class BoxScope {
fun component(component: Component) {
children.add(component)
}

fun backgroundColor(color: String) {
this@Box.backgroundColor = color
}

fun padding(value: Int) {
this@Box.padding = value
}
}

init {
BoxScope().apply(init)
}

override fun render(): String {
val style = "background-color: $backgroundColor; padding: ${padding}px;"
val childrenRendered = children.joinToString("") { it.render() }
return "<div style=\"$style\">$childrenRendered</div>"
}
}

// Text component
class Text(private val text: String, private val init: TextScope.() -> Unit = {}) : Component {
private var color: String = "black"
private var fontSize: Int = 14

inner class TextScope {
fun color(value: String) {
this@Text.color = value
}

fun fontSize(value: Int) {
this@Text.fontSize = value
}
}

init {
TextScope().apply(init)
}

override fun render(): String {
val style = "color: $color; font-size: ${fontSize}px;"
return "<span style=\"$style\">$text</span>"
}
}

// Builder functions
fun ui(init: () -> Component): String {
return init().render()
}

fun box(init: Box.BoxScope.() -> Unit): Box {
return Box(init)
}

fun text(value: String, init: Text.TextScope.() -> Unit = {}): Text {
return Text(value, init)
}

Using this UI builder:

kotlin
val ui = ui {
box {
backgroundColor("lightblue")
padding(16)

component(text("Hello, Kotlin DSL!") {
color("navy")
fontSize(24)
})

component(box {
backgroundColor("white")
padding(8)

component(text("Nested box with text") {
color("darkgray")
})
})
}
}

println(ui)

Output:

html
<div style="background-color: lightblue; padding: 16px;"><span style="color: navy; font-size: 24px;">Hello, Kotlin DSL!</span><div style="background-color: white; padding: 8px;"><span style="color: darkgray; font-size: 14px;">Nested box with text</span></div></div>

Key Concepts for Building Type-Safe DSLs

To create effective type-safe builders, remember these key principles:

1. Lambda with Receiver

The core mechanism uses function types with receivers (SomeType.() -> Unit).

kotlin
fun container(init: Container.() -> Unit): Container {
val container = Container()
container.init()
return container
}

2. Implicit Receivers

Inside a lambda with receiver, you access the receiver's properties and methods directly:

kotlin
container {
// 'this' is implicitly Container
title = "My Container" // calls setter on Container
addItem("First item") // calls method on Container
}

3. Nesting and Scope Control

Control your scopes carefully to provide context-specific functionality:

kotlin
class Form {
fun input(init: Input.() -> Unit): Input {
val input = Input()
input.init()
// Add to form elements
return input
}

fun button(init: Button.() -> Unit): Button {
// Similar implementation
}
}

// Usage
form {
input {
name = "username"
placeholder = "Enter username"
}
button {
text = "Submit"
onClick = { /* handle click */ }
}
}

4. Extension Functions

They help expand your DSL with more features:

kotlin
fun Form.submitButton(text: String, init: Button.() -> Unit = {}) {
button {
this.text = text
type = "submit"
init()
}
}

// Usage
form {
// Regular input
input { name = "email" }

// Extended functionality
submitButton("Send") {
cssClass = "primary-button"
}
}

Best Practices for Type Safe Builders

  1. Design for readability: The primary goal of a DSL is to be readable and intuitive.

  2. Use descriptive function names: The function names in your DSL should clearly describe what they do.

  3. Maintain type safety: Leverage Kotlin's type system to catch errors at compile time.

  4. Limit your scope: Provide only the methods and properties that make sense in a particular context.

  5. Document your DSL: Even though your DSL should be readable, documentation is still important, especially for complex operations.

  6. Be consistent: Use consistent naming and parameter ordering throughout your DSL.

  7. Provide sensible defaults: Make your DSL easy to use with reasonable default values.

Summary

Kotlin's type safe builders provide a powerful way to create domain-specific languages that are both readable and type-safe. They leverage Kotlin's features like lambda expressions with receivers, extension functions, and scope control to create expressive and intuitive APIs.

By using type safe builders, you can:

  • Create structured, hierarchical data in a declarative way
  • Provide domain-specific constraints and validations at compile time
  • Improve code readability and maintainability
  • Make your APIs more intuitive and user-friendly

These techniques are widely used in modern Kotlin libraries and frameworks, and mastering them will allow you to create powerful and expressive APIs for your own projects.

Additional Resources

Exercises

  1. Create a type safe builder for a simple todo list application that allows adding tasks with priorities and due dates.

  2. Extend the HTML builder example to include more HTML elements and attributes.

  3. Build a DSL for creating SQL queries in a type-safe manner.

  4. Create a configuration DSL for a hypothetical web server that allows configuring ports, routes, and middleware.

  5. Implement a DSL for creating charts and graphs with customizable styles and data points.



If you spot any mistakes on this website, please let me know at [email protected]. I’d greatly appreciate your feedback! :)