Search code examples
androidandroid-workmanagerglance-appwidget

How to inject parameters into Android Worker that are only known at runtime?


Goal

How can I pass myObject and myClass parameters to GlanceWorker below?

Background

I am using android-workmanager to run background tasks. These tasks are initialized from a home screen widget which I have added with Glance. As recommended by Google here (see “How do I fetch data?”) I use the worker to update the Glance composable which is started from an ActionCallback.

To update my widget, the worker constructor needs to take two extra parameters myObject and myClass (in addition to context and workerParameters). All examples that I have read so far have mentioned the use of a WorkerFactory in order to achieve this. This extra parameter is always provided to the factory at start-up or injected via the dagger graph.

However, in my case, the type of these extra parameters is dependent on which widget initiated the ActionCallback (each widget has its own extension of GlanceStateDefinition() and GlanceAppWidget()). Therefore I can only provide the extra parameters to the worker at runtime (as far as I'm aware) and don't know how to have it pre-provided to the custom WorkerFactory. To make it more complicated, one of my extra parameters uses a generic T.

abstract class Action<T : ShowPortfolioMenuState<T>>(
    val myObject: GlanceStateDefinition<T>,
    val myClass: Class<out WidgetAbstractClass>,
) : ActionCallback {

    override suspend fun onAction(
        context: Context,
        glanceId: GlanceId,
        parameters: ActionParameters,
    ) {
        val glanceIdInt = GlanceAppWidgetManager(context).getAppWidgetId(glanceId)
        val showMenu = parameters[ActionParameters.Key("showMenu")] ?: false

        val glanceWork =
            OneTimeWorkRequest.Builder(GlanceWorkerPortfolioChange::class.java)
                .setInputData(
                    workDataOf("GLANCE_ID" to glanceIdInt, "SHOW_MENU" to showMenu)
                ).build()

        WorkManager.getInstance(context)
            .beginUniqueWork("Worker Name",
                ExistingWorkPolicy.REPLACE,
                glanceWork
            ).enqueue()
    }
}
@HiltWorker
class GlanceWorker<T: ShowMenuState<T>> @AssistedInject constructor(
    @Assisted val context: Context,
    @Assisted workerParameters: WorkerParameters,
    val myObject: GlanceStateDefinition<T>, /*<--- how do i add to constructor*/
    val myClass: Class<out WidgetAbstractClass>, /*<--- how do i add to constructor*/
    ): CoroutineWorker(context, workerParameters) {
    
    override suspend fun doWork(): Result {
        val glanceIdInt = inputData.getInt("GLANCE_ID", -1)
        val glanceId = GlanceAppWidgetManager(context).getGlanceIdBy(glanceIdInt)
        val showMenu = inputData.getBoolean("SHOW_MENU", false)

        myClass.apply {
            updateAppWidgetState(context, myObject, glanceId) { state -> /*state is of type T*/
                state.updateShowMenu(showMenu)
            }
            update(context, glanceId)
        }
        return Result.success()
    }
}

Solution

  • A couple of things:

    1. Actions can't take parameters. Glance won't be able to instantiate the class and fail. Instead:
      1. Use the ActionParameters to pass the Class name as String and then use Class.forName(..) to get the type/instance.
      2. Use the GlanceAppWidget instance to get the state https://developer.android.com/reference/kotlin/androidx/glance/appwidget/GlanceAppWidget#(androidx.glance.appwidget.GlanceAppWidget).getAppWidgetState(android.content.Context,androidx.glance.GlanceId)
    2. You can do the same for the worker by passing it as workData

    Some workers examples:

    https://github.com/android/user-interface-samples/blob/main/AppWidget/app/src/main/java/com/example/android/appwidget/glance/weather/WeatherWorker.kt

    https://github.com/android/user-interface-samples/blob/main/AppWidget/app/src/main/java/com/example/android/appwidget/glance/image/ImageWorker.kt