ylliX - Online Advertising Network
Widgets With Glance: Beyond String States

Widgets With Glance: Beyond String States


Image created by ChatGPT.

I have recently been working on an app (Pay Day: Earnings Time Tracker) 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!

For the purposes of this article I have used a simpler example 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:

A CoroutineWorker is used to update the state periodically. You could use any method of setting the widget state, the same principles apply.

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:

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:

Then, we can use kotlinx.serialization.json to encode and decode the model to a string when writing and reading from the state object:

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.

But what if we want a different method of serialization? Or a different storage location (rather than the default preferences file). The documentation discusses the standard way of saving and fetching states, but there is not much information on using a custom model instead of strings. There is an example in the platform-samples Github repository that includes a breif implementation, I am expanding on this here.

In order to be able to handle different widget states, I have extended my model to be a sealed interface and include different implementations for various states:

The next step is to create a custom GlanceStateDefinition , this gives us several advantages:

  1. We can set a custom serializer, in this case I am using kotlinx.serialization as in the previous step, but you could use whatever works in readFrom and writeTo for your existing architecture.
    Errors in serialization can be handled here.
  2. We can specify the DataStore file location. In this case I am using a new file for every widget by using the fileKey as part of the DataStore file location name (see getLocation in the below example code). If you only have only one state for all of your widgets of a specific type and want them all to rely on the same file, you can set the file location as a static value (this is the approach used by the platform-samples example).
    If you are using a different DataStore for each widget you need to create a new one with a DataStoreFactory, otherwise the DataStore can be created as a variable at the top level of the GlanceStateDefinition (as is done in platform-samples)
  3. You can create a specific DataStore implementation. The example I am using creates a standard androidx.datastore.core.DataStore with a custom Serializer but you could instead use a database or protobuf storage implementation instead depending on your usecase. I have not found many examples of this other than this StackOverflow answer where the user has created a database backed version of the DataStore.
    This could be a good option if for some reason you do not want to make your model serializable or if your data object is too large to store in a DataStore string based file.
  4. The final advantage is allowing us to abstract the implementation of the widget state away from the rest of the widget implementation. If the data source or serialization method changes, then this can be adjusted in one place without affecting the rest of the implementation.

The GlanceStateDefinition is implemented as follows:

Using the custom GlanceStateDefinition

We then need to override the stateDefinition in the GlanceAppWidget class so that the widget implementation uses that instead of the default PreferencesGlanceStateDefinition. This is as simple as:

override val stateDefinition = QuoteWidgetStateDefinition

Following that, every instance of updateAppWidgetState needs to set the definition argument:

updateAppWidgetState(... definition = QuoteWidgetStateDefinition,...)

We can see this in more detail in the examples below:

Fetching the custom state

In this code snippit we are setting the stateDefinition and when reading the current state, specifying the type we are expecting (WidgetState in this case). From here I can detect which class implementation is used and select the right composable to display. It’s type safe and much more readable than using key-value pairs or Json serialization.

Saving the custom state

When saving the state the updateAppWidgetState function call is updated to include the definition argument, and then the updateState lambda just needs to return the right type, no serialization is need at this point — this is all done by the custom GlanceStateDefinition . Again, it is type safe, and allows us more freedom to set loading and error behaviours.

So there it is, a custom GlanceStateDefinition, the data model can be as complex or as large as you like as long as you can either serialize it or store it. The code is more readable and state management is easy no matter how many widgets you have!

To see a full example, see my sample widget app:



Source link

Leave a Reply

Your email address will not be published. Required fields are marked *