Using Extension Functions and Operator Overloading on SpannableStrings.

Alex Sullivan

I’m a big fan of Android development - but there’s no denying that there are some rough API’s in the Android ecosystem. I spend a non-trivial amount of time scratching my head and muttering under my breath trying to figure out what incantation I need to speak before something works the way I expect it to. As a result, whenever I have the opportunity to write a wrapper around a particularly nasty Android API I jump at the opportunity. Now that Kotlin is a first class citizen in the world of Android, we can use Extension Functions and Operator Overloading to make some really beautiful API’s. Let’s walk through an example using the SpannableString API.

The Rough API

Android provides the SpannableString API to customize portions of a string with some type of custom style - for example, we can style our string with a black background and red foreground color and make it appear larger with the following code:

val firstStyleString = SpannableStringBuilder("Check out this big text with a black background and red foreround")
firstStyleString.setSpan(RelativeSizeSpan(3.5f), 0, firstStyleString.length, SpannableString.SPAN_EXCLUSIVE_EXCLUSIVE)
firstStyleString.setSpan(BackgroundColorSpan(Color.BLACK), 0, firstStyleString.length, SpannableString.SPAN_EXCLUSIVE_EXCLUSIVE)
firstStyleString.setSpan(ForegroundColorSpan(Color.RED), 0, firstStyleString.length, SpannableString.SPAN_EXCLUSIVE_EXCLUSIVE)

Not too bad. Not great but its manageable. Now let’s say we want to add another string to this one that has a strikethrough style. The code now looks like this:

val firstStyleString = SpannableStringBuilder("Check out this big text with a black background and red foreround")
firstStyleString.setSpan(RelativeSizeSpan(3.5f), 0, firstStyleString.length, SpannableString.SPAN_EXCLUSIVE_EXCLUSIVE)
firstStyleString.setSpan(BackgroundColorSpan(Color.BLACK), 0, firstStyleString.length, SpannableString.SPAN_EXCLUSIVE_EXCLUSIVE)
firstStyleString.setSpan(ForegroundColorSpan(Color.RED), 0, firstStyleString.length, SpannableString.SPAN_EXCLUSIVE_EXCLUSIVE)

val secondStyleString = SpannableStringBuilder(" And now check out this text with a strikethrough style!")
secondStyleString.setSpan(StrikethroughSpan(), 0, secondStyleString.length, SpannableString.SPAN_EXCLUSIVE_EXCLUSIVE)

val appendedString = SpannableStringBuilder(firstStyleString).append(secondStyleString)

Alright, things are starting to get pretty ugly. That’s a lot of cryptic code to get a styled string.

Let’s utilize some of Kotlin’s many awesome features to clean up this API.

Introducing extension functions

First off we’re going to utilize Extension Functions to make this API more fluent. If you haven’t used Kotlins Extension Functions yet you’re in for a treat. They allow you to extend a classes functionality without inheriting from that class or modifying the core class. You can add extension methods or properties to any class you want, including Android framework classes. Pretty sweet.

Building a nicer API via extension functions

We can add an Extension Function to SpannableStringBuilder to make the block above a bit less gross:

fun SpannableStringBuilder.spanText(span: Any): SpannableStringBuilder {
  setSpan(span, 0, length, SpannableString.SPAN_EXCLUSIVE_EXCLUSIVE)
  return this
}

This function gives us a nice way to hide the ugly internals of SpannableString. Our block above now looks like this:

val firstStyleString = SpannableStringBuilder("Check out this big text with a black background and red foreground")
        .spanText(RelativeSizeSpan(3.5f))
        .spanText(BackgroundColorSpan(Color.BLACK))
        .spanText(ForegroundColorSpan(Color.RED))

val secondStyleString = SpannableStringBuilder(" And now check out this text with a strikethrough style!")
        .spanText(StrikethroughSpan())

val appendedString = SpannableStringBuilder(firstStyleString).append(secondStyleString)

Nice. But we can still do better - really all we want to say is “Here’s a string, it should be this big, have a black background, and a red foreground”. The block above still has a lot of boilerplate involved.

Let’s add a few more extension functions to get us to a nice spot:

