We will cover briefly:
- Custom WorkManager
- OneTimeWorkRequest
- PeriodicWorkRequest
- Write Tests for Workers (step2 and step3)
Custom WorkManager
There is detailed description here for WorkManager
As per the docs
By default, WorkManager configures itself automatically when your app starts. If you require more control of how WorkManager manages and schedules work, you can customise the WorkManager configuration.
- Install the dependencies inside
app.gradle
def work_version = "2.5.0"
implementation "androidx.work:work-runtime-ktx:$work_version"
androidTestImplementation "androidx.work:work-testing:$work_version"
- Remove the default initialiser from
AndroidManifest.xml
<provider
android:name="androidx.work.impl.WorkManagerInitializer"
android:authorities="${applicationId}.workmanager-init"
tools:node="remove" />
Create your application class and define your own custom configuration.
class TodoApplication() : Application(), Configuration.Provider {
override fun onCreate() {
super.onCreate()
// LATER PUT THE PERIODIC WORK REQUEST HERE
}
override fun getWorkManagerConfiguration(): Configuration {
return if (BuildConfig.DEBUG) {
Configuration.Builder().setMinimumLoggingLevel(Log.DEBUG).build()
}
else {
Configuration.Builder().setMinimumLoggingLevel(Log.ERROR).build()
}
}
}
Here, we use Configuration.Provider to extend our TodoApplication, and override the getWorkManagerConfiguration
In our case, we just configure the logging level. For the complete list of customisations available, see the Configuration.Builder()
OneTimeWorkRequest
This is how our folder structure looks like

For creating any worker, we need the following:
- Worker (this section) : Actual work you want to perform in the background
- WorkRequest (created in the viewmodel section): This represents a request to do some work.
- WorkManager (created above): Schedules your
WorkRequestand makes it run
We will start with OnDemandBackupWorker, which basically aims to save the data onto some backend, (for our demo, we fake in the network call)
This is then followed by FileWorker, which creates a file onto the device and appends the timestamp into the newly created file

