WorkManager meets Kotlin
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:
- Using WorkManager in Kotlin
CoroutineWorker
class- How to test your
CoroutineWorker
classes usingTestListenableWorkerBuilder
.
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 theExecutor
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 yourCoroutineWorker
it’s using andDispatchers.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.