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
provideGlance
method 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-appwidget
lib, released a few days ago, - I did not forget to add the
<receiver>
tag in myAndroidManifest.xml
, as well as the requiredandroid.appwidget.provider
xml 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
SQLiteOpenHelper
under the hood to access my data, with a few helper classes of my creation on top of it, not usingRoom
or any other ORM lib (I want to keep my application simple for now). - Here is what my
provideGlance
method 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
Hilt
or any other DI lib.