9

Kotlin DSLs

Men build too many walls and not enough bridges.

Joseph Fort Newton

Domain-specific languages (DSLs) regularly come up in discussions related to modern languages like Kotlin and Scala because these allow creating simple DSLs quickly within the language. Such internal DSLs can greatly improve code readability, development efficiency, and changeability. This chapter introduces DSLs and how to create them in Kotlin. It also explores the two most popular Kotlin DSLs on Android, namely a Kotlin DSL for Gradle build configurations and one for creating Android layouts.

Introducing DSLs

DSLs are by no means new; research goes back decades1 and is only increasing in recent times, from language modeling in general2 to DSLs in particular.3 Today, DSLs are pervasive in software development. This section explains the term and discusses how DSLs can help improve your code.

1. https://onlinelibrary.wiley.com/doi/abs/10.1002/spe.4380070406

2. http://mdebook.irisa.fr/

3. http://www.se-rwth.de/publications/MontiCore-a-Framework-for-Compositional-Development-of-Domain-Specific-Languages.pdf

What Is a DSL?

As the name implies, a domain-specific language is a language focused on, and often restricted to, a certain application domain. You are likely familiar with many DSLs already, such as SQL, LaTeX,4 or regular expressions. These are in contrast to general-purpose languages like Kotlin, Java, C++,5 Python,6 and others.

4. https://www.latex-project.org/

5. http://www.stroustrup.com/C++.html

6. https://www.python.org/

Focusing on a particular domain allows a more focused and clean syntax and hence better solutions. This is a general trade-off between generic approaches and specific approaches, and it is the reason why languages like COBOL7 and Fortran8 were initially created with a specific domain in mind: business processing and numerical computation, respectively.

7. http://archive.computerhistory.org/resources/text/Knuth_Don_X4100/PDF_index/k-8-pdf/k-8-u2776-Honeywell-mag-History-Cobol.pdf

8. http://www.ecma-international.org/publications/files/ECMA-ST-WITHDRAWN/ECMA-9,%201st%20Edition,%20April%201965.pdf

However, these are different from DSLs written in Kotlin because they are stand-alone languages. When you create a DSL in Kotlin, it is called an embedded DSL—it is embedded into a general-purpose language. This has several consequences that come into play when we discuss benefits and potential drawbacks of these DSLs.

Benefits and Drawbacks

As with any approach or tool, there are both pros and cons to consider when developing or using DSLs. Van Deursen, Klint, and Visser9 provide a good overview of both in their literature survey. Here, we view them under the light of embedded DSLs written in Kotlin.

9. http://sadiyahameed.pbworks.com/f/deursen+-+DSL+annotated.pdf

Benefits

The main benefits mentioned by van Deursen et al. are the following:

In addition to these, there are several advantages specific to a Kotlin DSL:

In summary, DSLs are particularly useful where they can embody domain knowledge and reduce complexity by offering a clean, readable API for developers and domain experts.

Potential Drawbacks

In their survey, van Deursen et al. also mention several drawbacks of DSLs that we will again examine in the light of Kotlin DSLs. Potential drawbacks of DSLs in general are as follows:

Another drawback of embedded DSLs in particular is their limited expressiveness. Being embedded into a general-purpose language, they are naturally constrained in their syntax and in what they can express. For Kotlin DSLs in particular, this chapter focuses on DSLs that allow constructing objects more easily.

All in all, most of the common drawbacks of DSLs do not apply to Kotlin DSLs or are at least alleviated. Still, DSLs are not a golden hammer. Not every object creation or configuration must be wrapped into a DSL. But if it can be used to encapsulate boilerplate, reduce complexity, or help domain experts contribute, a DSL can greatly benefit your project. Embedded DSLs, such as in Kotlin, remove most of the effort involved in creating DSLs and thus greatly reduce the barrier to introducing DSLs to your workflow (but they are also limited in their capabilities).

Creating a DSL in Kotlin

In this section, you will write your own DSL in Kotlin from scratch to understand the underlying structure. Kotlin DSLs are primarily based on higher-order functions, lambdas with receivers, default values, and extension functions. The resulting DSL should allow you to create user objects with the syntax shown in Listing 9.1.

Listing 9.1 User DSL

user {
  username = "johndoe"
  birthday = 1 January 1984
  address {
    street = "Main Street"
    number = 42
    postCode = "12345"
    city = "New York"
  }
}

As you can see, this syntax to create objects almost resembles JSON. It hides Kotlin as the underlying language (because it avoids using general-purpose language constructs) and hence does not require users to know Kotlin. Nontechnical team members would be able to understand, validate, and change the user.

DSL to Build Complex Objects

To explore how such a DSL works, you will build one from the top down. The entry point to the DSL is the user function. You know this trick by now. It is simply a higher-order function that accepts a lambda as its last parameter, thus allowing the syntax from Listing 9.1. The user function can be declared as shown in Listing 9.2.

Listing 9.2 DSL Entry Point

fun user(init: User.() -> Unit): User {
  val user = User()
  user.init()
  return user
}

The function accepts a lambda with receiver so that the lambda effectively becomes an extension of the User class. This allows accessing properties such as username and birthday directly from within the lambda. You may notice that you can write this function a lot better using Kotlin’s apply function, as shown in Listing 9.3.

