DEV Community

Cover image for Using WorkManager in Android
aseem wangoo
aseem wangoo

Posted on • Edited on

Using WorkManager in Android

In case it helped :)
Pass Me A Coffee!!

We will cover briefly:

  1. Custom WorkManager
  2. OneTimeWorkRequest
  3. PeriodicWorkRequest
  4. 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

Workers in the App
Workers in the App

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

FileWorker Output
FileWorker Output

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’s Result.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 a WorkInfo object. WorkInfo is an object that contains details about the current state of a WorkRequest 
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 our PeriodicWorkRequest 
  • 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 using enqueueUniquePeriodicWork, keeping the work policy as Keep 

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 test testOnDemandBackupWorker 
  • Since, our OnDemandBackupWorker is a CoroutineWorker, we make use of TestListenableWorkerBuilder which basically builds instances of ListenableWorker used for testing.
  • Next, we call the doWork inside runBlocking, (it executes the test synchronously on the main thread, which we want for testing)
OnDemandBackupWorkerTest
OnDemandBackupWorkerTest

PeriodicBackupWorkerTest

// 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 of Worker used for testing.
  • Next, we call the doWork and assert for the Result.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 initialise WorkManager 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 is ENQUEUED
PeriodicBackupWorkerTest
PeriodicBackupWorkerTest

Source code.

In case it helped :)
Pass Me A Coffee!!

Top comments (0)