I am implementing my first Jetpack Glance powered app widget for a simple todo list app I'm working on. I'm following the guide at https://developer.android.com/jetpack/compose/glance and everything is fine until I get to the point where I need to update my widget to match the data updates that occured from within my application.
From my understanding of Manage and update GlanceAppWidget, one can trigger the widget recomposition by calling either of update, updateIf or updateAll methods on a widget instance from the application code itself. Specifically, calls to those functions should trigger the GlanceAppWidget.provideGlance(context: Context, id: GlanceId) method, which is responsible for fetching any required data and providing the widget content, as described on this snippet :
class MyAppWidget : GlanceAppWidget() {
override suspend fun provideGlance(context: Context, id: GlanceId) {
// In this method, load data needed to render the AppWidget.
// Use `withContext` to switch to another thread for long running
// operations.
provideContent {
// create your AppWidget here
Text("Hello World")
}
}
}
But in my case, it is not always working. Here is what I observed after a few tries :
- It always works when first adding the widget to my dashboard. Data is always fresh there.
- It always works the first time the update method is called from the application itself (I'm using
updateAll). The widget is updated and shows up-to-date data, - Then it does not work if I call the update method too soon after the last call. In this case the
provideGlancemethod is not triggered at all.
I then looked into the GlanceAppWidget source code and noticed it relies on an AppWidgetSession class :
/**
* Internal version of [update], to be used by the broadcast receiver directly.
*/
internal suspend fun update(
context: Context,
appWidgetId: Int,
options: Bundle? = null,
) {
Tracing.beginGlanceAppWidgetUpdate()
val glanceId = AppWidgetId(appWidgetId)
if (!sessionManager.isSessionRunning(context, glanceId.toSessionKey())) {
sessionManager.startSession(context, AppWidgetSession(this, glanceId, options))
} else {
val session = sessionManager.getSession(glanceId.toSessionKey()) as AppWidgetSession
session.updateGlance()
}
}
If a session is running, it will be used to trigger the glance widget update. Otherwise it will be started and used for the same purpose.
I noticed that my problem occurs if and only if only a session is running, which would explain why it doesn't occur if I give it enough time between updates call : there are no more running sessions (whatever it means exactly) and a new one needs to be created.
I tried digging further in Glance internals to understand why it does not work when using a running session, with no success so far. The only thing I noticed and thought is weird is that at some point the AppWidgetSession internaly uses a class called GlanceStateDefinition, that I didn't see mentionned on the official Android Glance guide but that a few other guides on the web use to implement a Glance widget (Though using alpha or beta versions of Jetpack Glance libs).
Does anyone has a clue on why it behaves like this ? Here is some more information, please let me know if you need something else. Thanks a lot !
- I use version 1.0.0 of the
androidx.glance:glance-appwidgetlib, released a few days ago, - I did not forget to add the
<receiver>tag in myAndroidManifest.xml, as well as the requiredandroid.appwidget.providerxml file in my res/xml folder. I think I've correctly done everything that is mentioned on the Glance setup page given I have no problem adding the widget on my home screen in the first place, - I use
SQLiteOpenHelperunder the hood to access my data, with a few helper classes of my creation on top of it, not usingRoomor any other ORM lib (I want to keep my application simple for now). - Here is what my
provideGlancemethod looks like :
override suspend fun provideGlance(context: Context, id: GlanceId) {
val todoDbHelper = TodoDbHelper(context)
val dbHelper = DbHelper(todoDbHelper.readableDatabase)
val todoDao = TodoDao(dbHelper)
val todos = todoDao.findAll()
provideContent {
TodoAppWidgetContent(todos)
}
}
The todoDao.findAll() returns a plain List (it relies on a helper function that runs on Dispatchers.IO so that the main thread is not blocked)
- I also don't use
Hiltor any other DI lib.
I spent a few more hours searching and found my answer :
It turns out I was wrong assuming that the
provideGlancemethod, or even theprovideContentshould be triggered again when calling any of the aforementionedupdatemethods. You can fetch some initialization data in there but you cannot rely on it to keep your widget updated, it is only called when no Glance session is currently running (When first adding the widget / when time has passed since adding it). Instead you can (/should) rely on the state of your Glance Widget.I think this concept of Glance state is very poorly explained in the guide, to say the least, so I'll give it a short try hoping to help people having the same problem as I did :
GlanceAppWidget.updatemethods are called from within the app, Glance will recompose your widget content using a fresh copy of the Glance stateGlanceStateDefinitioninstance to your class extending GlanceAppWidget. This instance is responsible for providing the type (class) of your state, as well as theDataStorethat Glance will use internally to get an updated version of the Glance stateA
DataStoreis aninterfacethat provides two abstracts methods for getting and updating data (More info here : DataStore). There are 2 implementations provided, Preferences DataStore and Proto DataStore. The first one is intended to replaceSharedPreferencesas a mean to store key-value pairs, and the second can be used to store typed objects.Most of the Glance tutorials I found on the web make use of the Preferences DataStore in their examples, but for my purpose I chose to implement my own version of a
DataStoreas a readonly proxy to my Dao object, as follows :The state definition in my class extending
GlanceAppWidgetlooks like this :Meaning I can now rely on the state of my Glance Widget instead of using my Dao class directly, by using the
currentState()method :It works like a charm now !
I intend to fill an issue regarding the lack of documentation regarding the Glance state and its relation to the concept of Datastore in the Glance guide.