Listing 9.3 DSL Entry Point—Improved

fun user(init: User.() -> Unit) = User().apply(init)

The User class is a simple data class, and for this first version of the DSL, it partly uses nullable fields to keep things simple. Listing 9.4 shows its declaration.

Listing 9.4 User Data Class

import java.time.LocalDate

data class User(
    var username: String = "",
    var birthday: LocalDate? = null,
    var address: Address? = null
)

The code so far accounts for the creation of users with username and birthday in DSL syntax, but there’s currently no way to nest an address object as in the initial example. Adding the address is simply a matter of repeating the process, this time with a member function of User as implemented in Listing 9.5.

Listing 9.5 Adding an Address to a User

data class User(…) {
  fun address(init: Address.() -> Unit) {
    address = Address().apply(init)
  }
}

data class Address(
    var street: String = "",
    var number: Int = -1,
    var postCode: String = "",
    var city: String = ""
)

The address function follows the same concept as the user function, but additionally assigns the created address to the user’s address property. With these few lines of code, you are now able to create a user in DSL syntax as in Listing 9.1 (except for the date).

However, there are several downsides to the current implementation.

To illustrate this last issue, Listing 9.6 shows an undesirable but currently valid use of the DSL.

Listing 9.6 Shortcomings of Current DSL

user {
  address {
    username = "this-should-not-work"
    user {
      address {
        birthday = LocalDate.of(1984, Month.JANUARY, 1)
      }
    }
  }
}

This construct mitigates the DSL advantages of readability and reducing complexity. Properties of the outer scope are visible so that username can be set inside the address block, and both can be nested arbitrarily. This issue is addressed later in this chapter.

Immutability through Builders

The first aspect to be improved are the nullable and mutable properties. The desired DSL will effectively be a type-safe builder.11 Accordingly, builders12 are used in the underlying implementation to accumulate object data, validate the data, and ultimately build the user object. First, write down the desired data classes, as shown in Listing 9.7.

11. https://kotlinlang.org/docs/reference/type-safe-builders.html

12. https://sourcemaking.com/design_patterns/builder

Listing 9.7 Immutable Data Classes

import java.time.LocalDate

data class User(val username: String, val birthday: LocalDate, val address: Address)

data class Address(
    val street: String,
    val number: Int,
    val postCode: String,
    val city: String
)

This way, because both LocalDate and Address are immutable, the User class is immutable, too. Objects of these two classes are now constructed by builders so the next step is to add those, starting with the UserBuilder shown in Listing 9.8.

Listing 9.8 User Builder

class UserBuilder {
  var username = ""                    // Gets assigned directly in DSL => public
  var birthday: LocalDate? = null       // Gets assigned directly in DSL => public
  private var address: Address? = null  // Is built via builder => private

  fun address(init: AddressBuilder.() -> Unit) {  // Nested function to build address
    address = AddressBuilder().apply(init).build()
  }

  fun build(): User {  // Validates data and builds user object
    val theBirthday = birthday
    val theAddress = address
    if (username.isBlank() || theBirthday == null || theAddress == null)
      throw IllegalStateException("Please set username, birthday, and address.")

    return User(username, theBirthday, theAddress)
  }
}

The builder differentiates between properties that should be assigned directly (and are thus public) and properties that are created using a nested DSL syntax (the address) that are thus private. The address function is moved from the User class to the UserBuilder, and now accepts a lambda that has AddressBuilder as its receiver. Also, you can perform arbitrary validation before finally building the object in build. You could use this to implement optional properties as well. Next, you need a builder for address objects, as shown in Listing 9.9.

Listing 9.9 Address Builder

class AddressBuilder {
  var street = ""
  var number = -1
  var postCode = ""
  var city = ""

  fun build(): Address {
    if (notReady())
      throw IllegalStateException("Please set street, number, postCode, and city.")

    return Address(street, number, postCode, city)
  }

  private fun notReady()
      = arrayOf(street, postCode, city).any { it.isBlank() } || number <= 0
}

In this case, there is no more nested construct under address, so the builder only has public properties for all required data and a build method along with simple validation, but no more nested builders.

Lastly, the entry point to the DSL must be adapted as in Listing 9.10 to use the builder.

Listing 9.10 Adjusted Entry Point to the DSL

fun user(init: UserBuilder.() -> Unit) = UserBuilder().apply(init).build()

The lambda’s receiver is now UserBuilder. Accordingly, the init function is applied to the UserBuilder, and calling build is required. So inside init, you can initialize the public properties directly or call a DSL function to build a more complex object, such as an address.

Nesting Deeper

The current DSL allows adding an arbitrary number of address blocks, but each one would override the address from the previous. So a user can currently only have a single address, but multiple may be desired. There are different ways to design this part of the DSL.

For now, let’s assume a user can indeed have multiple addresses but there is no dedicated block to hold them (the second possibility). One way to achieve this easily is to give the data class a list of addresses, and then in address, add the built object to that list as done in Listing 9.11.

Listing 9.11 Allowing Multiple Addresses

