Kotlin
Completed
-
Section 1: Getting Started
-
Section 2: Core Syntax
-
Section 3: Null Safety
-
Section 4: Object-Oriented Kotlin
-
Section 5: Functional Kotlin
-
Section 6: Coroutines
-
Section 7: Collections Deep Dive
-
Section 8: Type System Deep Dive
-
Section 9: Interop and Tooling
-
Section 10: Kotlin DSLs and Patterns
-
Section 11: Testing and Quality
-
Section 12: Server-Side Kotlin
-
Section 13: Practical Projects
-
Section 14: Interview Practice
-
Section 15: More Practice Exercises
-
Section 16: More Standard Library
-
Section 17: Multiplatform Kotlin
-
Section 18: kotlin.collections In Depth
-
Section 19: kotlin.text In Depth
-
Section 20: kotlin.ranges and kotlin.sequences
-
Section 21: kotlin.io and File Handling
-
Section 22: kotlinx.coroutines Deep Dive
-
Section 23: kotlin.reflect
-
Section 24: Android Development with Kotlin Overview
-
Section 25: Kotlin Multiplatform Deep Dive
-
Section 26: Kotlin for Backend Deep Dive
-
Section 27: Kotlin Design Patterns
-
Section 28: Advanced Language Features
-
Section 29: More Practice Exercises
-
Section 30: More Interview Practice
-
Section 31: Kotlin Type System Deep Dive
-
Section 32: Kotlin Null Safety Advanced
-
Section 33: Kotlin Testing Deep Dive
-
Section 34: Kotlin Build Tooling Deep Dive
-
Section 35: Kotlin Serialization
-
Section 36: Kotlin Performance Considerations
-
Section 37: Kotlin Native Overview
-
Section 38: Kotlin for Data and Scripting
-
Section 39: More Coroutines Practice
-
Section 40: More Android-Adjacent Patterns
-
Section 41: More Practical Projects
-
Section 42: More Design and Architecture Practice
-
Section 43: Kotlin Language Evolution
-
Section 44: More Interview and Review
-
Section 45: Kotlin Delegation Patterns Deep Dive
-
Section 46: Kotlin Annotations Deep Dive
-
Section 47: Kotlin for Gradle Plugin Development
-
Section 48: Kotlin Concurrency Beyond Coroutines
-
Section 49: Kotlin Compiler Internals
-
Section 50: Real-World Kotlin Case Studies
-
Section 51: Final Practice Projects
-
Section 52: Kotlin for Server-Side Reactive Programming
-
Section 53: More Practice and Drills
-
Section 54: Kotlin Security Practices
52: Kotlin DSLs and Builder Patterns
I've spent a lot of time in large codebases where configuring objects felt like filling out a government form. You've seen it: constructors with fifteen nullable arguments, or a chain of .setSomething() calls that stretch across the screen. It's tedious and error-prone. This is why I love Kotlin DSLs. We can actually create a mini-language inside Kotlin that makes our configuration look like a declaration rather than a set of instructions.
To show you how this works, let's build a simple DSL for creating Email Templates. We want to be able to define a subject, a header, and a body in a way that feels natural.
The frustration of clunky constructors
If we did this the "standard" way, we'd have a data class like this:
data class Email(
val subject: String,
val header: String,
val body: String,
val footer: String
)
That's fine for three fields, but once you add CCs, BCCs, attachments, and priority levels, the constructor becomes a nightmare. You start forgetting which String is the header and which is the body. I've definitely pushed a bug to production because I swapped two adjacent String arguments in a constructor. Let's avoid that.
Moving toward a builder
The first step is the Builder pattern. We create a separate class to hold the state while we're "assembling" the email. I'll start by defining an EmailBuilder that has a build() method to return the final immutable Email object.
class EmailBuilder {
var subject: String = ""
var header: String = ""
var body: String = ""
var footer: String = ""
fun build() = Email(subject, header, body, footer)
}
Now, the "Kotlin magic" happens. We can write a top-level function that takes a lambda. But not just any lambda—a function literal with receiver. By using EmailBuilder.() -> Unit, we tell Kotlin that inside the lambda, the this context is an instance of EmailBuilder.
fun email(init: EmailBuilder.() -> Unit): Email {
val builder = EmailBuilder()
builder.init() // This executes the lambda within the context of the builder
return builder.build()
}
Now look at how we use it. It's clean. It's declarative. We don't even need to type this.subject because we're inside the receiver scope:
val myEmail = email {
subject = "Project Update"
header = "Hello Team,"
body = "The sprint is looking good!"
footer = "Best, Engineering Lead"
}
Adding nested complexity
Real-world DSLs usually have nesting. Let's say an email can have multiple "sections" in the body. I'll create a Section class and a way to add them to the builder.
class Section(var content: String = "")
class EmailBuilder {
var subject: String = ""
val sections = mutableListOf<Section>()
fun section(init: Section.() -> Unit) {
val s = Section().apply(init)
sections.add(s)
}
fun build() = Email(subject, sections)
}
Oops, my scope is leaking
Here is where I usually trip up the first time I build these. I tried to run this code and noticed something weird. Because section is a function of EmailBuilder, and the Section block is also a lambda, I could accidentally call section { ... } inside another section { ... } block.
Since Section doesn't have a section method, you'd think it's impossible. But because of how Kotlin handles receivers in nested lambdas, the inner block still has access to the outer EmailBuilder receiver. I could accidentally nest sections infinitely, which makes no sense for my email model.
email {
subject = "Wrong"
section {
content = "I am the first section"
section { // This should NOT be allowed, but Kotlin lets me!
content = "I am a nested section that shouldn't exist"
}
}
}
Locking it down with @DslMarker
To fix this, Kotlin provides @DslMarker. It's an annotation that tells the compiler: "Once you enter a receiver of this type, you can't access the receivers of the outer scope."
First, I define the marker annotation. Then, I apply it to all my builder classes.
@DslMarker
annotation class EmailDsl
@EmailDsl
class EmailBuilder {
var subject: String = ""
val sections = mutableListOf<Section>()
fun section(init: Section.() -> Unit) {
val s = Section().apply(init)
sections.add(s)
}
}
@EmailDsl
class Section {
var content: String = ""
}
Now, if I try to call section { ... } inside another section block, the compiler will throw an error. It forces the developer to stick to the hierarchy I've designed. It's a small addition, but it's the difference between a "loose" script and a professional, type-safe DSL.
📋 Practical Task
Build a Markdown-style List DSL
Your task is to create a DSL that allows a user to build a nested list structure (similar to a Markdown bulleted list).
Requirements:
- Create a
ListItemclass that can hold atextstring and a list of childListItemobjects. - Create a
ListBuilderclass to manage the assembly of these items. - Implement a top-level
bulletList { ... }function. - Ensure you use a
@DslMarkerannotation to prevent calling theitem { ... }function inside anotheritem { ... }block unless you explicitly design it to allow nesting (in this case, the requirements want you to allow nesting, but prevent the user from accessing the rootListBuilderproperties from within a nested item). - The final output should be a
ListofListItemobjects.
Example Usage you should be aiming for:
val myList = bulletList {
item {
text = "Shopping"
item { text = "Milk" }
item { text = "Eggs" }
}
item {
text = "Chores"
item { text = "Vacuum" }
}
}There are no comments for now.