Jetpack series (9) – WorkManager

WorkManager brief introduction

First impression

WorkManager is an API that allows you to easily schedule deferred asynchronous tasks that should run even after you exit the application or restart the device

WorkManager is suitable for deferrable work, that is, work that does not need to be run immediately but needs to be run reliably, even if you exit the application or restart the device. Such as:

  • Sends logs or analysis data to the back-end service
  • Periodically synchronize application data with the server
  • Apply a filter to the image and save the image

The basic concept

Worker: Actual work code performed in the background, overriding the doWor() method

WorkRequest: performs some work requests, passing in the Worker to create part of the WorkRequest

WorkManager: Manages work requests and makes them run, schedules work requests in a manner that distributes the load on system resources, while adhering to specified constraints

Basic use of WorkManager

The basic use

  1. Here we use the example of blur effect for pictures on the official website to practice the use of WorkManager. First, create Worker

    • Available if instantiatedHiltinjectionWorkerI use it hereandroidx.hilt:hilt-workThe version is1.0.0, so directly use@HiltWorkerAnnotations may vary from version to version and are generally not required
    • doWork()Inside to perform specific confusing operations, here and the official website to keep consistent
    class BlurWorker constructor(
        private val ctx: Context,
       params: WorkerParameters
    ) : Worker(ctx, params) {
    
        override fun doWork(a): Result {
    		// Notification, remember to add channel
            makeStatusNotification("Blurring image", ctx)
            return try {
                val picture = BitmapFactory.decodeResource(
                    ctx.resources,
                    R.drawable.test
                )
                val output = blurBitmap(picture, ctx)            / / fuzzy
                val outputUri = writeBitmapToFile(ctx, output)   // Save the image
                makeStatusNotification("Output is $outputUri", ctx)
                Result.success()
            } catch (throwable: Throwable) {
                Result.failure()
            }
        }
    }
    Copy the code
  2. In ViewModel, Worker is triggered by WorkManager. WorkRequest has three subclasses, the first two

    • PeriodicWorkRequestExecute the command repeatedly until the command is canceled
    • OneTimeWorkRequestExecute it only once
    • WorkRequestHoldercontainsWorkRequestInformation is not used for task execution
    @HiltViewModel
    class MainViewModel @Inject constructor(
        private val repository: WordRepository,
        private val workManager: WorkManager
    ) : ViewModel() {
        
        fun applyBlur(a) {
            workManager.enqueue(OneTimeWorkRequest.from(BlurWorker::class.java))
        }
    }
    Copy the code
  3. Instantiate the WorkManager. Since I’m using Hilt, create a separate Module to create the WorkManager

    @InstallIn(SingletonComponent::class)
    @Module
    object WorkModule {
    
        @Provides
        @Singleton
        fun providesWorkManager(@ApplicationContext context: Context): WorkManager =
            WorkManager.getInstance(context)
    }
    Copy the code

The Worker value to

  1. Worker’s inherited Class is passed in through Class. Therefore, in order to pass values to Worker, WorkerParameters can be used. SetInputData method that can use WorkRequest can be passed in

    @HiltViewModel
    class MainViewModel @Inject constructor(
        private val repository: WordRepository,
        private val workManager: WorkManager
    ) : ViewModel() {
        
        private var _imageUri = MutableLiveData<Uri>()
     
        valimageUri: LiveData<Uri? >get() = _imageUri
     
        fun applyBlur(a) {
            val workRequest = OneTimeWorkRequestBuilder<BlurWorker>()
                .setInputData(createInputDataForUri())
                .build()
            workManager.enqueue(workRequest)
        }
    
        private fun createInputDataForUri(a): Data {
            val builder = Data.Builder()
            imageUri.let {
                builder.putString(KEY_IMAGE_URI, imageUri.value.toString())
            }
            return builder.build()
        }
    
        fun setImageUri(uri: Uri) {
            _imageUri.postValue(uri)
        }
    
    }
    Copy the code
    1. Receive values usingWorkerParametersmInputDataBe familiar with
    @HiltWorker
    class BlurWorker @AssistedInject constructor(
       @Assisted private val ctx: Context,
       @Assisted params: WorkerParameters
    ) : Worker(ctx, params) {
    
       override fun doWork(a): Result {
           val resourceUri = inputData.getString(KEY_IMAGE_URI)
           makeStatusNotification("Blurring image", ctx)
           return try {
               val picture = BitmapFactory.decodeStream(
                   ctx.contentResolver.openInputStream(Uri.parse(resourceUri)))
               val output = blurBitmap(picture, ctx)
               val outputUri = writeBitmapToFile(ctx, output)
               makeStatusNotification("Output is $outputUri", ctx)
               Result.success()
           } catch (throwable: Throwable) {
               Result.failure()
           }
       }
    }
    Copy the code