data class User(…, val addresses: List<Address>)

class UserBuilder {
  // …
  private val addresses: MutableList<Address> = mutableListOf()

  fun address(init: AddressBuilder.() -> Unit) {
    addresses.add(AddressBuilder().apply(init).build())
  }

  fun build(): User { … }
}

This now lets you add multiple address blocks, and each one adds another address to the user object. Next, a dedicated addresses block should encompass all addresses, yielding the syntax shown in Listing 9.12.

Listing 9.12 Syntax of Dedicated Addresses Block

user {
  username = "johndoe"
  birthday = LocalDate.of(1984, Month.JANUARY, 1)
  addresses {  // New dedicated addresses block
    address {  // All address blocks must be placed here
      street = "Main Street"
      number = 42
      postCode = "12345"
      city = "New York"
    }
    address {
      street = "Plain Street"
      number = 1
      postCode = "54321"
      city = "York"
    }
  }
}

Here, addresses can only be created inside the addresses block. To implement such an additional nesting, you need a helper class, an object of which is built by the addresses block. Listing 9.13 adds such a class that simply represents a list of addresses.

Listing 9.13 Implementing Dedicated Addresses Block

class UserBuilder {
  // …
  private val addresses: MutableList<Address> = mutableListOf()

  inner class Addresses : ArrayList<Address>() {
    fun address(init: AddressBuilder.() -> Unit) {
      add(AddressBuilder().apply(init).build())
    }
  }

  fun addresses(init: Addresses.() -> Unit) {  // ‘Addresses’ is the receiver now
    addresses.addAll(Addresses().apply(init))
  }

  fun build(): User {
    val theBirthday = birthday
    if (username.isBlank() || theBirthday == null || addresses.isEmpty()) throw

    return User(username, theBirthday, addresses)
  }
}

This is all you need to enable the syntax shown in Listing 9.12. In general, to allow arbitrarily deep nesting, you must introduce the appropriate helper classes and methods, like Addresses and the addresses function in this case. Once you are familiar with creating DSLs like this, you could even generate (most of) the underlying code because the structure always follows the same pattern. In fact, JetBrains does this with a React DSL used internally.13

