Android WorkManager Tips and Pitfalls

Tips for successfully employing Android's WorkManager to schedule tasks.

Daniel Lifflander
Daniel Lifflander
Android WorkManager Tips and Pitfalls

Android's WorkManager is the newest and most streamlined way to work with scheduled background tasks for Android apps. It simplifies preceding frameworks into a cohesive package. WorkManager is very easy to use, but there are a couple of pitfalls to watch out for.

It's only been about a year since I began writing real Android code for a big production app, so WorkManager was available to me the first time I came across a project whose requirements included a deferred background task. However, the main codebase I've been working on is plenty old -- so I was able to see what the legacy solutions for background tasks looked like as previous developers had already implemented them. After browsing through those implementations, I quickly came to determine that I wanted to learn to use WorkManager rather than copy-pasting old code.

One of the things I like about WorkManager is how simple it is to set up and how few lines of code it requires. For a basic but functional implementation, you'll be working with just 3 objects: WorkManager (big surprise there), Worker, and WorkRequest.

WorkManager is the singleton instance of the library. You'll pass in your application's context and request an instance of it.

val workManager = WorkManager.getInstance(applicationContext)

To define what the actual work to be done is, you'll want to make a class that inherits from Worker. If you happen to be using KOIN for dependency injection, you'll probably want to also inherit from KoinComponent. This allows you to access your KOIN services right in the worker class, which is very convenient! If you aren't using KOIN, you can ignore trackerService and KoinComponent. (In the future, I'm interested in learning how to use Dagger as well)

class DanielsWorker(appContext: Context, workerParameters: WorkerParameters) : Worker(appContext, workerParameters), KoinComponent {
    private val trackerService: TrackerService by inject()
    override fun doWork(): Result {
        trackerDelegate.doSomethingAwesome()
        return Result.success()
    }
}

Now we have a unit of work defined. It's time to create a WorkRequest to define the schedule on which we want to run this work. In this case, we're going to use PeriodicWorkRequest which is the subclass of WorkRequest that allows us to request that the WorkManager run our task periodically. Kind of like using crontab, but with easier to remember configuration syntax :-)

val myWorkRequest = PeriodicWorkRequestBuilder<DanielsWorker>(
    1,               // Period quantity (min. 15 minutes)
    TimeUnit.HOURS,  // Period unit
    10,              // Flexibility quantity
    TimeUnit.MINUTES // Flexibility unit
    ).build()

This will run the work once per hour with 10 minutes of flexibility. If you need your work to run on a tighter schedule you would want to reduce the 3rd parameter. As a software engineer, I've never been a fan of strict schedules :-)

All we have left now is to queue the request up! For this app, I am running the queuing from the onCreate part of my base Application (in my case, subclassed from MultiDexApplication). Here are different ways to queue the request; the differences between these 2 have important ramifications.

// A
workManager.enqueue(myWorkRequest)

// B
workManager.enqueueUniquePeriodicWork(
    "MY_TAG",
    ExistingPeriodicWorkPolicy.KEEP,
    myWorkRequest
)

The first time I used WorkManager I simply used .enqueue and everything seemed to be working well. However, if you use .enqueue you ultimately will discover that each time your application is launched, and thus onCreate is called, another request is queued up in addition to any existing requests, even if they are periodic and are performing the same work.

enqueueUniquePeriodicWork solves this problem. You provide it with a tag, and it will ensure that no duplicates of your work are started. You can give it instructions on how to behave if it is trying to queue your work and it finds an existing worker with the same tag (KEEP and REPLACE are the 2 behaviors)

If you at some point used the basic .enqueue function you may have also discovered that it is difficult to cancel those background workers since you do not have a tag by which to refer to them. There is a cancelAllWork function, but the official docs warn against using it as it will cancel ALL work (presumably within the context of your app). This could interfere with other libraries and parts of your codebase.

After some digging, I discovered that there is a default tag given to workers if you do not specify one! It is, as a String, the full Java identifier of your Worker class.

com.daniel.core.worker.DanielsWorker

Here's how you can cancel WorkManager workers you've started without tags:

workManager.cancelAllWorkByTag("com.daniel.core.worker.DanielsWorker")

WorkManager provides many other features such as specifying conditions under which you want to run your work (having network connectivity is a common one).

Ultimately I've enjoyed using WorkManager and I'm happy to see it streamline scheduled and deferred background tasks. I'm looking forward to cleaning up some legacy code and replacing it with clean, concise implementations of WorkManager.