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:
- 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 inreadFrom
andwriteTo
for your existing architecture.
Errors in serialization can be handled here. - 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 theDataStore
file location name (seegetLocation
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 filelocation
as a static value (this is the approach used by the platform-samples example).
If you are using a differentDataStore
for each widget you need to create a new one with aDataStoreFactory
, otherwise theDataStore
can be created as a variable at the top level of theGlanceStateDefinition
(as is done in platform-samples) - You can create a specific DataStore implementation. The example I am using creates a standard
androidx.datastore.core.DataStore
with a customSerializer
but you could instead use a database orprotobuf
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 theDataStore
.
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 aDataStore
string based file. - 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: