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