The Work chain

  1. For complex, related work, you can use a smooth, natural interface to chain tasks together so that you can control which parts run in sequence and which parts run in parallel. Use beginWith() or beginUniqueWork() with WorkManager, which returns an instance of WorkContinuation, WorkContinuation adds the OneTimeWorkRequest dependency instance through THEN. In this case, CleanupWorker, BlurWorker and SaveImageToFileWorker are successively included

    // Clear the folder
    class CleanupWorker(
        ctx: Context,
        params: WorkerParameters
    ) : Worker(ctx, params) {
    
        override fun doWork(a): Result {
            makeStatusNotification("Cleaning up old temporary files", applicationContext)
    
            return try {
                val outputDirectory = File(applicationContext.filesDir, OUTPUT_PATH)
                if (outputDirectory.exists()) {
                    val entries = outputDirectory.listFiles()
                    if(entries ! =null) {
                        for (entry in entries) {
                            val name = entry.name
                            if (name.isNotEmpty() && name.endsWith(".png")) {
                                val deleted = entry.delete()
                            }
                        }
                    }
                }
                Result.success()
            } catch (exception: Exception) {
                Result.failure()
            }
        }
    }
    
    // Blur the image
    class BlurWorker(
       ctx: Context,
       params: WorkerParameters
    ) : Worker(ctx, params) {
    
        override fun doWork(a): Result {
            val appContext = applicationContext
            val resourceUri = inputData.getString(KEY_IMAGE_URI)
            makeStatusNotification("Blurring image", appContext)
    
            return try {
                if (TextUtils.isEmpty(resourceUri)) {
                    throw IllegalArgumentException("Invalid input uri")}val resolver = appContext.contentResolver
                val picture = BitmapFactory.decodeStream(
                    resolver.openInputStream(Uri.parse(resourceUri)))
                val output = blurBitmap(picture, appContext)
                val outputUri = writeBitmapToFile(appContext, output)
    
                val outputData = workDataOf(KEY_IMAGE_URI to outputUri.toString())
                Result.success(outputData)
            } catch (throwable: Throwable) {
                Result.failure()
            }
        }
    }
    
    // Save to gallery (don't forget to add dynamic permissions)
    class SaveImageToFileWorker(
        ctx: Context,
        params: WorkerParameters
    ) : Worker(ctx, params) {
    
        private val Title = "Blurred Image"
        private val dateFormatter = SimpleDateFormat(
            "yyyy.MM.dd 'at' HH:mm:ss z",
            Locale.getDefault()
        )
    
        override fun doWork(a): Result {
            
            makeStatusNotification("Saving image", applicationContext)
    
            val resolver = applicationContext.contentResolver
            return try {
                val resourceUri = inputData.getString(KEY_IMAGE_URI)
                val bitmap = BitmapFactory.decodeStream(
                    resolver.openInputStream(Uri.parse(resourceUri))
                )
                val imageUrl = MediaStore.Images.Media.insertImage(
                    resolver, bitmap, Title, dateFormatter.format(Date())
                )
                if(! imageUrl.isNullOrEmpty()) {val output = workDataOf(KEY_IMAGE_URI to imageUrl)
    
                    Result.success(output)
                } else {
                    Result.failure()
                }
            } catch (exception: Exception) {
                Result.failure()
            }
        }
    }
    Copy the code
  2. Create execution chain

    fun applyBlur(a) {
        var continuation = workManager
        .beginUniqueWork(
            IMAGE_MANIPULATION_WORK_NAME,
            ExistingWorkPolicy.REPLACE,
            OneTimeWorkRequest
            .from(CleanupWorker::class.java)
        )
    
        // Repeat blur, I will loop these three times
        for (i in 0 until 3) {
            val blurBuilder = OneTimeWorkRequestBuilder<BlurWorker>()
            if (i == 0) {
                blurBuilder.setInputData(createInputDataForUri())
            }
            continuation = continuation.then(blurBuilder.build())
        }
    
        val save = OneTimeWorkRequestBuilder<SaveImageToFileWorker>()
                    .addTag(TAG_OUTPUT)
                    .build()
        continuation = continuation.then(save)
    
        continuation.enqueue()
    }
    Copy the code

