Skip to content

Instantly share code, notes, and snippets.

@Zhuinden
Last active May 26, 2021 07:15
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save Zhuinden/54136120b15fdabd56aa164c20dc934f to your computer and use it in GitHub Desktop.
Save Zhuinden/54136120b15fdabd56aa164c20dc934f to your computer and use it in GitHub Desktop.
WorkManager Worker Assisted Injection for Constructor Injection
// assisted injected worker
class ActualWorker @AssistedInject constructor(
private val someDependency: SomeDependency,
@Assisted appContext: Context,
@Assisted workerParams: WorkerParameters
) : ListenableWorker(appContext, workerParams) {
@AssistedInject.Factory
interface Factory : AssistedWorkerFactory<ActualWorker>
override fun startWork(): ListenableFuture<Result> {
TODO("implement something")
}
}
// assisted worker factory (shared superclass for factories to allow map multibinding)
interface AssistedWorkerFactory<T : ListenableWorker> {
fun createWorker(appContext: Context, workerParams: WorkerParameters): T
}
// map multi-binding
@MustBeDocumented
@Target(
AnnotationTarget.FUNCTION,
AnnotationTarget.PROPERTY_GETTER,
AnnotationTarget.PROPERTY_SETTER
)
@Retention(AnnotationRetention.RUNTIME)
@MapKey
annotation class WorkerKey(val value: KClass<out ListenableWorker>)
@Module
abstract class ActualModule {
@Binds
@IntoMap
@WorkerKey(ActualWorker::class)
abstract fun actualWorkerFactory(actualWorkerFactory: ActualWorker.Factory): AssistedWorkerFactory<out ListenableWorker>
}
// the component
@Singleton
@Component(modules = [ActualModule::class, AssistedInjectionModule::class])
interface ApplicationComponent {
}
// the factory
class MyWorkerFactory @Inject constructor(
private val assistedWorkerFactories: Map<Class<out ListenableWorker>, @JvmSuppressWildcards Provider<AssistedWorkerFactory<out ListenableWorker>>>
) : WorkerFactory() {
override fun createWorker(
appContext: Context,
workerClassName: String,
workerParams: WorkerParameters
): ListenableWorker? {
val clazz = Class.forName(workerClassName) ?: return null // handle rename or deletion of worker
val factory =
assistedWorkerFactories[clazz] ?: assistedWorkerFactories.entries.firstOrNull {
clazz.isAssignableFrom(it.key)
}?.value ?: return null // delegate to default if not found
return factory.get().createWorker(appContext, workerParams)
}
}
@blakelee
Copy link

blakelee commented May 25, 2021

Line 54 should be wrapped in a try/catch since it's possible to have workers that are in the queue with classes that no longer exist. We found this issue at my company and have no idea how a user got into that state. It was causing app crashes after about 10 seconds of the app starting and the only way to get rid of them was to clear app data. With the try/catch in place it will try the default implementation assuming you add the below change also. The default implementation will just remove that work from the queue so then there is no crash.

For line 69, instead of throwing an error, if you return null, the WorkManager implementation will use the default method for fetching the factory. This is useful for workers that are not updated to use the assisted injection.

@Zhuinden
Copy link
Author

Line 54 should be wrapped in a try/catch since it's possible to have workers that are in the queue with classes that no longer exist. We found this issue at my company and have no idea how a user got into that state

If you rename a worker or put it in a different package. 🤔 that's something I didn't think about before now.

There is no line 69, where am I allowed to return null?

@blakelee
Copy link

blakelee commented May 26, 2021

Whoops, I messed up the line number. Here is the full code I was referring to

class AppWorkerFactory @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 {
                try {
                    Class.forName(workerClassName).isAssignableFrom(it.key)
                } catch (e: Exception) { 
                    false 
                }
            }

        // Return null to use default factory.
        return foundEntry?.value?.run {
            get().create(appContext, workerParameters)
        }
    }
}

It looks for a worker by class name in your factories. If it doesn't find a worker, it will return null and try and use the default implementation to find the worker. If the worker is not found at that point, that task will be dropped from the queue.

The theory I have in how our users got a class not found exception is that there are some users that have used the app a long time ago and stopped using it for >6 months. In that time we updated the code to remove an older worker. The user finally opened the app for the first time in who knows how long (likely since they saw the update notification) and it tried to run the worker by class name since it was still in the queue somehow.

If you look into the source code of WorkerFactory::createWorkerWithDefaultFallback you can see that it wraps its own Class.forName in a try/catch, which I think eventually leads to WorkerWrapper::setFailedAndResolve which removes the failed task.

@Zhuinden
Copy link
Author

I've updated my gist. It's unfortunate that it caused production issues due to not taking into account that FQN can change, while it is technically persisted as FQN to local storage (disk-level persistence). Thanks for the notice!

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment