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.

Leave a rating/review
Download materials
Save for later
Share
You are currently viewing page 2 of 4 of this article. Click here to view the first page.

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 object that looks like this:

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 a DialogPopupView.

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.