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:
- Lambda with receiver: A lambda function that can access methods and properties of a specific object (the receiver) without any qualifiers
- 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
// 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:
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:
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><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:
// 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:
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:
<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
).
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:
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:
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:
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
-
Design for readability: The primary goal of a DSL is to be readable and intuitive.
-
Use descriptive function names: The function names in your DSL should clearly describe what they do.
-
Maintain type safety: Leverage Kotlin's type system to catch errors at compile time.
-
Limit your scope: Provide only the methods and properties that make sense in a particular context.
-
Document your DSL: Even though your DSL should be readable, documentation is still important, especially for complex operations.
-
Be consistent: Use consistent naming and parameter ordering throughout your DSL.
-
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
- Kotlin Official Documentation on Type-Safe Builders
- Kotlin DSL GitHub Examples
- Gradle Kotlin DSL Documentation
Exercises
-
Create a type safe builder for a simple todo list application that allows adding tasks with priorities and due dates.
-
Extend the HTML builder example to include more HTML elements and attributes.
-
Build a DSL for creating SQL queries in a type-safe manner.
-
Create a configuration DSL for a hypothetical web server that allows configuring ports, routes, and middleware.
-
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! :)