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
WorkRequest
and 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.success
that 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
workDataOf
and 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
ExistingWorkPolicy
with options asKEEP
and create a OneTimeWorkRequest from OnDemandBackupWorker class - The output from
beginUniqueWork
is 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
cancelUniqueWork
with 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
LiveData
that holds aWorkInfo
object.WorkInfo
is an object that contains details about the current state of aWorkRequest
internal val backupDataInfo: LiveData<List<WorkInfo>> = workManager
.getWorkInfosByTagLiveData(TAG_BACKUP)
Here,
TAG_BACKUP
is 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
Constraints
are 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
PeriodicWorkRequest
usingenqueueUniquePeriodicWork
, 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
AndroidJUnit4
This 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
setUp
and write a single testtestOnDemandBackupWorker
- Since, our OnDemandBackupWorker is a CoroutineWorker, we make use of
TestListenableWorkerBuilder
which basically builds instances ofListenableWorker
used for testing. - Next, we call the
doWork
inside 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
setUp
method 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
TestWorkerBuilder
which basically builds instances ofWorker
used for testing. - Next, we call the
doWork
and 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
WorkManagerTestInitHelper
which helps initialiseWorkManager
for testing. - Set the constraints, create a PeriodicWorkRequest and enqueue it using the workManager instance
- Next, we make use of
testDriver
to make the constraints meet and assert if the status isENQUEUED
Top comments (0)