Your WorkerFactory could throw ClassNotFoundException if you rename, move or delete your Worker classes
Tags: android, workmanager, workerfactory, background
Published: 2019-11-08 17:03:17

## 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.

```kotlin
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](https://blog.stylingandroid.com/muselee-work-manager-part-2/).

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.

```kotlin
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.
```kotlin
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"
    )
}
```
```kotlin
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 default `WorkerFactory` does. Like the previous option, it would result in your worker being removed from the queue and data associated with it probably lost.
```kotlin
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! :)