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 3 of 4 of this article. Click here to view the first page.

DSL With Extension Functions

You just made your first DSL by using a builder pattern! :]

Next, you’ll make a DSL using the extension functions and applying the builder pattern to create the data to display puppies!

Extension functions are functions that allow you to add new functionality to an existing class type. Any object of the type you specify can use the extension function. If you are unfamiliar with extension functions, you can check out the official documentation.

You’ll create another DSL for the same DialogPopupView, but this time, usin extension functions.

Create a new Kotlin file called DialogPopupViewWithExtensionsDsl.kt. This time, you’ll want to pass the context as a parameter to separate it from UI properties. You also want to connect the title of the actions and their lambdas into a single function so that similar properties are grouped in one place.

First, create a function buildDialog that will create the whole dialog and build it:

inline fun buildDialog(context: Context, buildDialog: DialogPopupView.DialogPopupBuilder.() -> Unit): DialogPopupView {
  val builder = DialogPopupView.DialogPopupBuilder()

  builder.context = context
  builder.buildDialog()
  return builder.build()
}

The function accepts a Context and a lambda with a receiver of the type DialogPopupView.DialogPopupBuilder. Building the dialog is the same as in the last example.

Next, create a method for each property group and set the properties inside:

fun DialogPopupView.DialogPopupBuilder.title(title: String) {
  this.titleText = title
}

fun DialogPopupView.DialogPopupBuilder.viewToBlur(viewToBlur: View) {
  this.viewToBlur = viewToBlur
}

fun DialogPopupView.DialogPopupBuilder.negativeAction(
  negativeText: String,
  onNegativeClickAction: () -> Unit
) {
  this.onNegativeClickAction = onNegativeClickAction
  this.negativeText = negativeText
}

fun DialogPopupView.DialogPopupBuilder.positiveAction(
  positiveText: String,
  onPositiveClickAction: () -> Unit
) {
  this.onPositiveClickAction = onPositiveClickAction
  this.positiveText = positiveText
}

fun DialogPopupView.DialogPopupBuilder.backgroundAction(onBackgroundClickAction: () -> Unit) {
  this.onBackgroundClickAction = onBackgroundClickAction
}

Each of these methods is fairly similar. They set the properties for their corresponding groups.

Finally, you can change the createDialogPopup() inside PuppyActivity.kt to use the new DSL:

buildDialog(this) {
  viewToBlur(rootView)
  title(titleText)
  positiveAction(positiveText) { positiveClickAction() }
  negativeAction(negativeText) { negativeClickAction() }
  backgroundAction { backgroundClickAction() }
}

You can see that context is now set through a parameter and not a function. Positive and negative actions are joined with their respective texts to improve the readability by grouping the code.

Build and run the code to verify that everything works as expected. Once again, you’ll get the same screen as before because you only changed the syntax and not the logic. But it never hurts to be sure! :]

Collections With DSL

Another interesting use of DSLs is with collections. Take a look at puppies, which is instantiated at the top of PuppyActivity. The code by itself looks pretty clean and readable; this is because there are currently only two properties inside the Puppy class. But in case you have four or more properties, it will start to look clunky. For this reason, you’ll create a DSL that will change the way you instantiate collections.

Create a new Kotlin file called PuppiesDsl.kt. First, add a PuppyBuilder that has two properties and a build method:

class PuppyBuilder {
  var isLiked: Boolean = false
  var imageResourceId: Int = 0

  fun build(): Puppy = Puppy(isLiked, imageResourceId)
}

The build method returns a Puppy with the listed properties.

Next, create a Puppies.kt file which extends ArrayList of Puppy and has a puppy method:

class Puppies : ArrayList<Puppy>() {
  fun puppy(puppyBuilder: PuppyBuilder.() -> Unit) {
    add(PuppyBuilder().apply(puppyBuilder).build())
  }
}

puppy() uses a lambda with the receiver of type PuppyBuilder. Inside, you call add() from ArrayList, to add a new Puppy built with the PuppyBuilder.

Next, you need to create a PuppyViewModelBuilder that will hold the list of all the puppies, by adding the following code:

class PuppyViewModelBuilder {
  private val puppies = mutableListOf<Puppy>()

  fun puppies(puppiesList: Puppies.() -> Unit) {
    puppies.addAll(Puppies().apply(puppiesList))
  }

  fun build(): ArrayList<Puppy> = ArrayList(puppies)
}

The class contains a MutableList of Puppy. puppies() has a lambda with the receiver of type Puppies and it adds all of the elements returned by calling puppiesList(), to the collection. In addition, you added build() which returns an ArrayList of Puppy with puppies as its data.

Finally, create a puppyViewModel method inside PuppiesDsl:

fun puppyViewModel(puppies: PuppyViewModelBuilder.() -> Unit): ArrayList<Puppy> = 
  PuppyViewModelBuilder().apply(puppies).build()

The function uses a lambda with the receiver of type PuppyViewModelBuilder as a parameter, which you use to build the ArrayList of Puppy.

Now, you can replace the code in the PuppyActivity.kt, which creates puppies, using the DSL you’ve just created:

private var puppies: List<Puppy> = puppyViewModel {
    puppies {
      puppy {
        isLiked = false
        imageResourceId = R.drawable.samoyed
      }
      puppy {
        isLiked = false
        imageResourceId = R.drawable.shiba
      }
      puppy {
        isLiked = false
        imageResourceId = R.drawable.siberian_husky
      }
      puppy {
        isLiked = false
        imageResourceId = R.drawable.akita
      }
      puppy {
        isLiked = false
        imageResourceId = R.drawable.german_shepherd
      }
      puppy {
        isLiked = false
        imageResourceId = R.drawable.golden_retriever
      }
    }
  }

First, you call puppyViewModel(), to begin the data buildup. Inside, you call puppies() in which you call puppy() for each Puppy you need to create. Each of these calls will create a new Puppy, and you can their properties as you like.

The current syntax for creating a collection looks like a JSON structure, which is very user-friendly. The benefits of this DSL would grow, the larger and more complex the Puppy would become. And we all know how puppies can grow! :]

Build and run the app to check the current state. You should get the same starting screen with a list of puppies in the same order.

DSL Markers

Try to add a new Puppy to an existing Puppy or a new ArrayList of Puppy inside the existing list. You’ll see that you are able to do it even though you should not be, since it may break the data. Because you’re creating lambdas within other lambdas, you can still access the receivers of the outer lambdas! To prevent this, you need to create a DSL marker, which was made specifically to solve this case. Inside the PuppiesDsl, at the bottom, create a new annotation class called PuppyDslMarker:

@DslMarker
annotation class PuppyDslMarker

@DslMarker specifies that classes marked with it, or PuppyDslMarker define a DSL.

Next, annotate all the classes inside the PuppiesDsl.kt file with @PuppyDslMarker. Try adding a new Puppy to an existing one, and you’ll get an error that says, “can’t be called with implicit receiver.” Problem solved!