Schedule Image Displaying In Glance Widget With Work Manager API

Glance accelerates the creation of application widgets for different surfaces such as home-screen, keyboards, etc. As it provides a handful of APIs to help us build the widget in Jetpack Compose style.
Prerequisites 🤔
There are some concepts you need to at least be familiar with before reading this article.
- Jetpack Compose
- Preferences Datastore
- Retrofit
- ViewModels
- WorkManager
Project Setup 😃
Now let’s start coding, create a new project and add the following dependencies in build.gradle (Module)
Sidenote: The source code is available at the end of the article.
And before we forget, add internet permission in the AndroidManifest.xml file.
<uses-permission android:name="android.permission.INTERNET" />
First, we will create a simple widget to see how we could build one, then we will connect it with WorkManager to start scheduling.
To create a widget, we need to do the following:
- Create a class that inherits from GlanceAppWidget
- Create a receiver class that inherits from GlanceAppWidgetReceiver
- Register our receiver & provide our widget meta-data.
Once you implement GlanceAppWidget you will then need to override a composable function called Content, and there is where you write how your widget UI would look like. Here I am using the Image composable to display an image from the drawable resource. It is worth mentioning that most of the Jetpack compose-related code that we use within the Content composable is imported from androidx.glance. You could also notice that we use an ImageProvider for the image composable instead of a painter (painterResource) in normal composable functions, as well as the modifier, here we use a GlanceModifier instead of a normal compose modifier.
After creating your widget receiver, you will have to connect your receiver to the widget by implementing the property glanceAppWidget.
<receiver
android:name=".extra.RemoteWidgetReceiver"
android:enabled="true"
android:exported="false">
<intent-filter>
<action android:name="android.appwidget.action.APPWIDGET_UPDATE" />
</intent-filter>
<meta-data
android:name="android.appwidget.provider"
android:resource="@xml/widget_provider" />
</receiver>
And finally, in your AndoirdManifest.xml file within your application tag register your receiver and add the intent filter and meta-data for the widget.
Though our widget is written in compose, we need to create some files in XML, things like how the widget would look initially and some attributes and configurations related to the widget. All these configurations are stored in widget_provider.xml file.
As you can see in widget_provider.xml we define some information about our widget. Before we explain some of these attributes, here is the code for the widget_initial_layout.xml file
In the following figures, we see how some of these attributes are used.


You can also take a look at all the possible attributes for the appwidget-provider here. Now let’s run the app and see our result so far.

After knowing how we can set up our widget, it is time to learn how we can schedule images for the widget. It is worth mentioning though that glance widget uses Preferences Datastore to persist data.
All we gonna do is that we gonna let the user choose a category, and upon this choice, we are going to get a random image every 15 minutes and display it in the widget.

Now let’s start writing our program from the bottom of the hierarchy (data)
Two functions, one for fetching the image object and the other one for getting image raw bytes from the image object.
Our preferences datastore class for getting and setting user category index into preferences.
Before checking out our Worker class, let’s see our MainScreenViewModel class.
We monitor changes to our preferences on viewModel instantiation, and on each update, we call the setWork function to begin scheduling our RemoteImageWorker class to run every 15 minutes. Additionally, we set a restriction to our worker that the network is necessary in order to enqueue the worker. Finally, we pass initial data to the worker class as well, which is the index of the category from our preference.
Too much going on innit? It’s completely the opposite, In glance, we don’t have rememberAsyncImagePainter from coil nor rememberGlidePainter from glide or any other libraries, so we have to handle the image url ourselves. In my approach, we get the inputData (category) that we passed from our view model. Then we query the API to get a random image object according to our category, and then we download that image from the regular property which holds a url to the image with a regular size. Finally, we write the image bytes into a file and clean up any previous images from any old scheduling. Once we are done with the process, we call the updateRemoteImageWidget function and pass the path of the image file to query every widget that descends from RemoteWidget and update the image file path in the widget preferences. Eventually, update every instance state of our widget which will make the widgets reload a new image file.
Now we should do some changes to our widget in order to reflect those updates from the worker
The currentState inline function allows us to retrieve the state that contains the data that we saved in the widget preferences. As we have already seen, glance widgets use the Preferences Datastore to persist data. As a result, every modification to filePathString that we submit from the worker class will result in a recomposition of our widget with the new path and a fresh image.
All the code you see in the Content function is executed once you select the widget and begin to dragging it. So all the update functionality in our worker class is only applied to a widget that is attached to a surface. But what happens if we run the application for the first time when we don’t have any widgets? Well, the image gets downloaded yes, but the filePathString is empty because the datastore file of a widget is produced only when the widget is created. Therefore, I made a simple function to retrieve a bitmap from a file path, and if the file path is empty (which will only happen the first time we add a widget) we load the jpg file from fileDir.
On every creation of an instance of the same widget, an new datastore file is created. So imagine if we have a widget then we try to add a new one, the first one will get the image but the second won’t. For that, we created CustomGlanceStateDefinition to pass the same datastore file to every instance of the widget. The code of the CustomGlanceStateDefinition is probably a boilerplate, so you’d always have the same code when you want to pass the same datastore file to every widget. Just don’t forget to override stateDefinition property and pass your CustomGlanceStateDefinition.
Finally, you could see in GlanceAppWidget constructor it takes an optional parameter, XML layout for the error state of the widget which will appear whenever an error occurs while setting up the widget.
Now let’s take a glance at our result (got it? 😁). Widget Image is changed every 15 mins according to our category, or once we change the category.

That’s all for this article, here is the full source code. If you liked it don’t forget to give a star⭐ to the repository and a clap 👏🏻 for this article.