Styling dynamic strings directly in Android xml
with HTML markup
Tags: android, html, xml
Published: 2021-07-09 14:50:28

In Android, Strings are one of the most common Objects when working with views that show plain text. While implementing user interfaces, it happens often that the text requires some styling. For styled Strings, we are required to use `CharSequences` instead. Android supports some [html tags](https://android.googlesource.com/platform/frameworks/base/+/master/core/java/android/text/Html.java#782) out of the box, and those can be defined in the xml string resource, for instance

```xml
<string name="lorem_ipsum">
   This is <font color="red">red</font> and this
   <b><i>bold and italic (nested); </i>this just bold</b>,
   <u>underlined</u>
</string>
```

and resolved into a `CharSequence` by calling `context.getText(stringRes: Int)` which takes care of all the supported HTML tags to style the text without us needing to do anything else

However, sometimes we need to build a part of the string dynamically. In order to do this, we need to mark the dynamic parts in the xml with placeholders in the form of `%{digit}${type}`, for instance `%1$s` is a string passed as first vararg and `%2$d` is a decimal number passed as second vararg in `String.format(text: String, vararg args: String)`, which is called to resolve the placeholders.

Nevertheless, things start getting complicated if we define *HTML tags together with dynamic placeholders* in xml string resources, for instance something like this

```xml
<string name="lorem_ipsum">
   This is <font color="red">red</font> and this
   <b><i>bold and italic (nested); </i>this just bold</b>, <u>underlined</u>
   and here the placeholder = %1s
</string>
```

If we take a look at the method signature of `String.format(text: String, vararg args: String)`, its first argument requires a String instead of a `CharSequence`. This means, the dynamic text placeholders will be correctly replaced, but our `CharSequence` has to be converted to `String`, throwing away its styling.

In order to deal with HTML markup, Android provides HtmlCompat. It requires that the string resource encodes its opening unsafe characters, namely: `'<'` , which becomes `'&lt;'`

```xml
<string name="lorem_ipsum">
   This is &lt;font color="red">red&lt;/font> 
   and this &lt;b>&lt;i>bold and italic (nested); &lt;/i>this just bold&lt;/b>,
   &lt;u>underlined&lt;/u> and here the placeholder = %1s
</string>
```

or alternatively, we can wrap the resource inside [CDATASections](https://developer.android.com/reference/org/w3c/dom/CDATASection) instead to the xml as follows:
```xml
<string name="lorem_ipsum">
<![CDATA[This is <font color="red">red</font> and this <b><i>bold and italic (nested); </i>this just bold</b>, <u>underlined</u> and here the placeholder = %1s]]>
</string>
```

In any case, given our dynamic placeholder text to be "placeholder1", we can get the expected result by using *HtmlCompat* as follows:

```kotlin
val text = context.getString(R.string.lorem_ipsum)
val dynamicText = String.format(text, "placeholder1") 
val dynamicStyledText = HtmlCompat.fromHtml(
      dynamicText,
      HtmlCompat.FROM_HTML_MODE_COMPACT
)

textView.text = dynamicStyledText
```
![Alphanumeric placeholder](https://cdn.hashnode.com/res/hashnode/image/upload/v1618866510391/M4k07za1h.png)

Although the code above seems to work robustly, the result differs if the dynamic placeholder text contains at least one unescaped HTML character, for instance:`<`, `>`, `&`, `\` or `"`, like in `<placeholder1>`, leading to the result below

![Placeholder containing '<'](https://cdn.hashnode.com/res/hashnode/image/upload/v1618864999291/-qc_fiWXO.png)

Yes, the placeholder just disappears. That's because characters must be escaped before calling `HtmlCompat.fromHtml()`. We solve that by encoding the placeholders before using *HtmlCompat*, like this

```kotlin
val text = context.getString(R.string.lorem_ipsum)
val encodedPlaceholder = TextUtils.htmlEncode("<placeholder1>")
val dynamicText = String.format(text, encodedPlaceholder) 
val dynamicStyledText = HtmlCompat.fromHtml(
      dynamicText,
      HtmlCompat.FROM_HTML_MODE_COMPACT
)

textView.text = dynamicStyledText
```

![Encoded string placeholder](https://cdn.hashnode.com/res/hashnode/image/upload/v1618866598534/qh8UYzcXY.png)

Although it works and it is the recommended way according to the [official documentation](https://developer.android.com/guide/topics/resources/string-resource.html#kotlin), I personally do not like any of the approaches before. Why?
1. You end up changing the xml string resource completely for the sake of using a dynamic text placeholder
2. You lose xml highlighting in the styled parts of the string resource and therefore, it is harder to read

![Html tags highlighting in String resources](https://cdn.hashnode.com/res/hashnode/image/upload/v1617637524706/FZ3xQvaQ_.png)

A better approach would be to create a method that can handle the original xml string resource with HTML tags and placeholders. In doing so, it does not matter whether the string resource contains HTML markup or not, the method simply handles the placeholders while keeping the style defined by the (existing, if any) HTML tags ... no need to either replace opening unsafe characters or add [CDATASections](https://developer.android.com/reference/org/w3c/dom/CDATASection).

And yes, it is possible with a bit of hackery. Let's see how.



## Digging into a better solution
We already know that using `context.getText(R.string.lorem_ipsum)` returns the string resource styled as a `CharSequence`. If the string resource has a placeholder, it will be shown the same as in the xml.

![step 1 - getText()](https://cdn.hashnode.com/res/hashnode/image/upload/v1618863719712/yGCqzo8SU.png)

We also know that `HtmlCompat.fromHtml()` processes "some" HTML tags. Its inverse method exists and does exactly the opposite: takes a `Spanned` object and converts it to a string with the corresponding HTML tags. The flag we pass to the method also matters: `HtmlCompat.TO_HTML_PARAGRAPH_LINES_INDIVIDUAL` also adds a new line at the end of the HTML string and we have to account for that. Therefore, we can get the desired HTML string as follows

```kotlin
// step 2 - toHtml()
val spannedString = SpannedString(styledString)
val htmlString = HtmlCompat.toHtml(
        spannedString,
        HtmlCompat.TO_HTML_PARAGRAPH_LINES_INDIVIDUAL
    )
        .substringBeforeLast('>')
        .plus(">")
```

which results into

![step 2 - toHtml()](https://cdn.hashnode.com/res/hashnode/image/upload/v1618863756166/KJo868I_S.png)

We've got the equivalent HTML string to the styled String of the first step so far. However, the final goal is to replace its placeholders with the corresponding values. As you might remember, I mentioned at the beginning of the article that we can use `String.format(text: String, vararg args: String)` for that. It would not work with a `CharSequence`, but that is why we converted it into its equivalent HTML string in the first place.

```kotlin
// step 3 - String.format()
val dynamicHtmlString = String.format(htmlString, args)
```

![step 3 - String.format()](https://cdn.hashnode.com/res/hashnode/image/upload/v1618863883796/F2E0L2pW-.png)

Just convert the HTML text into a CharSequence and we get the desired style. Remember to use `HtmlCompat.FROM_HTML_MODE_COMPACT`, since it is the inverse of the `HtmlCompat.TO_HTML_PARAGRAPH_LINES_INDIVIDUAL` we've previously used

```kotlin
// step 4 - fromHtml()
val result = HtmlCompat.fromHtml(
    dynamicStyledString,
    HtmlCompat.FROM_HTML_MODE_COMPACT
)
.removeSuffix("\n")   // fromHtml() adds a new line at the end
```

![step 4 - fromHtml()](https://cdn.hashnode.com/res/hashnode/image/upload/v1618863870460/BFVdHbZuq.png)

Well we are almost done... as we have seen at the beginning of this article, if the placeholders are `Strings` containing unsafe characters, they do not show up. Therefore, do not forget that we need to encode the string values that will substitute the placeholders. A Kotlin extension function following all the aforementioned steps would look like this

```kotlin
fun Context.getHtmlStyledText(
    @StringRes htmlStringRes: Int,
    vararg args: Any
): CharSequence {

    // step 0 - Encode string placeholders  
    val escapedArgs = args.map {
        if (it is String) TextUtils.htmlEncode(it) else it
    }.toTypedArray()

    // step 1 - getText()
    val styledString = Context.getText(htmlStringRes)

    // step 2 - toHtml()
    val spannedString = SpannedString(styledString)
    val htmlString = HtmlCompat.toHtml(
        spannedString,
        HtmlCompat.TO_HTML_PARAGRAPH_LINES_INDIVIDUAL
    )
        .substringBeforeLast('>')
        .plus(">")

    // step 3 - String.format()
    val dynamicStyledString = String.format(htmlString, *escapedArgs)

    // step 4 - fromHtml()
    return HtmlCompat.fromHtml(
        dynamicStyledString,
        HtmlCompat.FROM_HTML_MODE_COMPACT
    )
    .removeSuffix("\n")   //fromHtml() adds one new line at the end
}
```

## BONUS
The same idea applies to plural resources. Simply replace

```kotlin
// step 1 - getText()
val styledString = context.getText(R.string.lorem_ipsum)
```

with

```kotlin
// step 1 - getText()
val styledString = context.resources.getQuantityText(R.plural.lorem_ipsum, quantity)
```

You can find the corresponding [working gist for strings and plurals here](https://gist.github.com/sergio-sastre/371191e5067c73af747f3d0939e0db29)

Cover photo by [Markus Spiske](https://unsplash.com/@markusspiske?utm_source=unsplash&utm_medium=referral&utm_content=creditCopyText) on [Unsplash](https://unsplash.com/s/photos/html?utm_source=unsplash&utm_medium=referral&utm_content=creditCopyText)