美文网首页Android进阶之路Android补给站Android开发经验谈
WorkManager从入门到实践,有这一篇就够了

WorkManager从入门到实践,有这一篇就够了

作者: 午后一小憩 | 来源:发表于2019-08-15 12:29 被阅读11次

    前言

    上一次我们对Paging的应用进行了一次全面的分析,这一次我们来聊聊WorkManager。

    如果你对Paging还未了解,推荐阅读这篇文章:

    Paging在RecyclerView中的应用,有这一篇就够了

    本来这一篇文章上周就能够发布出来,但我写文章有一个特点,都会结合具体的Demo来进行阐述,而WorkManager的Demo早就完成了,只是要结合文章一起阐述实在需要时间,上周自身原因也就延期了,想想还是写代码容易啊...😿😿

    哎呀不多说了,进入正题!

    WorkManager

    WorkManager是什么?官方给的解释是:它对可延期任务操作非常简单,同时稳定性非常强,对于异步任务,即使App退出运行或者设备重启,它都能够很好的保证任务的顺利执行。

    所以关键点是简单与稳定性。

    对于平常的使用,如果一个后台任务在执行的过程中,app突然退出或者手机断网,这时后台任务将直接终止。

    典型的场景是:App的关注功能。如果用户在弱网的情况下点击关注按钮,此时用户由于某种原因马上退出了App,但关注的请求并没有成功发送给服务端,那么下次用户再进入时,拿到的还是之前未关注的状态信息。这就产生了操作上的bug,降低了用户的体验,增加了用户不必要的操作。

    那么该如何解决呢?很简单,看WorkManager的定义,使用WorkManager就可以轻松解决。这里就不再拓展实现代码了,只要你继续看完这篇文章,你就能轻松实现。

    当然你不使用WorkManager也能实现,这就涉及到它的另一个好处:简单。如果你不使用WorkManager,你就要对不同API版本进行区分。

    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)
    

    通过JobScheduler来创建一个Job,一旦所设的条件达到,就会执行该Job。但JobScheduler是在API21加入的,同时在API21&22有一个系统Bug

    [图片上传失败...(image-80ea67-1565834257839)]

    [图片上传失败...(image-ae0ceb-1565834257839)]

    [图片上传失败...(image-d8fba1-1565834257839)]

    这就意味着它只能用在API23及以上的版本

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

    既然只能API23及以上才能使用JobScheduler,那么在API23以下又该如何呢?

    AlarmManager & BroadcastReceiver

    这时对于API23以下,可以使用AlarmManager来进行任务的执行,同时结合BoradcastReceiver来进行任务的条件监听,例如网络的连接状态、设备的启动等。

    看到这里是不是开始头大了呢,我们开始的目的只是想做一个稳定性的后台任务,最后发现居然还要进行版本兼容。兼容性与实现性进一步加大。

    那么有没有统一的实现方式呢?当然有,它就是WorkManager,它的核心原理使用的就是上面所分析的结合体。

    他会结合版本自动使用最佳的实现方式,同时还会提供额外的便利操作,例如状态监听、链式请求等等。

    WorkManager的使用,我将其分为以下几步:

    1. 构建Work
    2. 配置WorkRequest
    3. 添加到WorkContinuation中
    4. 获取响应结果

    下面我们来通过Demo逐步了解。

    构建Work

    WorkManager每一个任务都是由Work构成,所以Work是任务具体执行的核心所在。既然是核心所在,你可能会认为它会非常难实现,但恰恰相反,它的实现非常简单,你只需实现它的doWork方法即可。例如我们来实现一个清除相关目录下的.png图片的Work

    class CleanUpWorker(context: Context, workerParams: WorkerParameters) : Worker(context, workerParams) {
    
        override fun doWork(): 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()
        }
    }
    

    所有代码都在doWork中,实现逻辑也非常简单:找到相关目录,然后逐一判断目录中的文件是否为.png图片,如果是就删除。

    以上是逻辑代码,关键点是返回值Result.success(),它是一个Result类型,可用值有三个

    1. Result.success(): 成功
    2. Result.failure(): 失败
    3. Result.retry(): 重试

    对于success与failure,它还支持传递Data类型的值,Data内部是一个Map来管理的,所以对于kotlin可以直接使用workDataOf

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

    它传递的值将放入OutputData中,可以在链式请求中传递,与最终的响应结果获取。其实本质是WorkManager结合了Room,将数据保存在数据库中。

    这一步要点就是这么多,下面进入下一步。

    配置WorkRequest

    WorkManager主要是通过WorkRequest来配置任务的,而它的WorkRequest种类包括:

    1. OneTimeWorkRequest
    2. PeriodicWorkRequest

    OneTimeWorkRequest

    首先OneTimeWorkRequest是作用于一次性任务,即任务只执行一次,一旦执行完就自动结束。它的构建也非常简单:

    val cleanUpRequest = OneTimeWorkRequestBuilder<CleanUpWorker>().build()
    

    这样就配置了与CleanUpWorker相关的WorkRequest,而且是一次性的。

    在配置WorkRequest的过程中我们还可以对其添加别的配置,例如添加tag、传入inputData与添加constraint约束条件等等。

    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()
    

    添加tag是为了打上标签,以便后续获取结果;传入的inputData可以在BlurImageWork中获取传入的值;添加网络连接constraint约束条件,代表只有在网络连接的状态下才会触发该WorkRequest。

    而BlurImageWork的核心代码如下:

    override suspend fun doWork(): 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()
    }
    

    在doWork中,通过InputData来获取上述blurRequest中传入的InputData数据。然后通过apply来处理图片,最后使用writeToFile写入到本地文件中,并返回路径。

    由于篇幅有限,这里就不一一展开,感兴趣的可以查看源码

    PeriodicWorkRequest

    PeriodicWorkRequest是可以周期性的执行任务,它的使用方式与配置和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()
    

    不过需要注意的是:它的周期间隔最少为15分钟。

    添加到WorkContinuation中

    上面我们已经将WorkRequest配置好了,剩下要做的是将其加入到work工作链中进行执行。

    对于单个的WorkRequest,可以直接通过WorkManager的enqueue方法

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

    如果你想使用链式工作,只需调用beginWith或者beginUniqueWork方法即可。其实它们本质都是实例化了一个WorkContinuationImpl,只是调用了不同的构造方法。而最终的构造方法为:

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

    其中beginWith方法只需传入WorkRequest

    val workContinuation = mWorkManager.beginWith(cleanUpWork)
    

    beginUniqueWork允许我们创建一个独一无二的链式请求。使用也很简单:

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

    其中第一个参数是设置该链式请求的name;第二个参数ExistingWorkPolicy是设置name相同时的表现,它三个值,分别为:

    1. REPLACE: 当有相同name且未完成的链式请求时,将原来的进度取消并删除,重新加入新的链式请求
    2. KEEP: 当有相同name且未完成的链式请求时,链式请求保持不变
    3. APPEND: 当有相同name且未完成的链式请求时,将新的链式请求追加到原来的子队列中,即当原来的链式请求全部执行后才开始执行。

    而不管是beginWith还是beginUniqueWork,它都会返回WorkContinuation对象,通过该对象我们可以将后续任务加入到链式请求中。例如将上面的cleanUpRequest(清除)、blurRequest(图片模糊处理)与saveRequest(保存)串行起来执行,实现如下:

    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()
    

    除了串行执行,还支持并行。例如将cleanUpRequest与blurRequest并行处理,完成之后再与saveRequest串行

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

    需要注意的是:如果你的WorkRequest是PeriodicWorkRequest类型,那么它不支持建立链式请求,这一点需要注意了。简单的理解,周期性的任务原则上是没有终止的,是个闭环,也就不存在所谓的链了。

    获取响应结果

    这就到最后一步了,获取响应结果WorkInfo。WorkManager支持两种方式来获取响应结果

    1. Request.id: WorkRequest的id
    2. Tag.name: WorkRequest中设置的tag

    同时返回的WorkInfo还支持LiveData数据格式。

    例如,现在我们要监听上述blurRequest与saveRequest的状态,使用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() {
        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)
            }
        })
     
        ......
        ......
    }
    

    再来看一个通过id获取的:

        // ViewModel
        internal val dataSourceInfo: MediatorLiveData<WorkInfo> = MediatorLiveData()
      
        private fun addSource() {
            val periodicWorkInfo = mWorkManager.getWorkInfoByIdLiveData(mPeriodicRequest.id)
            dataSourceInfo.addSource(periodicWorkInfo) {
                dataSourceInfo.value = it
            }
        }
        
        // Activity
        private fun addObserver() {
            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()
                        }
                    }
                }
            })
        }
    

    结合LiveData使用是不是很简单呢? WorkInfo获取的本质是通过操作Room数据库来获取。在文章的Work部分已经提到,在执行完Work任务之后传递的数据将会保存到Room数据库中。

    所以WorkManager与AAC的结合度非常高,目的也是致力于为我们开发者提供一套完整的框架,同时也说明Google对AAC框架的重视。

    如果你还未了解AAC,推荐你阅读我之前的文章

    Room
    LiveData
    Lifecycle
    ViewModel

    最后我们将上面的几个WorkRequest结合起来执行,看下它们的最终效果:

    通过这篇文章,希望你能够熟悉运用WorkManager。如果这篇文章对你有所帮助,你可以顺手点赞、关注一波,这是对我最大的鼓励!

    项目地址

    Android精华录

    该库的目的是结合详细的Demo来全面解析Android相关的知识点, 帮助读者能够更快的掌握与理解所阐述的要点

    Android精华录

    blog

    [图片上传失败...(image-baab09-1565834257839)]

    相关文章

      网友评论

        本文标题:WorkManager从入门到实践,有这一篇就够了

        本文链接:https://www.haomeiwen.com/subject/jyqajctx.html