private fun CharSequence.toSpannable() = SpannableStringBuilder(this)

fun CharSequence.foregroundColor(@ColorInt color: Int): SpannableStringBuilder {
  val span = ForegroundColorSpan(color)
  return toSpannable().spanText(span)
}

fun CharSequence.backgroundColor(@ColorInt color: Int): SpannableStringBuilder {
  val span = BackgroundColorSpan(color)
  return toSpannable().spanText(span)
}

fun CharSequence.relativeSize(size: Float): SpannableStringBuilder {
  val span = RelativeSizeSpan(size)
  return toSpannable().spanText(span)
}

fun CharSequence.supserscript(): SpannableStringBuilder {
  val span = SuperscriptSpan()
  return toSpannable().spanText(span)
}

fun CharSequence.strike(): SpannableStringBuilder {
  val span = StrikethroughSpan()
  return toSpannable().spanText(span)
}

The above defines several functions to convert a CharSequence to a SpannableStringBuilder and apply a certain style to it. Now our API looks like this:

val firstStyleString = "Check out this big text with a black background and red foreground"
        .relativeSize(3.5f)
        .backgroundColor(Color.BLACK)
        .foregroundColor(Color.RED)

val secondStyleString = " And now check out this text with a strikethrough style!"
        .strike()

val appendedString = SpannableStringBuilder(firstStyleString).append(secondStyleString)

Niiicceee. We’ve ditched all of that boilerplate and replaced it with a much more readable code block. But we can still do better.

Introducing operator overloading

Another nice feature Kotlin provides is operator overloading. Kotlin exposes a series of special symbols such as + , *, -, and % that developers can overload for their own classes. You can also utilize those operators on existing classes outside of your control via Extension Functions.

Using operator overloading to make our API even better

One of the sore points of our current SpannableString code block is this line:

val appendedString = SpannableStringBuilder(firstStyleString).append(secondStyleString)

It breaks the clean flow we’ve been building up and exposes the SpannableStringBuilder that we’ve been working to hide. Luckily we can utilize the + operator to make things clearer:

operator fun SpannableStringBuilder.plus(other: SpannableStringBuilder): SpannableStringBuilder {
  return this.append(other)
}

operator fun SpannableStringBuilder.plus(other: CharSequence): SpannableStringBuilder {
  return this + other.toSpannable()
}

now the above line is shortened to the following:

val appendedString = firstStyleString + secondStyleString

b-e-a-utiful. Now it’s immediately clear what’s happening at each stage of our string-building experience. We can even add another normal string into the mix easy-peasy:

val firstStyleString = "Check out this big text with a black background and red foreround"
        .relativeSize(3.5f)
        .backgroundColor(Color.BLACK)
        .foregroundColor(Color.RED)

val secondStyleString = " And now check out this text with a strikethrough style!"
        .strike()

val appendedString = firstStyleString + secondStyleString + " And a regular string! "

Extending our API

Another common requirement when using the SpannableString API is apply a style to a single word in a string. We can utilize the set operator in Kotlin to enhance our API and accomplish this task:

operator fun SpannableStringBuilder.set(old: CharSequence, new: SpannableStringBuilder): SpannableStringBuilder {
  val index = indexOf(old.toString())
  return this.replace(index, index + old.length, new, 0, new.length)
}

Now we can use indexed accessor properties to replace one of the words with whatever we want, like this:

appendedString["regular"] = "green".foregroundColor(Color.GREEN)

Final product

Our final code is now much cleaner and much more straightforward to reason about:

val firstStyleString = "Check out this big text with a black background and red foreround"
        .relativeSize(3.5f)
        .backgroundColor(Color.BLACK)
        .foregroundColor(Color.RED)

val secondStyleString = " And now check out this text with a strikethrough style!"
        .strike()

val appendedString = firstStyleString + secondStyleString + " And a regular string! "
appendedString["regular"] = "GREEN".foregroundColor(Color.GREEN)

Another great way to clean up this particular API would be by wrapping the StringBuilder object in a DSL - but we’ll save that for another blog post. Its clear that Kotlin can help reshape some of Androids rougher API’s, and I for one am super excited to see what the community will build.