Related knowledge points

WorkManager uses low-level jobs to schedule services based on different apis

Work status monitoring

  • WorkManage The getWorkInfosXxx method obtains the WorkInfo and determines the working status based on the WorkInfo

    // MainViewModel
    @HiltViewModel
    class MainViewModel @Inject constructor(
        private val repository: WordRepository,
        private val workManager: WorkManager
    ) : ViewModel() {
    
        private var _imageUri = MutableLiveData<Uri>()           / / the incoming
        private var _imageUriOut = MutableLiveData<Uri>()        / / out
    
        internal var outputWorkInfos: LiveData<List<WorkInfo>> = MutableLiveData()
    
        init {
            outputWorkInfos = workManager.getWorkInfosByTagLiveData(TAG_OUTPUT)
        }
    
        valimageUri: LiveData<Uri? >get() = _imageUri
        valimageUriOut: LiveData<Uri? >get() = _imageUriOut
        
        @SuppressLint("EnqueueWork")
        fun applyBlur(a) {
    
            var continuation = workManager
                .beginUniqueWork(
                    IMAGE_MANIPULATION_WORK_NAME,
                    ExistingWorkPolicy.REPLACE,
                    OneTimeWorkRequest
                        .from(CleanupWorker::class.java)
                )
    
            // Repeat blur, I will loop these three times
            for (i in 0 until 3) {
                val blurBuilder = OneTimeWorkRequestBuilder<BlurWorker>()
                if (i == 0) {
                    blurBuilder.setInputData(createInputDataForUri())
                }
                continuation = continuation.then(blurBuilder.build())
            }
    
            val save = OneTimeWorkRequestBuilder<SaveImageToFileWorker>()
                .addTag(TAG_OUTPUT)
                .build()
            continuation = continuation.then(save)
    
            continuation.enqueue()
        }
    
        private fun createInputDataForUri(a): Data {
            val builder = Data.Builder()
            imageUri.let {
                builder.putString(KEY_IMAGE_URI, imageUri.value.toString())
            }
            return builder.build()
        }
    
        fun setImageUri(uri: Uri) {
            _imageUri.postValue(uri)
        }
    
        fun setOutputUri(uri: Uri) {
            _imageUriOut.postValue(uri)
        }
    
    }
    
    // FlowStepTwoFragment.kt
    observe(viewModel.outputWorkInfos, ::workInfosUpdate)
    
    private fun workInfosUpdate(list: List<WorkInfo>) {
        if (list.isNullOrEmpty()) {
            return
        }
        val workInfo = list[0]
        if (workInfo.state.isFinished) {
            showWorkFinished()
            val outputImageUri = workInfo.outputData.getString(KEY_IMAGE_URI)
            if(! outputImageUri.isNullOrEmpty()) { viewModel.setOutputUri(Uri.parse(outputImageUri)) bindingTwo.btnCheck.visibility = View.VISIBLE } }else {
            showWorkInProgress()
        }
    }
    Copy the code

A link to the

Jetpack (1) – Navigation

Jetpack series (Ii) — Lifecycle

Jetpack series (3) – LiveData

Jetpack series (4) – ViewModel

Jetpack series (5) — Room

Jetpack series (6) – Paging3

Jetpack (7) – Hilt

Jetpack (8) – Data Bidnding && View Binding

The resources

website

codelabs