Illustration by Virginia Poltrack
Illustration by Virginia Poltrack
Illustration by Virginia Poltrack

WorkManager meets Kotlin

Pietro Maggi
Jun 12, 2019 · 5 min read

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

More concise and idiomatic

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

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

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

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

Android Developers

The official Android Developers publication on Medium

Medium is an open platform where 170 million readers come to find insightful and dynamic thinking. Here, expert and undiscovered voices alike dive into the heart of any topic and bring new ideas to the surface. Learn more

Follow the writers, publications, and topics that matter to you, and you’ll see them on your homepage and in your inbox. Explore

If you have a story to tell, knowledge to share, or a perspective to offer — welcome home. It’s easy and free to post your thinking on any topic. Write on Medium

Get the Medium app

A button that says 'Download on the App Store', and if clicked it will lead you to the iOS App store
A button that says 'Get it on, Google Play', and if clicked it will lead you to the Google Play store