OnDemandBackupWorker
- This class extends from the CoroutineWorker (provides interop with Kotlin Coroutines)
- We override the doWork function for our suspending work
override suspend fun doWork(): Result {
val appContext = applicationContext
showNotifications("Backing up the data", appContext)
return try {
val res = dummyWork()
val outputData = workDataOf(KEY_ONDEMANDWORKER_RESP to res)
Result.success(outputData)
} catch (throwable: Throwable) {
Timber.e(throwable)
Result.failure()
}
}
private suspend fun dummyWork(): String {
// Faking the network call
sleep()
return "Completed successfully!"
}
Here, we create a dummyWork function, (which puts the thread to sleep) and returns string result.
- The result is then put inside
workDataOf(this converts list of pairs to Data object) using a key (which should be a string) - This result/output is then passed onto Worker’s
Result.successthat indicates the work completed successfully - In case of any error, we call Worker’s
Result.failure
FileWorker
- This class extends from the Worker (performs work synchronously on a background thread)
- We override the doWork function for our synchronous work
override fun doWork(): Result {
return try {
val content="Backed up on ${dateFormatter.format(Date())}"
val outputUri = saveToFile(appContext, content)
val data=workDataOf(KEY_FILEWORKER_RESP to outputUri.toString())
Result.success(data)
} catch (throwable: Throwable) {
Timber.e(throwable)
Result.failure()
}
}
Here, we create a file using saveToFile, and put in the current timestamp as text into that file
- The result is then put inside
workDataOfand like before, we pass it into the Worker’sResult.success
OnDemandBackupViewModel
- This class is responsible for creating the work request and extends from the AndroidViewModel.
- We define two functions (beginBackup and cancelBackup) inside this class
internal fun beginBackup() {
val constraints = Constraints.Builder()
.setRequiresStorageNotLow(true)
.setRequiresBatteryNotLow(true)
.setRequiredNetworkType(NetworkType.CONNECTED)
.build()
var continuation = workManager.beginUniqueWork(
ONDEMAND_BACKUP_WORK_NAME,
ExistingWorkPolicy.KEEP,
OneTimeWorkRequest.from(OnDemandBackupWorker::class.java)
)
// BACKUP WORKER
val backupBuilder = OneTimeWorkRequestBuilder<OnDemandBackupWorker>()
backupBuilder.addTag(TAG_BACKUP)
backupBuilder.setConstraints(constraints)
continuation = continuation.then(backupBuilder.build())
// SAVE FILE WORKER
val saveInFile = OneTimeWorkRequest.Builder(FileWorker::class.java)
.setConstraints(constraints)
.addTag(TAG_FILE)
.build()
continuation = continuation.then(saveInFile)
continuation.enqueue()
}
internal fun cancelBackup() {
workManager.cancelUniqueWork(ONDEMAND_BACKUP_WORK_NAME)
}
- Specify the constraints under which the worker is supposed to run, (in our case we specify storage, battery and internet)
- We get an instance of workmanager inside our viewmodel class. Using this instance, we call the
beginUniqueWork
Since, we need to chain our work requests, hence we use
beginUniqueWork
- We give our worker a unique name (basically a string), specify the
ExistingWorkPolicywith options asKEEPand create a OneTimeWorkRequest from OnDemandBackupWorker class - The output from
beginUniqueWorkis a WorkContinuation. - Next, we create the WorkRequest Builder, using
OneTimeWorkRequestBuilder. We add the tag for this work, which is used to later identify the progress for this work - This request is added to the
WorkContinuation - We repeat the last two steps for our FileWorker
- Finally, our chain of work request chain needs to be added to the queue (in order to run on the background thread). This is done using
enqueue
Note: For cancelling a work request, we simply call the
cancelUniqueWorkwith the tag (used for creating the work)
Tracking Progress of WorkRequest
- Since we added tags for our Worker, we can utilise them to get the status of any
WorkRequest - It returns a
LiveDatathat holds aWorkInfoobject.WorkInfois an object that contains details about the current state of aWorkRequest
internal val backupDataInfo: LiveData<List<WorkInfo>> = workManager
.getWorkInfosByTagLiveData(TAG_BACKUP)
Here,
TAG_BACKUPis our tag, specified previously
PeriodicWorkRequest
Note: PeriodicBackupWorker is the same like FileWorker (only difference is the file content)
- This work executes multiple times until it is cancelled, with the first execution happening immediately or as soon as the given
Constraintsare met. - The next execution will happen during the period interval.
- Go to our Application class and instantiate the
PeriodicWorkRequest.Builder
val constraints = Constraints.Builder()
.setRequiresStorageNotLow(true)
.setRequiresBatteryNotLow(true)
.setRequiredNetworkType(NetworkType.UNMETERED)
.build()
val periodicBackup = PeriodicWorkRequestBuilder<PeriodicBackupWorker>(1, TimeUnit.DAYS)
.addTag(TAG_PERIODIC_BACKUP)
.setConstraints(constraints)
.build()
WorkManager.getInstance(this).enqueueUniquePeriodicWork(
PERIODIC_BACKUP_WORK_NAME,
ExistingPeriodicWorkPolicy.KEEP,
periodicBackup
)
- We specify the constraints as needed
- Using
PeriodicWorkRequestBuilder, we create ourPeriodicWorkRequest - The time interval is specified as once per day.
Periodic work has a minimum interval of 15 minutes. Also if your periodic work has constraints, it will not execute until the constraints are met, even if the delay between periods has been met.
- We enqueue our
PeriodicWorkRequestusingenqueueUniquePeriodicWork, keeping the work policy asKeep
The normal lifecycle of a PeriodicWorkRequest is
ENQUEUED -> RUNNING -> ENQUEUED
Write Tests for Workers
We will now write tests for our workers: OnDemandBackupWorker and PeriodicBackupWorker
OnDemandBackupWorkerTest
- Create a test class OnDemandBackupWorkerTest
- We annotate our class with
AndroidJUnit4This is the thing that will drive the tests for a single class.
@RunWith(AndroidJUnit4::class)
class OnDemandBackupWorkerTest {
private lateinit var context: Context
private lateinit var executor: Executor
@Before
fun setUp() {
context = ApplicationProvider.getApplicationContext()
executor = Executors.newSingleThreadExecutor()
}
@Test
fun testOnDemandBackupWorker() {
val worker = TestListenableWorkerBuilder<OnDemandBackupWorker> (context).build()
runBlocking {
val result = worker.doWork()
assertTrue(result is ListenableWorker.Result.Success)
}
}
}
- We initialise the context and executor inside our
setUpand write a single testtestOnDemandBackupWorker - Since, our OnDemandBackupWorker is a CoroutineWorker, we make use of
TestListenableWorkerBuilderwhich basically builds instances ofListenableWorkerused for testing. - Next, we call the
doWorkinside runBlocking, (it executes the test synchronously on the main thread, which we want for testing)

PeriodicBackupWorkerTest
- Create a test class PeriodicBackupWorkerTest
- Same as before, we initialise in
setUpmethod and write 2 tests
// TEST 1
@Test
fun testPeriodicBackUpWorker() {
val worker = TestWorkerBuilder<PeriodicBackupWorker>(
context = context,
executor = executor
).build()
val result = worker.doWork()
assertTrue(result is ListenableWorker.Result.Success)
}
- Since, our PeriodicBackupWorker is a Worker, we make use of
TestWorkerBuilderwhich basically builds instances ofWorkerused for testing. - Next, we call the
doWorkand assert for theResult.success
- For the second test, we check if the periodic work state is
ENQUEUED
// TEST 2
@Test
fun testIfPeriodicBackupRunning() {
WorkManagerTestInitHelper.initializeTestWorkManager(context)
val testDriver = WorkManagerTestInitHelper.getTestDriver(context)
val workManager = WorkManager.getInstance(context)
val constraints = Constraints.Builder()
.setRequiresStorageNotLow(true)
.setRequiresBatteryNotLow(true)
.setRequiredNetworkType(NetworkType.UNMETERED)
.build()
val request =
PeriodicWorkRequestBuilder<PeriodicBackupWorker> (repeatInterval=24, TimeUnit.HOURS)
.setConstraints(constraints)
.build()
workManager.enqueue(request).result.get()
with(testDriver) {
this?.setPeriodDelayMet(request.id)
this?.setAllConstraintsMet(request.id)
}
val workInfo = workManager.getWorkInfoById(request.id).get()
assertEquals(workInfo.state, WorkInfo.State.ENQUEUED)
}
- We make use of
WorkManagerTestInitHelperwhich helps initialiseWorkManagerfor testing. - Set the constraints, create a PeriodicWorkRequest and enqueue it using the workManager instance
- Next, we make use of
testDriverto make the constraints meet and assert if the status isENQUEUED

PeriodicBackupWorkerTest

Top comments (0)