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
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!