Widgets With Glance: Beyond String States
Widgets With Glance: Beyond String States ę´ë ¨
Note
This the next in my series of blog posts all about widgets. Check out Widgets with Glance: Blending in (proandroiddev
) and Widgets with Glance: Standing out (proandroiddev
) for some Widget UI tricks and tips.
I have recently been working on an app (Pay Day: Earnings Time Tracker (dev.veryniche.buckaroo
)) that includes a lot of widgets that show different types of data, but very quickly I came across a problem. The standard way of passing data to a widget uses PreferencesGlanceStateDefinition
 to manage the state. The way of setting state is using key & value pairs where the values are always strings
. In my app I also needed enums
 & float
 values and was constantly converting to and from strings for many different data arguments and many different widget implementations. This became hard to manage and hard to read and a reusable and type safe solution was required.
I had read about using a CustomGlanceStateDefinition
 but I couldnât find much about it in the official documentation so here is my deep dive to hopefully help anyone else struggling with managing complex GlanceWidget
 state!
Basic widget state
For the purposes of this article I have used a simpler example (KatieBarnett/MotivateMe
)Â that just displays a text quote. While this example probably could get away with just using the string based values, adding some structure to the model can enable better loading and error states.
The starting point just sets a topic and quote as strings:
class QuoteWidget : GlanceAppWidget(errorUiLayout = R.layout.widget_error_layout) {
companion object {
val KEY_TOPIC = stringPreferencesKey("topic")
val KEY_QUOTE = stringPreferencesKey("quote")
}
override suspend fun provideGlance(context: Context, id: GlanceId) {
provideContent {
// Fetch the state
val displayText = currentState(KEY_QUOTE) ?: "Quote not found"
val topic = currentState(KEY_TOPIC) ?: ""
// Use the state
// ...
}
}
}
class QuoteWidgetWorker(...) : CoroutineWorker(context, params) {
override suspend fun doWork(): Result {
// ...
appWidgetManager.getGlanceIds(QuoteWidget::class.java).forEach { glanceId ->
// ...
// Update the widget with the new state
updateAppWidgetState(context, glanceId) { prefs ->
prefs[KEY_QUOTE] = newQuote?.text ?: "Quote not found"
prefs[KEY_TOPIC] = topicName
}
// Let the widget know there is a new state so it updates the UI
QuoteWidget().update(context, glanceId)
}
return Result.success()
}
}
AÂ CoroutineWorker
is used to update the state periodically. You could use any method of setting the widget state, the same principles apply.
A custom state model with Json Serialization
So this works well if the state is fairly straightforward and is just represented as simple strings, but what if we want a more complex model?
My first attempt to use a more complex model, I started by serializing the model to Json
.
Using my QuoteWidget
 example, a better model might be:
data class WidgetState(
val topicName: String,
val quote: Quote,
)
data class Quote(
val text: String
)
Then, we can serialize the model as Json
 and then use that as the string value in the widget.
The first step is to use kotlinx.serialization
 to serialize the data model:
@Serializable
data class WidgetState(
val topicName: String,
val quote: Quote,
)
@Serializable
data class Quote(
val text: String
)
Then, we can use kotlinx.serialization.json
to encode and decode the model to a string when writing and reading from the state object:
class QuoteWidget : GlanceAppWidget(errorUiLayout = R.layout.widget_error_layout) {
companion object {
val KEY_STATE = stringPreferencesKey("state")
}
override suspend fun provideGlance(context: Context, id: GlanceId) {
provideContent {
// Fetch the state
val state = currentState(KEY_STATE)
val item = try {
state?.let {
Json.decodeFromString<WidgetState>(state)
}
} catch (e: Exception) {
null
}
// Use the state
...
}
}
}
class QuoteWidgetWorker(/* ... */) : CoroutineWorker(context, params) {
// ...
// Update the widget with the new state
updateAppWidgetState(context, glanceId) { prefs ->
val newState = WidgetState(...)
prefs[KEY_STATE] = Json.encodeToString(newState)
}
//...
}
This is pretty good, we can easily fetch and save the model as long as it serializes well. We do have to handle any encoding or decoding errors and respond as needed.