Background
Most Android developers have come across WorkManager and have used it at least once, but in case you haven't it's a powerful and useful framework for handling the execution of background tasks with constraints.
By default the Android system creates your workers based on the WorkRequest you provided to it e.g.OneTimeWorkRequest
or PeriodicWorkRequest
, through its own WorkerFactory
.
However in certain situations you may want to use your own WorkerFactory instead of the default one provided by the framework. One such situation is when you want to inject dependencies into your worker using Dagger. The following is a common code sample given when integrating your worker with Dagger.
class WorkerFactory @Inject constructor(
private val workerFactories: Map<Class<out ListenableWorker>, @JvmSuppressWildcards Provider<ChildWorkerFactory>>
) : WorkerFactory() {
override fun createWorker(
appContext: Context,
workerClassName: String,
workerParameters: WorkerParameters
): ListenableWorker? {
val foundEntry =
workerFactories.entries.find { Class.forName(workerClassName).isAssignableFrom(it.key) }
val factoryProvider = foundEntry?.value
?: throw IllegalArgumentException("unknown worker class name: $workerClassName")
return factoryProvider.get().create(appContext, workerParameters)
}
}
You would then set this as the WorkerFactory for your app as described here.
That's cool, what's the problem?
The problem
When the Android system wants to start your worker it uses the class name, seen above as the parameter workerClassName
in the createWorker
function.
package me.rojan.worker
import android.content.Context
import androidx.work.Worker
import androidx.work.WorkerParameters
import javax.inject.Inject
class ExampleWorker @Inject constructor(
appContext: Context,
params: WorkerParameters
) : Worker(appContext, params) {
override fun doWork(): Result {
//Do work
return Result.success()
}
}
For the above worker, the class name would be me.rojan.worker.ExampleWorker
, your code in WorkerFactory
would then try to find this class and then create the worker.
Let's assume the device is not currently idle and we enqueue our worker with the constraint. setRequiresDeviceIdle(true)
. So in this case it's only going to be created and executed when the device has been deemed idle by the system.
What if we then install an update over our app that has renamed/moved/delete our worker class in the code? Let's rename our worker to me.rojan.worker.ExampleWorker2
and update the app.
Can you guess what might happen when your device is deemed idle by the Android system? It's going to invoke createWorker
on your WorkerFactory
with the workerClassName
parameter as me.rojan.worker.ExampleWorker
. This is the same value that was provided when enqueuing the worker prior to updating the app. But wait! We renamed this class, the WorkerFactory
is not going to be able to find your class and create your worker in the updated version of the app. It's going to crash!
Caused by java.lang.ClassNotFoundException: me.rojan.worker.ExampleWorker
at java.lang.Class.classForName(Class.java)
at java.lang.Class.forName + 453(Class.java:453)
at java.lang.Class.forName + 378(Class.java:378)
at me.rojan.worker.WorkerFactory.createWorker + 20(WorkManagerFactory.java:20)
at androidx.work.WorkerFactory.createWorkerWithDefaultFallback + 81(WorkerFactory.java:81)
at androidx.work.impl.WorkerWrapper.runWorker + 228(WorkerWrapper.java:228)
at androidx.work.impl.WorkerWrapper.run + 127(WorkerWrapper.java:127)
at androidx.work.impl.utils.SerialExecutor$Task.run + 91(SerialExecutor.java:91)
at java.util.concurrent.ThreadPoolExecutor.processTask + 1187(ThreadPoolExecutor.java:1187)
at java.util.concurrent.ThreadPoolExecutor.runWorker + 1152(ThreadPoolExecutor.java:1152)
at java.util.concurrent.ThreadPoolExecutor$Worker.run + 641(ThreadPoolExecutor.java:641)
at java.lang.Thread.run + 784(Thread.java:784)
What are our options?
- Map
workerClassName
of an old class name to their new names. If that fails to find the class, let's return null which will dequeue the worker but this will probably result in your worker and any data associated with it lost.
object WorkerClassNameFixer {
fun fixIfRequired(className: String): String {
return when (className) {
exampleWorkerClassName.first -> exampleWorkerClassName.second
else -> className
}
}
private val exampleWorkerClassName = Pair(
"me.rojan.worker.ExampleWorker",
"me.rojan.worker.ExampleWorker2"
)
}
class WorkerFactory @Inject constructor(
private val workerFactories: Map<Class<out ListenableWorker>, @JvmSuppressWildcards Provider<ChildWorkerFactory>>
) : WorkerFactory() {
override fun createWorker(
appContext: Context,
workerClassName: String,
workerParameters: WorkerParameters
): ListenableWorker? {
val clazz = try {
val fixedWorkerClassName = WorkerClassNameFixer.fixIfRequired(workerClassName)
Class.forName(fixedWorkerClassName)
} catch (e: ClassNotFoundException) {
return null
}
val foundEntry = workerFactories.entries.find { clazz.isAssignableFrom(it.key) }
val factoryProvider = foundEntry?.value
?: throw IllegalArgumentException("unknown worker class name: $workerClassName")
return factoryProvider.get().create(appContext, workerParameters)
}
}
- Alternatively, we can catch the
ClassNotFoundException
and return null. This is what the defaultWorkerFactory
does. Like the previous option, it would result in your worker being removed from the queue and data associated with it probably lost.
class WorkerFactory @Inject constructor(
private val workerFactories: Map<Class<out ListenableWorker>, @JvmSuppressWildcards Provider<ChildWorkerFactory>>
) : WorkerFactory() {
override fun createWorker(
appContext: Context,
workerClassName: String,
workerParameters: WorkerParameters
): ListenableWorker? {
val clazz = try {
Class.forName(workerClassName)
} catch (e: ClassNotFoundException) {
return null
}
val foundEntry = workerFactories.entries.find { clazz.isAssignableFrom(it.key) }
val factoryProvider = foundEntry?.value
?: throw IllegalArgumentException("unknown worker class name: $workerClassName")
return factoryProvider.get().create(appContext, workerParameters)
}
}
Feel free to share your own approaches or opinions! :)
Top comments (0)