I paired Glance Widget with Work Manager API to create a feature for my open source project SlimeKT, a Medium clone (GitHub). The result was interesting. I want to share my learnings and experience through this article.
Question: What’s the feature I was trying to build?
I tried to mock Medium’s Daily Read Reminder feature. The user is reminded daily at a particular time to read an article from the category/topic they have subscribed to.
⚒️Complexity level of Implementing this feature
Intermediate: Requires basic knowledge of Jetpack’s WorkManager, DataStore, Jetpack Compose, and Hilt Worker.
I would like to share my experience in the following areas,
- Understanding how Glance widget works by building our not-so-fancy UI.
- Registering the widget.
- Pairing the widget with Work Manager.
- Learning about GlanceStateDefinition and updating all instances of the Glance widget.
- The needs and steps to create a custom GlanceStateDefinition.
- Enqueuing the worker to run periodically.
- Bonus 🎁: Adding Material You support for Glance widget.
Let’s dissect how to implement this feature in-depth.
1. Adding required dependencies and understanding how the Glance widget works by building a simple UI.
Note: Glance can translate Composables into actual RemoteViewsand it requires Compose to be enabled as It depends on Runtime, Graphics, and Unit UI Compose layers. Still, it’s not directly interoperable with other existing Jetpack Compose UI elements. Learn More.
In short, Glance API has a set of UI elements that looks similar to Jetpack Compose API. If you didn’t get this point, check out the sample below.
- Create a Kotlin class, namely “MyWidget” which should extend GlanceAppWidget and override its Content function. Add a new Column composable and import it but make sure you see the import block below.
You may see that the Colum composable is imported from the Glance library. Also, we are not using the regular Modifier from Compose library; Instead, we are using the GlanceModifier from the Glance library.
Let’s add a Text composable (again, make sure to get it from the Glance library), and all of its parameters, such as modifier and style, should also be imported from the Glance library.
The UI Feel’s not so fancy? That’s fine 🙋♀️. Let’s move forward.
2. Register your widget receiver.
Create a Kotlin class, namely “MyWidgetReceiver” which should extend GlanceAppWidgetReceiver and implement its only non-optional member and instantiate your GlanceWidget.
You will need to register this receiver in your
AndroidManifest.xml. This gist contains all the necessary code. (You can also refer to the Android Manifest file of SlimeKT for a more robust example).
3. Pairing the widget with Work Manager.
The following code snippet is self-explanatory. We have requested our API to get an article from the user’s subscription, and as soon as we get the result, we need to update our Glance widget. Simple isn’t it?.
But wait, there’s a catch! How would you update your widget content from the worker? Don’t worry; we got it covered in the next part.
4. Learning about GlanceStateDefinition and updating the widget.
The Glance API has its state maintainer called GlanceStateDefinition, which utilizes Jetpack Datastore.
Jetpack DataStore is a data storage solution that allows you to store key-value pairs or typed objects with protocol buffers. DataStore uses Kotlin coroutines and Flow to store data asynchronously, consistently, and transactionally. Learn More.
Let’s go back to the MyWidget file and use GlanceStateDefinition to update our widget content.
- First step: Override
stateDefinitionand instantiate it with PreferencesGlanceStateDefinition. (A class that implements GlanceDefinitionState of type DataStore Preferences, and it comes out of the box from AndroidX Glance library) (Line number 3)
- Second step: Retrieve the current GlanceDefinitionState (of type DataStore Preferences) inside Content composable function by using
currentState<Preferences>()(Line number 7)
- Third step: Fetch the string stored inside of our DataStore Preferences by passing its unique key and default it to an empty string with the help of Elvis operator. (Line number 8)
Now we can update our widget by updating the string value in Datastore Preferences. For that, we need to access the Datastore instance. Glance library provides us with a public function
updateAppWidgetState which takes Glance ID (a GlanceAppWidget instance) as a parameter.
Let’s create a small utility function that updates our widget after retrieving all the available Glance IDs.
And Viola! We can call this function inside our worker and update the widget’s content.
But wait, I discovered a new issue 😲
When we create a new instance of the same widget, the content disappears! Don’t worry; we got it covered too in the next part.
5. The needs and steps to create a custom GlanceStateDefinition.
I asked Marcel (Developer Relations Engineer at Google) why the content disappears upon creating a new instance of the same widget while primarily Datastore is known to persist the data? He clarified that on every new instance of the widget, a brand new preference (Datastore) file is created, which is why the content is initially null. He further guided me that I should make my implementation of GlanceStateDefinition and share the same preference file. (to avoid the issue)
Let’s have a view at PreferencesGlanceStateDefinition (no pun intended). It is a class that implements GlanceDefinitionState of type DataStore Preferences, and it comes out of the box from the AndroidX Glance library.
You may see that a new file is created with the suffix
.preferences_pband the prefix is the
fileKey which probably changes on every new widget instance.
Again creating a custom GlanceDefinitionState that shares the same preferences file is pretty simple.
- Step 1: Extend your CustomState object class with the GlanceDefinitionState of type DataStore Preferences.
- Step 2: In the
getDataStoremethod, avoid the creation of a new preference file, ie, create a file with an immutable/fixed name.
- Step 3: Last but not least, return the location of the preference file.
Now you can use your Custom GlanceStateDefinition instead of the one provided by the library.
Note: If you have multiple Glance widgets, you should consider passing the preference file name in the constructor (of your custom state definition) to have separate preference files to avoid issues. If you still want to use a single preference file across multiple Glance widgets, make sure that the key of the preferences should not be the same.
6. Enqueuing the DailyReadWorker to run periodically.
If you have used the Work Manager library, you may know that we can perform a specific work one time or periodically. In this use case, we would need to enqueue a periodic worker that repeats after 24 hours.
7. Adding Material You Support (Android 12+)
Your widget background and text color can be adapted to Dynamic colors by adding a background modifier to Glance composable that accepts a color resource. You can create a color resource file inside res/colors.
Note: Wrapping up your Glance widgets inside of Jetpack Compose MaterialTheme composable won’t have any effect and is discouraged. Marcel Pintó has clarified more common doubts in his article, Demystifying Jetpack Glance for app widgets. Make sure to check it out.
That’s it. 🙋♂️ If you have any queries, feel free to reach me on Twitter. I would be more than happy to help you!
You can refer to this pull request for more information: https://github.com/kasem-sm/SlimeKT/pull/148
👍 A clap for this article would be glanceful, oops! Graceful. Thank You!