Kotlin DSL - let's express code in "mini-language" - Part 3 of 5

In this third post of this series , we take a look at the some of the use cases of DSLs in Android.

Spans

Custom spans builders for Android Text

Our Textview example with multiple spans

In this example , we have a simple screen where some text is displayed, each line has some words wrapped up in spans - Bold , italics , colored text

To achieve this , we would have to write the following :

val spannable1 = SpannableString("some formatted text")

spannable1.setSpan(StyleSpan(Typeface.BOLD), 0, 4, SPAN_EXCLUSIVE_EXCLUSIVE)
spannable1.setSpan(StyleSpan(Typeface.ITALIC), 6, 15, SPAN_EXCLUSIVE_EXCLUSIVE)
spannable1.setSpan(ForegroundColorSpan(COLOR.RED),17,21, SPAN_EXCLUSIVE_EXCLUSIVE)

val spannable2 = SpannableString("nested text")

spannable.setSpan(StyleSpan(Typeface.BOLD), 0, 6, SPAN_EXCLUSIVE_EXCLUSIVE)
spannable.setSpan(StyleSpan(Typeface.ITALIC), 0, 6, SPAN_EXCLUSIVE_EXCLUSIVE)
spannable.setSpan(URLSpan(url), 8, 12, SPAN_EXCLUSIVE_EXCLUSIVE)

val spannable3 = SpannableString("no wrapping")

spannable.setSpan(StyleSpan(Typeface.BOLD), 0, 42, SPAN_EXCLUSIVE_EXCLUSIVE)
spannable.setSpan(SuperscriptSpan(), 4, 12, SPAN_EXCLUSIVE_EXCLUSIVE)

What if we could simply this with DSLs

First we have :

fun spannable(func: () -> SpannableString) = func()

As you can see spannable  has a parameter of a function type, which it can call in its method body. If we now want to use this higher order function, we can make use of lambdas, also referred to as “function literal”

Next , we have a function span , which takes a charsequence param and another param which accepts Any

private fun span(s: CharSequence, o: Any) =
  (if (s is String) SpannableString(s) else s as? SpannableString
   ?: SpannableString(""))
   .apply { setSpan(o, 0, length, SPAN_EXCLUSIVE_EXCLUSIVE) }

This adds the span to the charsequence and returns a SpannableString

Next , we declare

operator fun SpannableString.plus(s: SpannableString) =
    SpannableString(this concat s)
operator fun SpannableString.plus(s: String) =
    SpannableString(this concat s)

When you use operator in Kotlin, it's corresponding member function is called. For example, expression a+b transforms to a.plus(b) under the hood.

For creating bold , italics span , we could declare it like this :

fun bold(s: CharSequence) =
    span(s, StyleSpan(android.graphics.Typeface.BOLD))
fun italic(s: CharSequence) =
    span(s, StyleSpan(android.graphics.Typeface.ITALIC))

more Spans could be declared :

fun sub(s: CharSequence) =
    span(s, SubscriptSpan()) // baseline is lowered
fun size(size: Float, s: CharSequence) =
    span(s, RelativeSizeSpan(size))
fun color(color: Int, s: CharSequence) =
    span(s, ForegroundColorSpan(color))
fun url(url: String, s: CharSequence) =
    span(s, URLSpan(url))

Finally , we could call it :

val spanned = spannable{ bold("some") +
  italic(" formatted") +
  color(Color.RED, " text") }

val nested = spannable{ bold(italic("nested ")) +
  url("www.google.com", “text")
}
      
val noWrapping = bold("no ") + sub(“wrapping")

Intents

Simplifying Android Intents

var intent = Intent(myActivity, TargetActivity::class)
    intent.putExtra("myIntVal", 10)
    intent.putExtra("myStrVal", "Hello String")
    intent.putExtra("myBoolVal", false)
    myActivity.startActivity(intent)
myActivity.launchActivity<TargetActivity> {
        putExtra("myIntVal", 10)
        putExtra("myStrVal", "Hello String")
        putExtra("myBoolVal", false)
    }

Let's simplify this with a custom DSL

launchActivity<UserDetailActivity> {
        putExtra(INTENT_USER_ID, user.id)
        addFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP)
    }

Its implementation would be as follows :

inline fun <reified T : Any> Activity.launchActivity(
        requestCode: Int = -1,
        options: Bundle? = null,
        noinline init: Intent.() -> Unit = {}) {

    val intent = newIntent<T>(this)
    intent.init()
    if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN) {
        startActivityForResult(intent, requestCode, options)
    } else {
        startActivityForResult(intent, requestCode)
    }
}

and an extension function for Context

inline fun <reified T : Any> Context.launchActivity(
        options: Bundle? = null,
        noinline init: Intent.() -> Unit = {}) {

    val intent = newIntent<T>(this)
    intent.init()
    if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN) {
        startActivity(intent, options)
    } else {
        startActivity(intent)
    }
}

And to start newIntent

inline fun <reified T : Any> newIntent(context: Context): Intent =
        Intent(context, T::class.java)

When using lambdas, the extra memory allocations and extra virtual method call introduce some runtime overhead. So, if we were executing the same code directly, instead of using lambdas, our implementation would be more efficient.

In Part 4 of this series , we will take a look at how to make Broadcast receiver react to lifecycle events.