In the last article, we introduced how to elegantly encapsulate anonymous inner classes (DSLS, higher-order functions) in Kotlin, and I went into some detail on how to use DSLS in Kotlin. This article can be viewed as an exercise in DSLS from the previous article.

The source come from

Spannable realizes rich text display in Android development, which is also a relatively common use scenario, such as displaying privacy Policy and Service Agreement on the login page. Usually, this is a Span with custom colors and click events, and the following code is roughly needed to use it:


private fun agreePrivate(a) {
    val tv = findViewById<TextView>(R.id.tv_agree)
    val builder = SpannableStringBuilder()
    val text = "I have read and agree to the Privacy Policy."
    builder.append(text)
    // Set span click events
    val clickableSpan = object :ClickableSpan(){
        override fun onClick(widget: View) {
            //do some thing
        }
    }
    builder.setSpan(clickableSpan, 9.15, Spannable.SPAN_EXCLUSIVE_EXCLUSIVE)
    // Set span without underscores
    val noUnderlineSpan = NoUnderlineSpan()
    builder.setSpan(noUnderlineSpan, 9.15, Spanned.SPAN_MARK_MARK)
    // Set the span text color
    val foregroundColorSpan = ForegroundColorSpan(Color.parseColor("#0099FF"))
    builder.setSpan(foregroundColorSpan, 9.15, Spannable.SPAN_EXCLUSIVE_EXCLUSIVE)
    // The Settings are clickable
    tv.movementMethod = LinkMovementMethod.getInstance()
    tv.setText(builder)
}

class NoUnderlineSpan : UnderlineSpan() {
    override fun updateDrawState(ds: TextPaint) {
        ds.color = ds.linkColor
        ds.isUnderlineText = false}}Copy the code

It’s a bit of a hassle to use, as the code above has three setSpans for just one span, and if you need to use a span in many places, the code looks really unelegant. Is there a more elegant way? The answer is DSL, and the above code is finally wrapped in DSL as follows:

tvTestDsl.buildSpannableString {
    addText("I have read and agreed.")
    addText("Privacy Policy"){
        setColor("#0099FF")
        onClick(false) {
            //do some thing}}}Copy the code

Their display is exactly the same, and there is no doubt that the DSL approach is more elegant and convenient for the caller.

Implementation idea:

When I encapsulated the idea of Spannable with a DSL, the first thing I wrote was how I would use it, and I scribbled the code above on a piece of paper.

  1. It should be an extension function of TextView
  2. Inside it is DSL-style code
  3. Each paragraph of text has a function that sets the color and click events

So there are two interfaces and extension functions as follows:

interface DslSpannableStringBuilder {
    // Add a paragraph
    fun addText(text: String, method: (DslSpanBuilder. () - >Unit)? = null)
}

interface DslSpanBuilder {
    // Set the text color
    fun setColor(color: String)
    // Set the click event
    fun onClick(useUnderLine: Boolean = true, onClick: (View) - >Unit)
}

// Create an extension function for TextView that takes the extension function of the interface
fun TextView.buildSpannableString(init: DslSpannableStringBuilder. () - >Unit) {
    // Concrete implementation class
    val spanStringBuilderImpl = DslSpannableStringBuilderImpl()
    spanStringBuilderImpl.init()
    movementMethod = LinkMovementMethod.getInstance()
    // Return SpannableStringBuilder via the implementation class
    text = spanStringBuilderImpl.build()
}
Copy the code

In the last article, we said that in a DSL-style function, the argument should be an extension function of an interface (or its implementation class), so we are essentially restricting the functions that can be called in the DSL through the interface. The last article used the implementation class, this article uses the interface, the reason is very simple, the above is to extend the original interface into DSL style, this article is directly from nothing, the implementation of DSL style.

Implement the corresponding interface:

In fact, like me for the first time out of the DSL novice, thinking is the most difficult, with the interface, DSL hierarchy, the rest is relatively simple implementation. Look directly at the code:

class DslSpannableStringBuilderImpl : DslSpannableStringBuilder {
    private val builder = SpannableStringBuilder()
    // Record the last index value since the last text was added
    var lastIndex: Int = 0
    var isClickable = false

    override fun addText(text: String, method: (DslSpanBuilder. () - >Unit)? {
        val start = lastIndex
        builder.append(text)
        lastIndex += text.length
        valspanBuilder = DslSpanBuilderImpl() method? .let { spanBuilder.it() } spanBuilder.apply { onClickSpan? .let { builder.setSpan(it, start, lastIndex, Spannable.SPAN_EXCLUSIVE_EXCLUSIVE) isClickable =true
            }
            if(! useUnderLine) {valnoUnderlineSpan = NoUnderlineSpan() builder.setSpan(noUnderlineSpan, start, lastIndex, Spanned.SPAN_MARK_MARK) } foregroundColorSpan? .let { builder.setSpan(it, start, lastIndex, Spannable.SPAN_EXCLUSIVE_EXCLUSIVE) } } }fun build(a): SpannableStringBuilder {
        return builder
    }
}

class DslSpanBuilderImpl : DslSpanBuilder {
    var foregroundColorSpan: ForegroundColorSpan? = null
    var onClickSpan: ClickableSpan? = null
    var useUnderLine = true

    override fun setColor(color: String) {
        foregroundColorSpan = ForegroundColorSpan(Color.parseColor(color))
    }

    override fun onClick(useUnderLine: Boolean, onClick: (View) - >Unit) {
        onClickSpan = object : ClickableSpan() {
            override fun onClick(widget: View) {
                onClick(widget)
            }
        }
        this.useUnderLine = useUnderLine
    }
}

class NoUnderlineSpan : UnderlineSpan() {
    override fun updateDrawState(ds: TextPaint) {
        ds.color = ds.linkColor
        ds.isUnderlineText = false}}Copy the code

conclusion

To use a DSL, you need to create the interface of the function you want to use in the DSL, and then declare the function parameter as the extension function of that interface.

If you have a nest like mine in your DSL, you need to create an interface for the nested call for that nest (the nesting in this article is intentional, but using a single interface to pass arguments can also do the trick).