13. Source: “Create Your Own DSL in Kotlin” by Victor Kropp (https://youtu.be/tZIRovCbYM8)

Introducing @DslMarker

This small DSL is now mostly finished, but the problems of arbitrary nesting and accessing properties of an outer scope remain (see Listing 9.6). To help alleviate the problem of accessing the outer scope, Kotlin 1.1 introduced the @DslMarker annotation. It’s a meta-annotation that can be used only on other annotations, such as @UserDsl shown in Listing 9.14.

Listing 9.14 User DSL Annotation

@DslMarker
annotation class UserDsl

Now, whenever there are two implicit receivers of a lambda (for instance, when nesting deeper than one level in your DSL), and both are annotated with @UserDsl, only the innermost receiver is accessible. So if you annotate all your DSL classes with it, you can no longer access User properties inside the address block (or addresses block). It also prevents nesting an address block into another address block because the address function belongs to the outer receiver (UserBuilder) and not the innermost one.

What is not prevented this way is calling the user function again from somewhere within the DSL because it is just a top-level function and thus accessible everywhere. However, this is a mistake developers are unlikely to make accidentally. If you want to prevent it, add a deprecated member method to UserBuilder that overshadows the top-level function, as shown in Listing 9.15.

Listing 9.15 Preventing Nested Calls to Entry Point (in UserBuilder)

@Deprecated("Out of scope", ReplaceWith(""), DeprecationLevel.ERROR)
fun user(init: UserBuilder.() -> Unit): Nothing = error("Cannot access user() here.")

With this, your IDE immediately complains if you try to call user from within another call to user. It will not compile, either.

Note

You can follow this same procedure to implement a type-safe builder DSL even if you don’t own the classes (or if they are implemented in Java) by using extension functions.

Only in the case of annotations, it is a little more tricky because you cannot annotate a thirdparty class. Instead, you can annotate the lambda receiver:

fun user(init: (@UserDsl UserBuilder).() -> Unit)
    = UserBuilder().apply(init).build()

To enable annotating types as done above, add the corresponding annotation target (also, retaining the annotation info after compilation is not necessary):

@DslMarker
@Target(AnnotationTarget.CLASS, AnnotationTarget.TYPE) // Can be used on types
@Retention(AnnotationRetention.SOURCE)
annotation class UserDsl

Leveraging Language Features

As embedded DSLs, your Kotlin DSLs can make use of Kotlin’s stack of language features. For example, you could use variables within your DSL naturally and without any extra effort (see Listing 9.16).

Listing 9.16 Using Variables in Your DSL

user {
  // …
  val usercity = "New York"
  addresses {
    address {
      // …
      city = usercity
    }
    address {
      // …
      city = usercity
    }
  }
}

You could also use conditionals, loops, and other constructs. This automatically allows DSL users to avoid code duplication within the DSL but requires programming knowledge. Other powerful features—such as extensions, infix functions, and operators—can be used to make the code read even more naturally. As an example, Listing 9.17 enables a more natural way to write dates with an infix extension.

Listing 9.17 Improving DSL Readability

infix fun Int.January(year: Int) = LocalDate.of(year, Month.JANUARY, this)

user {
  username = "johndoe"
  birthday = 1 January 1984
  // …
}

In reality, you will need 12 extensions to cover all the months, but it can be worth it if your DSL users have to denote many dates because it prevents potential mistakes from zero-indexing versus one-indexing the days and months.

With the addition of this extension function, you can now write code as shown in the initial Listing 9.1, or with a dedicated addresses block if you prefer that convention. But there is still room for improvement because the DSL currently comes with overhead for lambda objects created. You can resolve this issue by inlining the higher-order functions (see Listing 9.18).

Listing 9.18 Improving DSL Performance

@UserDsl
class UserBuilder {
  // …
  val addresses: MutableList<Address> = mutableListOf()

  inner class Addresses : ArrayList<Address>() {
    inline fun address(init: AddressBuilder.() -> Unit) { … }  // Now inlined
  }

  inline fun addresses(init: Addresses.() -> Unit) { … }       // Now inlined
  // …
}

inline fun user(init: UserBuilder.() -> Unit) =                // Now inlined
    UserBuilder().apply(init).build()

Note that, to inline the addresses function, the addresses property it uses must become public as well. Otherwise, it may not be accessible in the place where it’s inlined. The other two higher-order functions can be inlined without further ripple effects. Now, using the DSL has no performance overhead compared with using the builders directly.

DSL for Android Layouts with Anko

A fantastic use case for such a DSL are layouts. Here, the underlying object created by the type-safe builder DSL is a root view that encompasses a layout, such as a linear layout or a constraint layout. Creating a layout programmatically like this has several advantages over XML layouts.

In this section, you learn how to programmatically create layouts the hard way first and then familiarize yourself with Anko Layouts, part of JetBrains’ utility library for Android called Anko.14 To do so, this section covers how to rewrite the layout for the AddTodoActivity from the Kudoo app using Anko.

14. https://github.com/Kotlin/anko

Creating Layouts Programmatically

Before introducing Anko, it is worth mentioning that you could create layouts programmatically without any library. Listing 9.19 shows the code required to implement the AddTodoActivity layout programmatically without Anko.

Listing 9.19 Creating a Layout Programmatically the Hard Way

class AddTodoActivity : AppCompatActivity() {
  // …
  override fun onCreate(savedInstanceState: Bundle?) {
    super.onCreate(savedInstanceState)
    setContentView(createView())  // No inflating of an XML layout
    viewModel = getViewModel(TodoViewModel::class)
  }

  private fun createView(): View {
    val linearLayout = LinearLayout(this).apply {  // Sets up the linear layout
      orientation = LinearLayout.VERTICAL
    }
    val etNewTodo = EditText(this).apply {  // Sets up the EditText
      hint = getString(R.string.enter_new_todo)
      textAppearance = android.R.style.TextAppearance_Medium
      layoutParams = ViewGroup.LayoutParams(
          ViewGroup.LayoutParams.MATCH_PARENT,
          ViewGroup.LayoutParams.WRAP_CONTENT
      )
    }
    val btnAddTodo = Button(this).apply {  // Sets up the Button
      text = getString(R.string.add_to_do)
      textAppearance = android.R.style.TextAppearance
      layoutParams = LinearLayout.LayoutParams(
          ViewGroup.LayoutParams.WRAP_CONTENT,
          ViewGroup.LayoutParams.WRAP_CONTENT
      ).apply { gravity = Gravity.CENTER_HORIZONTAL }
      setOnClickListener {
        val newTodo = etNewTodo.text.toString()
        launch(DB) { viewModel.add(TodoItem(newTodo)) }
        finish()
      }
    }
    return linearLayout.apply {  // Adds views to the linear layout and returns it
      addView(etNewTodo)
      addView(btnAddTodo)
    }
  }
}

You can see that, even though Kotlin’s apply function helps simplify the code quite a bit, creating a layout like this is quite verbose. There’s no support around setting layout parameters, defining listeners, or using string resources to set texts. Luckily, you can do better using Anko.

Anko Dependencies

The first way to include Anko in your Gradle project is to use a metadependency that incorporates all of Anko’s features. Apart from Anko Layouts, this includes Anko Commons, Anko SQLite, and more. Listing 9.20 shows the corresponding Gradle dependency.

Listing 9.20 Anko Metadependency

def anko_version = "0.10.5"
implementation "org.jetbrains.anko:anko:$anko_version"  // Includes all of Anko

You likely do not need all of Anko’s features, so there are smaller dependencies available. For the Anko Layout discussed in this section, all you need is given in Listing 9.21.

Listing 9.21 Dependencies for Anko Layouts

implementation "org.jetbrains.anko:anko-sdk25:$anko_version"
implementation "org.jetbrains.anko:anko-sdk25-coroutines:$anko_version"

Anko has many fine-grained dependencies for Android support libraries and coroutines, all of which are listed on GitHub.15

15. https://github.com/Kotlin/anko#gradle-based-project

Creating Layouts with Anko

Anko uses the same idea you used in the previous section to make layout creation a lot easier. For instance, creating a vertical linear layout that contains a button with a listener is achieved with a few lines of code, as shown in Listing 9.22.

Listing 9.22 Simple Anko Layout

verticalLayout {
  button {
    text = "Receive reward"
    onClick { toast("So rewarding!") }
  }
}

The verticalLayout function is a utility to create a LinearLayout with vertical orientation. By nesting another view inside the lambda, it is automatically added to the containing view. Here, the button becomes part of the linear layout. Setting the text and adding a click listener is as simple as assigning the property and using onClick, respectively. The toast function is also one of Anko’s many utilities, and it works just like the one you wrote yourself.

Adding Layout Parameters

Properties like width, height, margin, and padding are set via layout parameters, for short lparams in Anko. These are chained to the declaration of a view to define how it should be laid out inside its parent. As an example, Listing 9.23 makes the button span its parent’s width and adds margins on all sides to it.

Listing 9.23 Adding Layout Parameters with Anko

verticalLayout {
  button { … }.lparams(width = matchParent) {
    margin = dip(5)
  }
}

Here, the lparams function is chained to the button call. There are many overloads of lparams, one of which allows setting layout width and height directly as arguments (inside parentheses). Both are set to wrapContent by default so, in contrast to XML, you can skip those if the default works for you; Anko provides a matchParent property for the other cases.

Inside the lparams lambda, you can set margin and padding. As in XML, there are convenient properties for margin (all sides), verticalMargin, and horizontalMargin (the same goes for padding). The dip function is used for dp values (density-independent pixels), and there is also an sp function for text sizes (scale-independent pixels).

Tip

With the Anko Support plugin for Android Studio, you can preview your Anko layouts in Android Studio’s Design View—if you modularize them into an Anko component:

class ExampleComponent : AnkoComponent<MainActivity> {
  override fun createView(ui: AnkoContext<MainActivity>): View = with(ui) {
    verticalLayout {
      button { … }.lparams(width = matchParent) { … }
    }
  }
}

Unfortunately, the preview requires a rebuild, so the feedback cycle is significantly slower than with XML layouts. Also, it currently seems to work with activities only, not fragments. To get the benefits of both XML and Anko, you can create your layout in XML initially and then migrate it to Anko. The process is straightforward, and Anko even comes with an XML-to-Anko converter that you can find in the menu under Code and then Convert to Anko Layouts DSL while inside an XML layout.

Migrating Kudoo’s AddTodoActivity to Anko Layouts

Let’s put these concepts into practice by rewriting part of the layout for the Kudoo app. The simple examples shown so far already cover most of what you need to create a proper layout with Anko. Following the same structure, you can build up the basic layout as it is used in the AddTodoActivity, as shown in Listing 9.24.

Listing 9.24 Activity Layout with Anko

class AddTodoActivity : AppCompatActivity() {
  // …
  override fun onCreate(savedInstanceState: Bundle?) {
    super.onCreate(savedInstanceState)
    setContentView(createView())  // Still no inflating of an XML layout
    viewModel = getViewModel(TodoViewModel::class)
  }

  private fun createView() = verticalLayout {  // Sets up vertical linear layout

    val etNewTodo = editText {  // Sets up EditText and adds it to the linear layout
      hintResource = R.string.enter_new_todo
      textAppearance = android.R.style.TextAppearance_Medium
    }.lparams(width = matchParent, height = wrapContent) {
      margin = dip(16)
    }

    button(R.string.add_to_do) {  // Sets up Button and adds it to the linear layout
      textAppearance = android.R.style.TextAppearance
    }.lparams(width = wrapContent, height = wrapContent) {
      gravity = Gravity.CENTER_HORIZONTAL
    }.setOnClickListener {        // Could also use onClick inside button {…} instead
      val newTodo = etNewTodo.text.toString()
      launch(DB) { viewModel.add(TodoItem(newTodo)) }
      finish()
    }
  }
}

Here, the layout makes use of variables and assignments naturally within the DSL. Again, this is a benefit of an embedded DSL. Second, it uses one of the helper properties provided by Anko to assign resources directly, namely hintResource. This way, you can avoid calls to methods like getString to read the value of an Android resource. Note that the views no longer require IDs for this layout; views that are required outside of the DSL code can be made accessible by assigning them to variables from an outer scope.

Modularizing the Anko Layout

The code from Listing 9.24 creates a view (a LinearLayout) that you can directly use inside the activity’s onCreate method instead of a layout inflater. However, a better approach is to use AnkoComponent to modularize the code, as shown in Listing 9.25.

Listing 9.25 Modularized Activity Layout Using an Anko Component

class AddTodoActivity : AppCompatActivity() {
  // …
  override fun onCreate(savedInstanceState: Bundle?) {
    super.onCreate(savedInstanceState)
    setContentView(AddTodoActivityUi().createView(AnkoContext.create(ctx, this)))
    viewModel = getViewModel(TodoViewModel::class)
  }

  private inner class AddTodoActivityUi : AnkoComponent<AddTodoActivity> {
    
    override fun createView(ui: AnkoContext<AddTodoActivity>): View = with(ui) {
      
      verticalLayout {
        val etNewTodo = editText {
          hintResource = R.string.enter_new_todo
          textAppearance = android.R.style.TextAppearance_Medium
        }.lparams(width = matchParent, height = wrapContent) {
          margin = dip(16)
        }

        button(R.string.add_to_do) {
          textAppearance = android.R.style.TextAppearance
        }.lparams(width = wrapContent, height = wrapContent) {
          gravity = Gravity.CENTER_HORIZONTAL
        }.setOnClickListener {
          val newTodo = etNewTodo.text.toString()
          launch(DB) { viewModel.add(TodoItem(newTodo)) }
          finish()
        }
      }
    }
  }
}

Adding Custom Views

Anko comes with builder functions for all of Android’s views, but what if you have a custom view? How can you incorporate that into the layout DSL? Fortunately, Anko is extensible in this regard so that you can extend it with your custom view via extension functions, and the syntax looks familiar. Assume you have a custom frame layout like the one in Listing 9.26 that ensures that it always has the same width as height.

Listing 9.26 Custom Frame Layout

import android.content.Context
import android.util.AttributeSet
import android.widget.FrameLayout

class SquareFrameLayout(
    context: Context,
    attributes: AttributeSet? = null,
    defStyleAttr: Int = 0
) : FrameLayout(context, attributes, defStyleAttr) {
  
  override fun onMeasure(widthMeasureSpec: Int, heightMeasureSpec: Int) {
    super.onMeasure(widthMeasureSpec, widthMeasureSpec)  // Equal width and height
  }
}

You can incorporate this into the Anko Layout DSL by adding an extension function on Android’s ViewManager that handles its creation, as shown in Listing 9.27.

Listing 9.27 Integrating a Custom Layout into Anko

import android.view.ViewManager
import org.jetbrains.anko.custom.ankoView

inline fun ViewManager.squareFrameLayout(init: SquareFrameLayout.() -> Unit) =
    ankoView({ SquareFrameLayout(it) }, theme = 0, init = init)

As you can see, the function signature closely resembles the ones you used to build the User DSL before; the lambda parameter becomes an extension of your SquareFrameLayout. The ankoView function is used to create the view, it can optionally apply a theme, and it handles further initialization based on the lambda expression that is passed in. Its implementation is not much different from the builder methods in the User DSL. Its first parameter represents a factory so that you can tell it how to construct the initial object before applying init, here just using SquareFrameLayout(it). You could also add a theme parameter to your extension function and pass it along to ankoView to allow users to set a theme. With this, you can use squareFrameLayout from within the Anko DSL to build an object of this custom view.

Anko Layouts versus XML Layouts

Anko Layouts have several benefits over XML layouts as listed at the beginning of this section—most notably, type safety and improved performance while saving battery. However, there are also drawbacks when compared with XML. In parts, they have already become apparent throughout this section but I will list them again here.

In the end, which one is the better choice for your project depends on which of these points you prioritize. In any case, I recommend starting out with an XML layout until you are satisfied with it. After that, you can evaluate the possibility of migrating it to Anko.

DSL for Gradle Build Scripts

In 2016, Gradle announced a DSL based on Kotlin as an alternative to Groovy to write build scripts, and so the Gradle Kotlin DSL16 was born. The main reason for this decision was Kotlin’s static typing that enables better tool support in Gradle, from code completion and navigation to the ability to use all of Kotlin’s language features,17 thus making it easier to write build scripts from scratch.

16. https://github.com/gradle/kotlin-dsl

17. https://blog.gradle.org/kotlin-meets-gradle

The Gradle Kotlin DSL is a fair alternative to the usual Groovy build scripts and has its advantages and drawbacks. It is certainly not a tool you have to use today (at the time of writing); it still has its weaknesses and not the best documentation. But it is worth exploring, especially in the context of Kotlin DSLs. So in this section, you’ll rewrite your Nutrilicious build scripts using the Gradle Kotlin DSL.

Migrating Nutrilicious to Gradle Kotlin DSL

Based on the existing build scripts, you will migrate to the Gradle Kotlin DSL step by step in this section. This will uncover many similarities and some differences between the Kotlin DSL and the Groovy DSL.

Note

At the time of writing, Android Studio may not immediately recognize the Gradle Kotlin DSL. In that case, try Refresh All Gradle Projects in the Gradle view, and if that does not help try restarting Android Studio.

Migrating Gradle Settings

Start migrating the simplest Gradle file, the settings.gradle. For the Nutrilicious app, its definition in Groovy is just the one line shown in Listing 9.28.

Listing 9.28 settings.gradle (Groovy)

include ":app"

As opposed to Groovy, Kotlin does not allow skipping the parentheses of such method calls, thus its equivalent in the Kotlin DSL uses parentheses, as shown in Listing 9.29.

Listing 9.29 settings.gradle.kts (Kotlin)

include(":app")

You have to rename the file to settings.gradle.kts to indicate that you are using a Kotlin script. Nothing else is required, so the project should still build successfully.

Migrating the Root Build Script

Although not as complex as the app module’s build script, the root build script introduces several new concepts of the Gradle Kotlin DSL.

Build Script Block

The buildscript section defines an extra for the Kotlin version along with repositories and dependencies. Its Groovy code is shown again for reference in Listing 9.30.

Listing 9.30 buildscript Block (Groovy)

buildscript {
  ext.kotlin_version = '1.2.50'  // Extra that stores Kotlin version
  repositories {
    google()
    jcenter()
  }
  dependencies {
    classpath 'com.android.tools.build:gradle:3.1.3'
    classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version"
  }
}

This block looks similar using the Gradle Kotlin DSL shown in Listing 9.31.

Listing 9.31 buildscript Block (Kotlin)

buildscript {
  extra["kotlin_version"] = "1.2.50"
  repositories {
    jcenter()
    google()
  }
  dependencies {
    classpath("com.android.tools.build:gradle:3.1.3")
    classpath("org.jetbrains.kotlin:kotlin-gradle-plugin:${extra["kotlin_version"]}")
  }
}

The only notable difference is that extras are defined using extra["key"] = value, and accordingly, they must also be accessed via the extra. Also, you cannot omit the parentheses when calling the classpath function.

All Projects Block

The allprojects block is in fact the exact same in both DSLs (see Listing 9.32).

Listing 9.32 allprojects Block (Groovy and Kotlin)

allprojects {
    repositories {
        jcenter()
        google()
    }
}

Delete Task

The syntax to create tasks is slightly different. In Groovy, the clean task is defined as shown in Listing 9.33.

Listing 9.33 Delete Task (Groovy)

task clean(type: Delete) {
    delete rootProject.buildDir
}

Kotlin uses a higher-order function to create tasks. It accepts the task type as a generic parameter, the task name as a string, and a lambda defining the task (see Listing 9.34).

Listing 9.34 Delete Task (Kotlin)

task<Delete>("clean") {
    delete(rootProject.buildDir)
}

Other than that, the only difference is again the syntax for method calls. This is all that’s required to migrate the root build.gradle file to the Gradle Kotlin DSL. To make it work, rename the file to build.gradle.kts. Android Studio should recognize it as a Gradle build script.

Migrating the Module Build Script

The module’s build script is longer than the other scripts, but the migration is mostly straightforward.

Plugins

The syntax to apply plugins is quite different. Instead of writing apply plugin: 'my-plugin' on the top level for each one, Kotlin introduces a plugins block, as shown in Listing 9.35.

Listing 9.35 Applying Plugins (Kotlin)

plugins {
  id("com.android.application")
  id("kotlin-android")
  id("kotlin-android-extensions")
  id("kotlin-kapt")
}

Using id, you can use the same string as in Groovy to identify the plugins. Alternately, you could use kotlin, which prepends "org.jetbrains.kotlin." to the given plugin. For instance, you could use kotlin("android") instead of id("kotlin-android"). A full list of plugins under org.jetbrains.kotlin is available via the Gradle plugin search.18 Personally, I prefer the consistent look of using only id.

18. https://plugins.gradle.org/search?term=org.jetbrains.kotlin

Android

Next comes the android block. As shown in Listing 9.36, its definition in Kotlin is similar to the Groovy way.

Listing 9.36 Android Setup (Kotlin)

android {
  compileSdkVersion(27)
  defaultConfig {
    applicationId = "com.example.nutrilicious"
    minSdkVersion(19)
    targetSdkVersion(27)
    versionCode = 1
    versionName = "1.0"
    testInstrumentationRunner = "android.support.test.runner.AndroidJUnitRunner"
  }
  buildTypes {
    getByName("release") {
      isMinifyEnabled = false
      proguardFiles("proguard-rules.pro")
    }
  }
}

It is not always obvious which properties can be set using property access and which require a method call. Fortunately, autocompletion helps with this. In the case of SDK versions, the Kotlin DSL requires method calls because the property type is ApiVersion under the hood and the methods are helpers to create these objects from the given integer.

Note that existing build types are accessed using getByName. Creating a new build type works the same way using create("buildtype") { … }. Product flavors are accessed and created with these same two methods—but in a productFlavors block—as you can find out using autocomplete thanks to static typing.

Dependencies

Adding dependencies in the Gradle Kotlin DSL is straightforward: Here, we omit dependencies that don’t show a new concept and focus on a few different ones, shown in Listing 9.37.

Listing 9.37 Adding Dependencies (Kotlin)

dependencies {
  val kotlin_version: String by rootProject.extra  // Uses extra from root script
  implementation("org.jetbrains.kotlin:kotlin-stdlib-jdk7:$kotlin_version")
  // …
  val moshi_version = "1.6.0"
  implementation("com.squareup.moshi:moshi:$moshi_version")
  kapt("com.squareup.moshi:moshi-kotlin-codegen:$moshi_version")
  // …
  testImplementation("junit:junit:4.12")
  androidTestImplementation("com.android.support.test:runner:1.0.2")
  androidTestImplementation("com.android.support.test.espresso:espresso-core:3.0.2")
}

Extras defined in the root build script are accessed by using the rootProject.extra map as a delegate (remember you can use maps as delegates). Other than that, you simply use val instead of def and add parentheses to method calls. The method names themselves are the same as in Groovy—for example, implementation and kapt—so they are easily discoverable.

Experimental Features

The last block in the script enables experimental Kotlin Android Extensions. Unfortunately, at the time of writing, enabling experimental Android extensions does not work in the Kotlin DSL as expected. But the DSL allows you to inject Groovy closures at any point, meaning you can always fall back to Groovy to circumvent such issues, as done in Listing 9.38.

Listing 9.38 Enabling Experimental Android Extensions (Kotlin)

androidExtensions {
  configure(delegateClosureOf<AndroidExtensionsExtension> {  // Injects Groovy code
    isExperimental = true
  })
}

The given lambda is transformed to a Groovy Closure<AndroidExtensionsExtension>, where the AndroidExtensionsExtension is also the receiver of the androidExtensions lambda. So the closure is simply passed along. By the time you read this, the issue may have been resolved so I recommend trying it without the configure call first.

Using buildSrc in Gradle

One way to modularize your Gradle builds is to make use of Gradle’s buildSrc directory. If you add it under the project root, Gradle will compile it and add all its declarations to the classpath of your build scripts. The buildSrc directory is placed in the project’s root directory (next to the app directory). Its structure is the same as for any module, including a Gradle build file of its own and the directory buildSrc/src/main/java. Inside this java directory, add a new file GradleConfig.kt. Any declarations in this file will be available in your build script so you can extract all your versions and dependencies into this file, as shown in Listing 9.39.

Listing 9.39 Gradle Config in buildSrc

private const val kotlinVersion = "1.2.50"
private const val androidGradleVersion = "3.1.3"

private const val supportVersion = "27.1.1"
private const val constraintLayoutVersion = "1.1.0"
// All versions as in build.gradle.kts…

object BuildPlugins {
  val androidGradle = "com.android.tools.build:gradle:$androidGradleVersion"
  val kotlinGradlePlugin = "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlinVersion"
}

object Android {
  val buildToolsVersion = "27.0.3"
  val minSdkVersion = 19
  val targetSdkVersion = 27
  val compileSdkVersion = 27
  val applicationId = "com.example.nutrilicious"
  val versionCode = 1
  val versionName = "1.0"
}

object Libs {
  val kotlin_std = "org.jetbrains.kotlin:kotlin-stdlib:$kotlinVersion"
  val appcompat = "com.android.support:appcompat-v7:$supportVersion"
  val design = "com.android.support:design:$supportVersion"
  // All dependencies as in build.gradle.kts…
}

This file now encapsulates all the concrete versions and dependencies of the app. To scope the properties, you can place them in corresponding objects.

Next, to enable Kotlin in this module, add the build.gradle.kts script from Listing 9.40 directly into the buildSrc directory.

Listing 9.40 buildSrc/build.gradle.kts

plugins {
  `kotlin-dsl`  // Uses ticks: ``
}

All declarations from the GradleConfig file can now be used in the build script. For the sake of brevity, Listing 9.41 only shows snippets, but the full source is available in the GitHub repository for this book.19

19. https://github.com/petersommerhoff/kotlin-for-android-app-development

Listing 9.41 Using the Gradle Config

android {
  // …
  targetSdkVersion(Android.targetSdkVersion)  // Uses the ‘Android’ object
  versionCode = Android.versionCode
  // …
}

dependencies {
  // …
  implementation(Libs.moshi)                  // Uses the ‘Libs’ object
  kapt(Libs.moshi_codegen)
  // …
}

The BuildPlugins object can be used in the same way in the root build script. Note that you don’t need to import anything to use the declarations from buildSrc in your build scripts.

Benefits and Drawbacks

The benefits of the Gradle Kotlin DSL were already mentioned and are based on tool support through static typing. Android Studio can autocomplete available functions for further nesting, and which other properties and methods can be called. This is especially helpful when writing or extending a build script.

On the other hand, at the time of writing, Android Studio may not correctly discover the DSL methods when migrating to the Gradle Kotlin DSL. As mentioned, a restart usually solves this problem. Unfortunately, at the time of writing, Android Studio also does not seem to pick up on the settings.gradle.kts file correctly and does not show it under Gradle Scripts.

When using Gradle’s buildSrc, Android Studio currently needs a rebuild to reflect updates to the files and provide the correct autocompletions. Also, Android Studio will no longer provide hints to indicate that newer versions of dependencies are available (but there is a Gradle plugin to do this20). If you currently use the root build script to store version and dependency info, following this approach can significantly clean up your root build script.

20. https://github.com/ben-manes/gradle-versions-plugin

Most of these issues refer to Android Studio and should be resolved in future releases. In summary, it is convenient to use Kotlin as a single language (that you know well) for both logic and build scripts. But considering the current tool limitations, sticking to Groovy is a reasonable choice.

Summary

You can now create your own simple Kotlin DSLs from scratch by combining higher-order functions, extensions, infix functions, and other language features. You have also seen how this concept is applied to Android layouts with Anko and Gradle build scripts with the Gradle Kotlin DSL. These two are currently the most prevalent Kotlin DSLs for Android development. Both have their advantages and drawbacks you have to weigh before deciding on which approach to use. In any case, Kotlin DSLs are a powerful tool to add to your toolbox to create even cleaner APIs to build complex objects or configurations.