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.