Domain-Specific Languages In Kotlin: Getting Started
In this Kotlin tutorial, learn how to create a DSL using Kotlin lambdas with receivers, builder pattern and extension functions! By Tino Balint.
Sign up/Sign in
With a free Kodeco account you can download source code, track your progress, bookmark, personalise your learner profile and more!
Create accountAlready a member of Kodeco? Sign in
Sign up/Sign in
With a free Kodeco account you can download source code, track your progress, bookmark, personalise your learner profile and more!
Create accountAlready a member of Kodeco? Sign in
Sign up/Sign in
With a free Kodeco account you can download source code, track your progress, bookmark, personalise your learner profile and more!
Create accountAlready a member of Kodeco? Sign in
Contents
Domain-Specific Languages In Kotlin: Getting Started
25 mins
DSLs in Kotlin
Now that you know what you want to achieve, you’ll need to use two important features of the Kotlin language to create a DSL:
- Lambda expressions
- Lambdas outside of parentheses.
- Lambdas with receivers.
< The lambda expression is an anonymous function used to wrap behavior you can call any number of times, with or without parameters. You can pass also them around as function parameters and store them as class properties. If you are unfamiliar with lambda expressions, you can check the official documentation.
To create or use a lambda expression, you first have to declare the type of a lambda you need. The lambda-type syntax is as follows: (parameter, parameter) -> returnType
.
You can interpret this by separating it into two parts, left and right side of the arrow. On the left side, there are required parameters inside the parentheses. You can define any number of required parameters for every lambda function, so it doesn’t have to be one, or two. Every lambda can have no parameters, and it can have any number of parameters.
And on the right side, you define the return type. In easier terms, on the left side, you specify which parameters you want to pass to the right side, where you call a method, and return a result, using those parameters. For a lambda function which doesn’t return anything, you can use Unit
as the return type. If you were to write a lambda which takes a String
and returns an Int
, you’d write the following: (String) -> Int
.
However, when you use lambdas expressions in code, they are written in a different way from the lambda-type syntax. Using lambda goes as follows: { parameter, parameter -> behavior }
. You first open the curly braces and define the list of parameters you receive and need to use. Then you separate the code which you’ll run, using an arrow (->
). After that, you can run any number of function calls between the arrow and the closing braces.
Since lambdas are extremely useful, there’s quite a lot of syntax sugar around them. Three most common sugars include inferring lambda parameters, writing lambdas outside of parentheses and lambdas with receivers.
Inferring Lamda Parameters
For example, if a lambda has only one parameter, you don’t have to list it in the expression. It’s then inferred within the curly braces, and named it. It’s common when using collection functions, as you write something like: list.filter { it.isFavorite }
when filtering a list of items. The it
in this example is then inferred as each item in the list, over which you’re iterating.
Lambdas Outside of Parentheses
Lambdas outside of parentheses is a feature in Kotlin which allows you to move the lambda argument outside of parentheses if it is the last parameter. In addition, if it is the only parameter, you can completely remove the parentheses. This means that if you have a method call resembling method({})
, with lambdas outside the parentheses, it can also be called method{}
. This feature makes the code more readable and is suggested by the Kotlin style guide.
Lambdas With Receivers
Imagine that you have a model class for a
data class Puppy(var isLiked: Boolean = false, var imageResource: Int = 0)
You can make a DSL by creating a function named puppy
which takes a lambda as a parameter and returns a full Puppy
object:
fun puppy(lambda: (Puppy) -> Unit): Puppy {
// 1
val puppy = Puppy()
// 2
lambda(puppy)
// 3
return puppy
}
In this function you do the following:
- Instantiate a
Puppy
. - Call the lambda which will use the
Puppy
- Returns the
Puppy
You can invoke the DSL, creating and editing a Puppy
:
puppy {
it.isLiked = true
it.imageResource = R.drawable.golden_retriever
}
You call the puppy
function and set the properties inside the curly brackets. The current solution works, but you have to use it
to access the properties instead of accessing them directly. This can be fixed by implementing lambdas with receivers.
A lambda with receivers allows you to call methods of an object in the body of a lambda without any qualifiers, such as it
. You add the receiver with the class type, which means that the received action is a function that any object of the type can call. Moreover, it changes the this object within the braces. Examine the snippet below:
fun puppy(lambda: Puppy.() -> Unit): Puppy {
val puppy = Puppy()
puppy.lambda()
return puppy
}
Notice that, previously, the lambda passed a Puppy
(lambda: (Puppy) -> Unit
) to the caller. However, with receivers, you can call the lambda as a function directly on any Puppy
. To make the code more simple, you can use Kotlin’s apply
extension function:
fun puppy(lambda: Puppy.() -> Unit) = Puppy().apply(lambda)
apply
allows you to make the function implementation a one-liner by directly referencing the new object without the need to create a named property. The puppy
function is now called like:
puppy {
isLiked = true
imageResource = R.drawable.golden_retriever
}
Instead of using the it
keyword, you can now reference the object directly because you explicitly said that it must be of type Puppy
.
Your First DSL
Armed with the above knowledge, it’s time to modify the DialogPopupBuilder
and create your first DSL!
You’ll use what you learned to refactor the DialogPopupView
and make a DSL for it. The goal is to replace the current functions to set the properties by calling a lambda. By doing this, you’ll be able to call the builder with lambdas outside of parentheses, and without using chained function calls. Replace the DialogPopupBuilder
with:
class DialogPopupBuilder {
var context: Context? = null
var viewToBlur: View? = null
var titleText: String = ""
var negativeText: String = ""
var positiveText: String = ""
var onBackgroundClickAction: () -> Unit = {}
var onNegativeClickAction: () -> Unit = {}
var onPositiveClickAction: () -> Unit = {}
inline fun with(context: () -> Context) {
this.context = context()
}
inline fun viewToBlur(viewToBlur: () -> View) {
this.viewToBlur = viewToBlur()
}
inline fun titleText(title: () -> String) {
this.titleText = title()
}
inline fun negativeText(negativeText: () -> String) {
this.negativeText = negativeText()
}
inline fun positiveText(positiveText: () -> String) {
this.positiveText = positiveText()
}
fun onNegativeClickAction(onNegativeClickAction: () -> Unit) {
this.onNegativeClickAction = onNegativeClickAction
}
fun onPositiveClickAction(onPositiveClickAction: () -> Unit) {
this.onPositiveClickAction = onPositiveClickAction
}
fun onBackgroundClickAction(onBackgroundClickAction: () -> Unit) {
this.onBackgroundClickAction = onBackgroundClickAction
}
fun build() = DialogPopupView(context!!, this)
}
You removed the constructor and added a with()
call, to make it look cleaner in the long run. Since you are going to use a DSL to build the dialog, you don’t need the method to build it in DialogPopupView
. Remove the builder
method from the companion object
.
Finally, you have to write a DSL which will create the DialogPopupBuilder
and build the DialogPopupView
. Create a new Kotlin file called DialogPopupViewDsl.kt and paste in the following code:
fun dialogPopupView(lambda: DialogPopupView.DialogPopupBuilder.() -> Unit) =
DialogPopupView.DialogPopupBuilder() // 1
.apply(lambda) // 2
.build() // 3
The function has a lambda with a DialogPopupBuilder
receiver as a parameter. Each line breaks down as follows:
- Instantiate an instance of
DialogPopupBuilder
. - Set all the properties using the lambda.
- Call
build()
to return aDialogPopupView
.
You can use the DSL instead of the old builder pattern from within PuppyActivity
. Change createDialogPopup()
to use the DSL:
dialogPopupView {
with { this@PuppyActivity }
viewToBlur { rootView }
titleText { titleText }
negativeText { negativeText }
positiveText { positiveText }
onPositiveClickAction { positiveClickAction() }
onNegativeClickAction { negativeClickAction() }
onBackgroundClickAction { backgroundClickAction() }
}
dialogPopupView()
takes every dialog property directly, without the need to chain function calls or calling build()
.
Build and run the code. You’ll see the everything as before, but test the dialog to ensure that it works the same.