preface

Last time we did a comprehensive analysis of the application of Paging, this time we will talk about WorkManager.

If you are not familiar with Paging, it is recommended to read this article:

The application of Paging in RecyclerView, this article is enough

Originally, this article could have been released last week, but I have a characteristic of writing articles, I will combine specific Demo to elaborate, and the Demo of WorkManager has been completed long ago, but it takes time to explain it together with the article, which was delayed last week due to my own reasons, but it is easier to write code. 😿 😿

Oh, no more talking, get to the point!

WorkManager

What is a WorkManager? The official explanation is that it is very simple for the operation of deferred tasks and has strong stability. For asynchronous tasks, it can ensure the smooth execution of tasks even if the App exits or the device restarts.

So the key is simplicity and stability.

For ordinary use, if the APP suddenly exits or the phone is disconnected from the network during the execution of a background task, the background task will be terminated directly.

Typical scenario: the App’s focus function. If the user clicks the “follow” button under the condition of weak network, the user immediately quits the App for some reason, but the request to follow is not successfully sent to the server, the next time the user enters the App, he will still get the status information that he did not follow before. This creates operational bugs that degrade the user experience and add unnecessary actions to the user.

So how to solve it? It is very simple, look at the definition of WorkManager, using WorkManager can easily solve. I won’t extend the implementation code here, but it should be easy to implement as long as you continue through this article.

Of course, you can do this without using WorkManager, which brings us to its other benefit: simplicity. If you don’t use WorkManager, you need to differentiate between different API versions.

JobScheduler

val service = ComponentName(this, MyJobService::class.java)
val mJobScheduler = getSystemService(Context.JOB_SCHEDULER_SERVICE) as JobScheduler
val builder = JobInfo.Builder(jobId, serviceComponent)
 .setRequiredNetworkType(jobInfoNetworkType)
 .setRequiresCharging(false)
 .setRequiresDeviceIdle(false)
 .setExtras(extras).build()
mJobScheduler.schedule(jobInfo)
Copy the code

The JobScheduler creates a Job and executes it once the conditions are met. But JobScheduler is added at API21 and there is a system Bug in API21&22

This means that it can only be used with API23 and above

if (Build.VERSION.SDK_INT >= 23) {
	// use JobScheduler
}
Copy the code

Since JobScheduler can only be used with API23 and above, what about below API23?

AlarmManager & BroadcastReceiver

At this time, AlarmManager can be used for task execution below API23, and BoradcastReceiver can be used for task condition monitoring, such as network connection status and device startup.

We started out as a stable background task and ended up with version compatibility. Compatibility and implementation are further enhanced.

So is there a unified way to achieve it? Of course, it is WorkManager, and its core principles use the combination of the above analysis.

It automatically uses the best implementation for versioning, while also providing additional conveniences such as status listening, chained requests, and so on.

The use of WorkManager, WHICH I break down into the following steps:

  1. Build the Work
  2. Configuration WorkRequest
  3. Add to WorkContinuation
  4. Get response results

Now let’s go through the Demo step by step.

Build the Work

Each task of WorkManager is composed of Work, so Work is the core of specific task execution. Since it is the core, you might think it would be very difficult to implement, but on the contrary, it is very simple to implement, you just need to implement its doWork method. For example, let’s implement a Work that clears.png images in related directories

class CleanUpWorker(context: Context, workerParams: WorkerParameters) : Worker(context, workerParams) {

    override fun doWork(a): Result {
        val outputDir = File(applicationContext.filesDir, Constants.OUTPUT_PATH)
        if (outputDir.exists()) {
            val fileLists = outputDir.listFiles()
            for (file in fileLists) {
                val fileName = file.name
                if(! TextUtils.isEmpty(fileName) && fileName.endsWith(".png")) {
                    file.delete()
                }
            }
        }
        return Result.success()
    }
}
Copy the code

All the code is in doWork, and the implementation logic is simple: find the relevant directories, then determine if the files in each directory are.png images, and delete them if they are.

The above is the logical code, but the key point is that the return value result.success (), which is a Result type, can have three values

  1. Result. The success () : success
  2. Result. The failure () : failure
  3. Result. Retry () : try again

For success and failure, it also supports passing values of type Data, which is managed internally by a Map, so you can use workDataOf directly for Kotlin

return Result.success(workDataOf(Constants.KEY_IMAGE_URI to outputFileUri.toString()))
Copy the code

The value it passes will be put into OutputData, which can be passed in a chained request, with the final response result retrieved. The essence is that WorkManager combines Room to store data in a database.

So much for this step, let’s move on to the next step.

Configuration WorkRequest

