Illustration by Virginia Poltrack
Illustration by Virginia Poltrack

WorkManager meets Kotlin

Pietro Maggi
Android Developers
Published in
5 min readJun 12, 2019

--

Welcome to the third post of our WorkManager series. WorkManager is an Android Jetpack library that makes it easy to schedule deferrable, asynchronous tasks that must be run reliably. It is the current best practice for most background work on Android.

If you’re been following thus far, we’ve talked about:

In this blog post, I’ll cover:

WorkManager in Kotlin

The code snippets in this blog post are in Kotlin, using the KTX library (KoTlin eXtensions). The KTX version of WorkManager provides extension functions for more concise and idiomatic Kotlin. You can use the KTX version of WorkManager adding a dependency to the androidx.work:work-runtime-ktx artifact to your build.gradle file, as described on WorkManager’s release notes. This artifact includes CoroutineWorker and other helpful extension methods for WorkManager.

More concise and idiomatic

WorkManager’s KTX provides a sweeter syntax when you need to build a Data object to pass in or out a Worker class. In this case the Java syntax looks like:

Data myData = new Data.Builder()
.putInt(KEY_ONE_INT, aInt)
.putIntArray(KEY_ONE_INT_ARRAY, aIntArray)
.putString(KEY_ONE_STRING, aString)
.build();

In Kotlin we can do much better using the workDataOf helper function:

inline fun workDataOf(vararg pairs: Pair<String, Any?>): Data

This allows to write the previous Java expression as:

val data = workDataOf(
KEY_MY_INT to myIntVar,
KEY_MY_INT_ARRAY to myIntArray,
KEY_MY_STRING to myString
)

CoroutineWorker

In addition to the worker classes available in Java (Worker, ListenableWorker and RxWorker), there’s a Kotlin only class that uses Kotlin’s Coroutines for your Work.

The main difference between a Worker class and a CoroutineWorker is that the doWork() method in a CoroutineWorker is a suspend function and can run asynchronous tasks, while Worker’s doWork() can only execute synchronous talks. Another CoroutineWorker feature is that it automatically handles stoppages and cancellation while a Worker class needs to implement the onStopped() method to cover these cases.

For the full context on these different options you can refer to the Threading in WorkManager guides.

Here I want to focus on what is a CoroutineWorker, covering some small, but important differences, and then dive into how to test your CoroutineWorker classes using the new test features introduced in WorkManager v2.1.

As we wrote before, CoroutineWorker#doWork() is just a suspend function. This is, by default, launched on Dispatchers.Default:

class MyWork(context: Context, params: WorkerParameters) :
CoroutineWorker(context, params) {
override suspend fun doWork(): Result {return try {
// Do something
Result.success()
} catch (error: Throwable) {
Result.failure()
}
}
}

It’s important to understand that this is a fundamental difference when using a CoroutineWorker in place of a Worker or a ListenableWorker:

Unlike Worker, this code does not run on the Executor specified in your WorkManager’s Configuration.

As we just said, CoroutineWorker#doWork() defaults to Dispatchers.Default. You can customize this using withContext():

class MyWork(context: Context, params: WorkerParameters) :
CoroutineWorker(context, params) {
override suspend fun doWork(): Result = withContext(Dispatchers.IO) {return try {
// Do something
Result.success()
} catch (error: Throwable) {
Result.failure()
}
}
}

There is rarely the need to change the Dispatcher that your CoroutineWorker it’s using and Dispatchers.Default it’s a good option for most of the cases.

To learn more about how to use WorkManager with Kotlin, you can try out this codelab.

Testing Worker classes

WorkManager has a couple of additional artifacts to allow to easily test your Work. You can read more about these on WorkManager’s testing documentation page and the new “Testing with WorkManager 2.1.0” guide. The original implementation for this testing helper made possible to customize WorkManager so that it act synchronously and you can then use WorkManagerTestInitHelper#getTestDriver() to be able to simulate delays and test periodic work.

The key point here is that you’re modifying the behaviour or WorkManager to drive your Worker classes to make it possible to test them.

WorkManager v2.1 adds a new TestListenableWorkerBuilder functionality that introduce a new way to test your Worker classes.

This is a very important update for CoroutineWorker because with TestListenableWorkerBuilder you’re actually directly running your worker classes to test their logic.

@RunWith(JUnit4::class)
class MyWorkTest {
private lateinit var context: Context
@Before
fun setup() {
context = ApplicationProvider.getApplicationContext()
}
@Test
fun testMyWork() {
// Get the ListenableWorker
val worker =
TestListenableWorkerBuilder<MyWork>(context).build()
// Run the worker synchronously
val result = worker.startWork().get()
assertThat(result, `is`(Result.success()))
}
}

The important thing here is that the results of running CoroutineWorker are obtained synchronously and you can check directly that your Worker class logic behaves correctly

Using TestListenableWorkerBuilder you can pass input data to your Worker or set the runAttemptCount, something that can be useful to test retry logic inside your work.

As an example, if you’re uploading some data to a server, you may want to add some retry logic to take into consideration connectivity issues.

class MyWork(context: Context, params: WorkerParameters) :
CoroutineWorker(context, params) {
override suspend fun doWork(): Result {
val serverUrl = inputData.getString("SERVER_URL")
return try {
// Do something with the URL
Result.success()
} catch (error: TitleRefreshError) {
if (runAttemptCount <3) {
Result.retry()
} else {
Result.failure()
}
}
}
}

You can then test this retry logic in your tests using the TestListenableWorkerBuilder:

@Test
fun testMyWorkRetry() {
val data = workDataOf("SERVER_URL" to "http://fake.url")
// Get the ListenableWorker with a RunAttemptCount of 2
val worker = TestListenableWorkerBuilder<MyWork>(context)
.setInputData(data)
.setRunAttemptCount(2)
.build()
// Start the work synchronously
val result = worker.startWork().get()
assertThat(result, `is`(Result.retry()))
}
@Test
fun testMyWorkFailure() {
val data = workDataOf("SERVER_URL" to "http://fake.url")
// Get the ListenableWorker with a RunAttemptCount of 3
val worker = TestListenableWorkerBuilder<MyWork>(context)
.setInputData(data)
.setRunAttemptCount(3)
.build()
// Start the work synchronously
val result = worker.startWork().get()
assertThat(result, `is`(Result.failure()))
}

Conclusion

With the release of WorkManager v2.1 and the new features in workmanager-testing, CoroutineWorker really shines for its simplicity and functionality. Testing is now really easy and the overall experience in Kotlin is great.

If you’ve not yet tried CoroutineWorker and the other extensions included in the workmanager-runtime-ktx, I highly encourage you to try in your projects. When I’m working with Kotlin (which is always nowadays), this is my preferred way to use WorkManager!

I hope that you find this article useful and I’d love to hear about how you’re using WorkManager or what WorkManager’s feature can be better explained or documented.

You can reach me on Twitter @pfmaggi.

Resources

--

--