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 :
updateAll
). The widget is updated and shows up-to-date data,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 !
androidx.glance:glance-appwidget
lib, released a few days ago,<receiver>
tag in my AndroidManifest.xml
, as well as the required android.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,SQLiteOpenHelper
under the hood to access my data, with a few helper classes of my creation on top of it, not using Room
or any other ORM lib (I want to keep my application simple for now).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)
Hilt
or any other DI lib.I spent a few more hours searching and found my answer :
It turns out I was wrong assuming that the provideGlance
method, or even the provideContent
should be triggered again when calling any of the aforementioned update
methods. 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.update
methods are called from within the app, Glance will recompose your widget content using a fresh copy of the Glance stateGlanceStateDefinition
instance to your class extending GlanceAppWidget. This instance is responsible for providing the type (class) of your state, as well as the DataStore
that Glance will use internally to get an updated version of the Glance stateA DataStore
is an interface
that 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 replace SharedPreferences
as 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 DataStore
as a readonly proxy to my Dao object, as follows :
class TodoDataStore(private val context: Context): DataStore<List<TodoListData>> {
override val data: Flow<List<TodoListData>>
get() {
val todoDbHelper = TodoDbHelper(context)
val dbHelper = DbHelper(todoDbHelper.readableDatabase)
val todoDao = TodoDao(dbHelper)
return flow { emit(todoDao.findAll()) }
}
override suspend fun updateData(transform: suspend (t: List<TodoListData>) -> List<TodoListData>): List<TodoListData> {
throw NotImplementedError("Not implemented in Todo Data Store")
}
}
The state definition in my class extending GlanceAppWidget
looks like this :
override val stateDefinition: GlanceStateDefinition<List<TodoListData>>
get() = object: GlanceStateDefinition<List<TodoListData>> {
override suspend fun getDataStore(
context: Context,
fileKey: String
): DataStore<List<TodoListData>> {
return TodoDataStore(context)
}
override fun getLocation(context: Context, fileKey: String): File {
throw NotImplementedError("Not implemented for Todo App Widget State Definition")
}
}
Meaning I can now rely on the state of my Glance Widget instead of using my Dao class directly, by using the currentState()
method :
override suspend fun provideGlance(context: Context, id: GlanceId) {
provideContent {
TodoAppWidgetContent(currentState())
}
}
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.