WorkManager mainly uses WorkRequest to configure tasks, and its WorkRequest types include:

  1. OneTimeWorkRequest
  2. PeriodicWorkRequest

OneTimeWorkRequest

First, OneTimeWorkRequest applies to a one-time task, that is, the task is executed only once. Once the task is completed, it is terminated automatically. It’s also very simple to build:

val cleanUpRequest = OneTimeWorkRequestBuilder<CleanUpWorker>().build()
Copy the code

This configates the WorkRequest associated with the CleanUpWorker and is one-time.

There are other configurations you can add to WorkRequest as you configure it, such as adding tags, passing inputData, adding constraints, and so on.

val constraint = Constraints.Builder()
        .setRequiredNetworkType(NetworkType.CONNECTED)
        .build()
 
val blurRequest = OneTimeWorkRequestBuilder<BlurImageWorker>()
        .setInputData(workDataOf(Constants.KEY_IMAGE_RES_ID to R.drawable.yaodaoji))
        .addTag(Constants.TAG_BLUR_IMAGE)
        .setConstraints(constraint)
        .build()
Copy the code

The tag is added to label the data for subsequent results. The incoming inputData can be retrieved in BlurImageWork; Add a network connection constraint that indicates that the WorkRequest is triggered only when the network connection is in the state.

The core code of BlurImageWork is as follows:

override suspend fun doWork(a): Result {
    val resId = inputData.getInt(Constants.KEY_IMAGE_RES_ID, - 1)
    if(resId ! =- 1) {
        val bitmap = BitmapFactory.decodeResource(applicationContext.resources, resId)
        val outputBitmap = apply(bitmap)
        val outputFileUri = writeToFile(outputBitmap)
        return Result.success(workDataOf(Constants.KEY_IMAGE_URI to outputFileUri.toString()))
    }
    return Result.failure()
}
Copy the code

In doWork, InputData is used to retrieve the InputData data passed in to the above blurRequest. The image is then processed by Apply, and finally written to a local file using writeToFile and returned to the path.

Because space is limited, here is not a spread out, interested can view the source code

PeriodicWorkRequest

PeriodicWorkRequest performs tasks periodically and is used in a manner consistent with the configuration and OneTimeWorkRequest.

val constraint = Constraints.Builder()
        .setRequiredNetworkType(NetworkType.CONNECTED)
        .build()
 
// at least 15 minutes
mPeriodicRequest = PeriodicWorkRequestBuilder<DataSourceWorker>(15, TimeUnit.MINUTES)
        .setConstraints(constraint)
        .addTag(Constants.TAG_DATA_SOURCE)
        .build()
Copy the code

One caveat, though: its cycles are at least 15 minutes apart.

Add to WorkContinuation

Now that we have the WorkRequest configured, all we need to do is add it to the Work work chain for execution.

For a single WorkRequest, you can use the EnQueue method of the WorkManager directly

private val mWorkManager: WorkManager = WorkManager.getInstance(application)
 
mWorkManager.enqueue(cleanUpRequest)
Copy the code

If you want to use chain work, just call the beginWith or beginUniqueWork method. They are essentially instantiating a WorkContinuationImpl, just calling different constructors. And the final construction method is:

    WorkContinuationImpl(@NonNull WorkManagerImpl workManagerImpl,
            String name,
            ExistingWorkPolicy existingWorkPolicy,
            @NonNull List<? extends WorkRequest> work,
            @Nullable List<WorkContinuationImpl> parents) { }
Copy the code

The beginWith method simply passes in the WorkRequest

val workContinuation = mWorkManager.beginWith(cleanUpWork)
Copy the code

BeginUniqueWork allows us to create a unique chained request. It’s also easy to use:

val workContinuation = mWorkManager.beginUniqueWork(Constants.IMAGE_UNIQUE_WORK, ExistingWorkPolicy.REPLACE, cleanUpWork)
Copy the code

The first parameter sets the name of the chained request; The second parameter ExistingWorkPolicy sets the performance of the same name. It has three values:

  1. REPLACE: When there is an incomplete chain request with the same name, cancel and delete the original progress and rejoin the new chain request
  2. KEEP: When there is an outstanding chained request with the same name, the chained request remains unchanged
  3. APPEND: When there is an incomplete chain request with the same name, the new chain request is appended to the original subqueue, that is, the execution of the original chain request is not started until all the original chain request has been executed.

Either beginWith or beginUniqueWork returns a WorkContinuation object, through which we can add subsequent tasks to the chained request. For example, the above cleanUpRequest, blurRequest and saveRequest are executed in sequence as follows:

