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:
- Build the Work
- Configuration WorkRequest
- Add to WorkContinuation
- 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
- Result. The success () : success
- Result. The failure () : failure
- 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:
- OneTimeWorkRequest
- 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:
- REPLACE: When there is an incomplete chain request with the same name, cancel and delete the original progress and rejoin the new chain request
- KEEP: When there is an outstanding chained request with the same name, the chained request remains unchanged
- 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
- The Request id: WorkRequest id
- 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