Android widgets have evolved with Jetpack Compose, and the new Compose Glance API makes it easier to create widgets using a declarative approach. Sadly, there are notable differences between Compose for Apps and Compose for Widgets (Glance), where a lot less things are supported (probably due to the widget API limitations inside Android).

These differences can make development tricky, as I discovered during the development of my own widget. To bridge some gaps, I developed a few helper functions to make working with text styles, fonts, and colors in Glance more intuitive. In this article, I’ll briefly explain these utilities.

Converting Compose TextStyle to Glance

One of the key differences is how TextStyle is handled between Compose and Glance. To facilitate conversion, I wrote the following function:

fun androidx.compose.ui.text.TextStyle.toGlance(): androidx.glance.text.TextStyle =
    androidx.glance.text.TextStyle(
        color = ColorProvider(color),
        fontStyle = fontStyle?.toGlance(),
        fontSize = fontSize,
        fontWeight = fontWeight?.toGlance(),
        textAlign = textAlign.toGlance(),
        textDecoration = textDecoration?.toGlance(),
        fontFamily = fontFamily?.toGlance()
    )

This function simply maps a Compose TextStyle to a Glance TextStyle. Funnily enough, the conversion is completely fine, even though we have to use the original compose TextStylein our glance widget to even use this function.

I promise the other toGlance functions are a bit more interesting (weird!) from a programming point of view.

Handling Font Styles

FontStyle in Compose is different from Glance, requiring explicit mapping:

fun androidx.compose.ui.text.font.FontStyle.toGlance(): androidx.glance.text.FontStyle =
    when (value) {
        1 -> androidx.glance.text.FontStyle.Italic
        else -> androidx.glance.text.FontStyle.Normal
    }

I checked in the code, there are just these two values, anything else in "Invalid"

Similarly, for FontWeight:

fun androidx.compose.ui.text.font.FontWeight.toGlance(): androidx.glance.text.FontWeight =
    when (weight) {
        in 700..1000 -> androidx.glance.text.FontWeight.Bold
        in 500..699 -> androidx.glance.text.FontWeight.Medium
        else -> androidx.glance.text.FontWeight.Normal
    }

While normal Compose supports any weight in 1..1000, according to the check in the init-block, Glance only has three predefined weights. The ranges convert each possible weight to the most probably expected glance alternative.

Text Alignment Mapping

Since Justify, Unspecified, and Invalid are not part of the glance API, I decided to make it nullable, to enforce choosing a default, where it is necessary (e.g. ...toGlance() ?: androidx.glance.text.TextAlign.Start).

fun androidx.compose.ui.text.style.TextAlign.toGlance(): androidx.glance.text.TextAlign? =
    when (toString()) {
        "Left" -> androidx.glance.text.TextAlign.Left
        "Right" -> androidx.glance.text.TextAlign.Right
        "Center" -> androidx.glance.text.TextAlign.Center
        "Start" -> androidx.glance.text.TextAlign.Start
        "End" -> androidx.glance.text.TextAlign.End
        else -> null
    }

Also: TextAlign is weird: It appears to be a value class of Int, but then all valid values are numbered inside the companion object. This looks like it could have been an enum class!

Text Decorations

Text decorations underline and line-through are handled using their masks, while all other options are ignored. They simply do not exist on the glance side:

fun androidx.compose.ui.text.style.TextDecoration.toGlance(): androidx.glance.text.TextDecoration =
    when (mask) {
        0x1 -> androidx.glance.text.TextDecoration.Underline
        0x2 -> androidx.glance.text.TextDecoration.LineThrough
        else -> androidx.glance.text.TextDecoration.None
    }

Fun fact: you can use the + operator or the or infix function to combine the decorations!

Font Family Conversion

I have no clue why I did things for this one, but it sure needed conversion! I do remember that GenericFontFamily was the only FontFamily that had easily convertible differentiation for glance. This is probably not the most elegant solution, but it works:

fun androidx.compose.ui.text.font.FontFamily.toGlance(): androidx.glance.text.FontFamily =
    when (this) {
        is androidx.compose.ui.text.font.GenericFontFamily ->
            when (name) {
                "cursive" -> androidx.glance.text.FontFamily.Cursive
                "serif" -> androidx.glance.text.FontFamily.Serif
                "monospace" -> androidx.glance.text.FontFamily.Monospace
                else -> androidx.glance.text.FontFamily.SansSerif
            }
        is androidx.compose.ui.text.font.SystemFontFamily ->
            androidx.glance.text.FontFamily.SansSerif
        else ->
            androidx.glance.text.FontFamily.SansSerif
    }

Color Handling

To apply colors with Glance, I created this helper function, since androidx.glance.Image needs a ColorFilter instead of a Color:

fun androidx.compose.ui.graphics.Color.toColorFilter() =  
    androidx.glance.ColorFilter.tint(androidx.glance.unit.ColorProvider(color = this))

The ColorProvider() function call is marked as an error by the IDE, but I am pretty sure this is a mistake. While many things around the function are marked as library-internal, the function itself is not.

Handling Strings with Localization

Since Glance doesn’t provide a stringResource function, and LocalContext.current.getString is way harder to type:

@androidx.compose.runtime.Composable  
fun glanceString(@androidx.annotation.StringRes resId: Int, vararg formatArgs: Any): String =  
    androidx.glance.LocalContext.current.getString(resId, *formatArgs)

This should cover anything you can throw at it!

Conclusion

While Compose Glance makes widget development much nicer, there are a few annoying differences from standard Jetpack Compose. I hope that we can bridge the gap by using helper functions like these.