val cleanUpRequest = OneTimeWorkRequestBuilder<CleanUpWorker>().build()
val workContinuation = mWorkManager.beginUniqueWork(Constants.IMAGE_UNIQUE_WORK, ExistingWorkPolicy.REPLACE, cleanUpRequest)
 
val blurRequest = OneTimeWorkRequestBuilder<BlurImageWorker>()
        .setInputData(workDataOf(Constants.KEY_IMAGE_RES_ID to R.drawable.yaodaoji))
        .addTag(Constants.TAG_BLUR_IMAGE)
        .build()
 
val saveRequest = OneTimeWorkRequestBuilder<SaveImageToMediaWorker>()
        .addTag(Constants.TAG_SAVE_IMAGE)
        .build()
 
workContinuation.then(blurRequest)
        .then(saveRequest)
        .enqueue()
Copy the code

In addition to serial execution, parallelism is supported. For example, cleanUpRequest and blurRequest are processed in parallel and then serialized with saveRequest

val left = mWorkManager.beginWith(cleanUpRequest)
val right = mWorkManager.beginWith(blurRequest)
 
WorkContinuation.combine(arrayListOf(left, right))
        .then(saveRequest)
        .enqueue()
Copy the code

Note that if your WorkRequest is PeriodicWorkRequest, it does not support chained requests. To put it simply, a periodic task has no termination in principle, it is a closed loop, so there is no such thing as a chain.

Get response results

This is the last step to get the response result, WorkInfo. WorkManager supports two ways to get response results

  1. The Request id: WorkRequest id
  2. Tag.name: indicates the Tag set in WorkRequest

The WorkInfo returned also supports the LiveData data format.

For example, now we want to listen to the status of the blurRequest and saveRequest mentioned above using a tag:

// ViewModel
internal val blurWorkInfo: LiveData<List<WorkInfo>>
get() = mWorkManager.getWorkInfosByTagLiveData(Constants.TAG_BLUR_IMAGE)
 
internal val saveWorkInfo: LiveData<List<WorkInfo>>
get() = mWorkManager.getWorkInfosByTagLiveData(Constants.TAG_SAVE_IMAGE)
 
// Activity
private fun addObserver(a) {
    vm.blurWorkInfo.observe(this, Observer {
        if (it == null || it.isEmpty()) return@Observer
        with(it[0]) {
            if(! state.isFinished) { vm.processEnable.value =false
            } else {
                vm.processEnable.value = true
                val uri = outputData.getString(Constants.KEY_IMAGE_URI)
                if(! TextUtils.isEmpty(uri)) { vm.blurUri.value = Uri.parse(uri) } } } }) vm.saveWorkInfo.observe(this, Observer {
        saveImageUri = ""
        if (it == null || it.isEmpty()) return@Observer
        with(it[0]) { saveImageUri = outputData.getString(Constants.KEY_SHOW_IMAGE_URI) vm.showImageEnable.value = state.isFinished && ! TextUtils.isEmpty(saveImageUri) } }) ...... . }Copy the code

Let’s look at another one obtained by id:

	// ViewModel
    internal val dataSourceInfo: MediatorLiveData<WorkInfo> = MediatorLiveData()
  
    private fun addSource(a) {
        val periodicWorkInfo = mWorkManager.getWorkInfoByIdLiveData(mPeriodicRequest.id)
        dataSourceInfo.addSource(periodicWorkInfo) {
            dataSourceInfo.value = it
        }
    }
    
    // Activity
    private fun addObserver(a) {
        vm.dataSourceInfo.observe(this, Observer {
            if (it == null) return@Observer
            with(it) {
                if (state == WorkInfo.State.ENQUEUED) {
                    val result = outputData.getString(Constants.KEY_DATA_SOURCE)
                    if(! TextUtils.isEmpty(result)) { Toast.makeText(this@OtherWorkerActivity, result, Toast.LENGTH_LONG).show()
                    }
                }
            }
        })
    }
Copy the code

Is it easy to use with LiveData? The essence of the WorkInfo fetch is to get it by manipulating the Room database. As mentioned in the Work section of the article, the data passed after the Work task is performed is saved in the Room database.

So the integration of WorkManager with AAC is very high, and the goal is to provide us developers with a complete framework, and also to illustrate the importance Google attaches to THE AAC framework.

If you are not familiar with AAC, I recommend you read my previous article

Room

LiveData

Lifecycle

ViewModel

Finally, let’s combine the above workRequests to see what they look like:

Hopefully, this article will familiarize you with working with WorkManager. If this article is helpful to you, you can easily like, follow a wave, this is the biggest encouragement to me!

The project address

Android excerption

The purpose of this library is to fully analyze the knowledge points related to Android with detailed Demo, to help readers to grasp and understand the main points explained faster

Android excerption

blog