Fluent text styling and concatenation in Android

What if styling spans in Android was fluent and joining them as simple String concatenation?

The fluent span API and it’s result

Spans and Spannables are the fundamental abstraction for text styling and markup in Android. A thorough breakdown of how they work can be seen in detail here:

The issue with spans in Android however is how difficult they can be to create. The SpannableStringBuilder class helps with this significantly, especially when combined with the available KTX extensions. It still remains tedious to work with however, as each bit of text that needs to be styled, still has to have a span appended to it, before adding it to the SpannableStringBuilder . This typically goes something like this:

While this DSL-like API is much better than what we had before, it’s still a bit cumbersome and verbose. It’s nowhere quite as natural or simple String concatenation or using the String.format() method. Ideally, we should be able to create styled spans and join them arbitrarily in the ways that are most natural, i.e, like they were ordinary Strings .

First, let’s start with creating arbitrary styled spans from simple Strings . A suitable point of abstraction for this is the CharSequence interface; using Kotlin extensions, we can create spanned CharSequence instances from raw Strings by marking them up with spans via a SpannableStringBuilder . This is done by first creating an empty SpannableStringBuilder , opening Spannable tags on it with the Spanned.SPAN_MARK_MARK constant, appending the bit of text that needs markup, then finally closing the tags with the Spanned.SPAN_EXCLUSIVE_EXCLUSIVE constant.

In the snippet above, an arbitrary amount of tags (spans) can be applied to a CharSequence , and a SpannableStringBuilder is returned with them applied. When combined with the many implementations of spans in the android.text.style.* package, applying styles to text can be as easy as the following:

The above can also be chained to create a fluent API, so bolding, italicizing, and underlining a raw String can be:

"Hi! I am bold, italicized and underlined"

.bold().italic().underline()

Which will create a new SpannableStringBuilder for each invocation, and apply the specified style. If you want to reduce the number of SpannableStringBuilders created, you could apply the spans yourself by calling:

"Hi! I am bold, italicized and underlined".applyStyles(

StyleSpan(Typeface.BOLD),

StyleSpan(Typeface.ITALIC),

UnderlineSpan()

)

Losing a bit of fluency for some efficiency, which is a sensible tradeoff to make at your discretion. Also, if you created a custom Span, the above is the route to adding it to the fluent API via your own extension method.

Output of the code snippets above

With that done, the next step is creating a version of String.format() that preserves styles that are encoded in whatever spans of the format arguments.

CharSequence formatting that preserves styling

The above makes some common Android tasks easier. Terms and conditions in your login flow?

"Please accept the %1\$s and %2\$s before continuing".formatSpanned(

"terms".underline().click { /*go to link*/ },

"conditions".underline().click { /*go to link*/ }

)

Where the actual string values in the above should be gotten from resources to allow for easy internationalization.

Easy terms and conditions span creation

Finally, to round out the API, an operator plus overload for concatenating styled CharSequences can be done. Given spanned Strings A and B, concatenating them would be the application of the format replacements to “%1\$s%2\$s” , where the first and second arguments are A and B respectively.

Operator overloading allowing for CharSequence concatenation

A decidedly contrived example of which would resemble something like this:

"This".bold() +

" last " +

"paragraph".italic() +

" is a " +

"flex".underline() +

" to show the " +

"plus".bold().italic().color(color).scale(1.8f) +

" operator overload".color(color)

Appending spans easily by concatenating them