即学即用Android Jetpack - WorkManger

前言

即学即用Android Jetpack系列Blog的目的是通过学习Android Jetpack完成一个简单的Demo,本文是即学即用Android Jetpack系列Blog的第六篇。

经过前面几篇博客的学习,我们的Demo已经基本成型,先上图:

即学即用Android Jetpack - WorkManger_第1张图片
列表页

即学即用Android Jetpack - WorkManger_第2张图片
详情页

即学即用Android Jetpack - WorkManger_第3张图片
喜欢页

这里我得提一下,鞋子的数据不是从网络请求中获取的,这个时候小王就举手了,那鞋子的数据是哪里来的呢?其实很简单,数据是从 assets目录下的 json读取出来的,通常情况下,从文件读取数据都不会放在主线程中执行,所以呢,我们Demo中的数据初始化当然也没有在主线程执行了,这时,就得请出我们今天的主角—— WorkManager,它是我们能够在后台执行数据初始化的原因。

语言:Kotlin
我的Demo:https://github.com/mCyp/Hoo

目录

一、介绍

友情提示
官方文档:WorkManager
谷歌实验室:官方教程
官方案例:android-workmanager
以及强力安利:
WorkManger介绍视频:中文官方介绍视频(主要是小姐姐好看~)

1. 定义

通过一开始粗略的介绍,我们已经了解到,WorkManager是用来执行后台任务的,正如官方介绍:

WorkManager, a compatible, flexible and simple library for deferrable background work.
WorkManger是一个可兼容、灵活和简单的延迟后台任务。

2. 选择WorkManager的理由

Android中处理后台任务的选择挺多的,比如ServiceDownloadManagerAlarmManagerJobScheduler等,那么选择WorkManager的理由是什么呢?

  1. 版本兼容性强,向后兼容至API 14。
  2. 可以指定约束条件,比如可以选择必须在有网络的条件下执行。
  3. 可定时执行也可单次执行。
  4. 监听和管理任务状态。
  5. 多个任务可使用任务链。
  6. 保证任务执行,如当前执行条件不满足或者App进程被杀死,它会等到下次条件满足或者App进程打开后执行。
  7. 支持省电模式。

3. 多线程任务如何选择?

后台任务会消耗设备的系统资源,如若处理不当,可能会造成设备电量的急剧消耗,给用户带来糟糕的体验。所以,选择正确的后台处理方式是每个开发者应当注意的,如下是官方给的选择方式:

即学即用Android Jetpack - WorkManger_第4张图片
选择方式
图片来自: 官方文档
关于一些后台任务的知识,我推荐你阅读: [译] 从Service到WorkManager,很好的一篇文章。

二、实战

本次的实战来自于我上面的介绍的官方例子,最终我将它添加进我的Demo里面:

即学即用Android Jetpack - WorkManger_第5张图片
效果

如图所见,我们要做的就是选取一张图片,将图片做模糊处理,之后显示在我们的头像上。

第一步 添加依赖

ext.workVersion = "2.0.1"
dependencies {
    // ...省略

    implementation "androidx.work:work-runtime-ktx:$rootProject.workVersion"
}

第二步 自定义Worker

构建Worker之前,我们有必要了解一下WorkManger中重要的几个类:

作用
Worker 需要继承Worker,并复写doWork()方法,在doWork()方法中放入你需要在后台执行的代码。
WorkRequest 指后台工作的请求,你可以在后台工作的请求中添加约束条件
WorkManager 真正让Worker在后台执行的类

除了这几个重要的类,我们仍需了解WorkManger的执行流程,以便于我们能够更好的使用:

即学即用Android Jetpack - WorkManger_第6张图片
WorkerManger
图片来自: 谷歌工程师的博客
主要分为三步:

  1. WorkRequest生成以后,Internal TaskExecutor将它存入WorkManger的数据库中,这也是为什么即使在程序退出之后,WorkManger也能保证后台任务在下次启动后条件满足的情况下执行。
  2. 当约束条件满足的情况下,Internal TaskExecutor告诉WorkFactory生成Worker
  3. 后台任务Worker执行。

下面开始我们的构建Worker,为了生成一张模糊图片,我们需要:清除之前的缓存路径、图片模糊的处理和图片的生成。我们可以将这三个步骤分为三个后台任务,三个后台任务又分别涉及到无变量情况、往外传参和读取参数这三种情况:

通常情况

/**
 * 清理临时文件的Worker
 */
class CleanUpWorker(ctx: Context, params: WorkerParameters) : Worker(ctx, params) {
    private val TAG by lazy {
        this::class.java.simpleName
    }

    override fun doWork(): Result {
        // ... 省略

        return try {
            // 删除逻辑
            // ...代码省略
            // 成功时返回
            Result.success()
        } catch (exception: Exception) {
            // 失败时返回
            Result.failure()
        }
    }
}

输出参数

/**
 * 模糊处理的Worker
 */
class BlurWorker(context: Context, params: WorkerParameters) : Worker(context, params) {

    override fun doWork(): Result {
        //...
        return try {
            // 图片处理逻辑
            // 图片处理逻辑省略...

            // 将路径输出
            val outPutData = workDataOf(KEY_IMAGE_URI to outputUri.toString())
            makeStatusNotification("Output is $outputUri", context)
            Result.success(outPutData)
        }catch (throwable: Throwable){
            Result.failure()
        }
    }
}

读取参数

/**
 * 存储照片的Worker
 */
class SaveImageToFileWorker(ctx:Context,parameters: WorkerParameters):Worker(ctx,parameters) {
    //...

    override fun doWork(): Result {
        //...
        return try {
            // 获取从外部传入的参数
            val resourceUri = inputData.getString(KEY_IMAGE_URI)
            //... 存储逻辑
            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()
        }
    }
}

第三步 创建WorkManger

这一步还是挺简单的,MeModel中单例获取:

class MeModel(val userRepository: UserRepository) : ViewModel() {
    //...
    private val workManager = WorkManager.getInstance()
    // ...
}

第四步 构建WorkRequest

WorkRequest可以分为两类:

名称 作用
PeriodicWorkRequest 多次、定时执行的任务请求,不支持任务链
OneTimeWorkRequest 只执行一次的任务请求,支持任务链
1. 执行一个任务

我们以OneTimeWorkRequest为例,如果我们只有一个任务请求,这样写就行:

        val request = OneTimeWorkRequest.from(CleanUpWorker::class.java)
        workManager.enqueue(request)
2. 执行多个任务

但是,这样写显然不适合我们当前的业务需求,因为我们有三个Worker,并且三个Worker有先后顺序,因此我们可以使用任务链:

        // 多任务按顺序执行
        workManager.beginWith(
            mutableListOf(
                OneTimeWorkRequest.from(CleanUpWorker::class.java)
            ))
            .then(OneTimeWorkRequestBuilder().setInputData(createInputDataForUri()).build())
            .then(OneTimeWorkRequestBuilder().build())
            .enqueue()

等等,假设我多次点击图片更换头像,提交多次请求,由于网络等原因(虽然我们的Demo没有网络数据请求部分),最后返回的很有可能不是我们最后一次请求的图片,这显然是糟糕的,不过,WorkManger能够满足你的需求,保证任务的唯一性:

        // 多任务按顺序执行
        workManager.beginUniqueWork(
            IMAGE_MANIPULATION_WORK_NAME, // 任务名称
            ExistingWorkPolicy.REPLACE, // 任务相同的执行策略 分为REPLACE,KEEP,APPEND
            mutableListOf(
                OneTimeWorkRequest.from(CleanUpWorker::class.java) 
            ))
            .then(OneTimeWorkRequestBuilder().setInputData(createInputDataForUri()).build())
            .then(OneTimeWorkRequestBuilder().build())
            .enqueue()

无顺序多任务
这里有必要提一下,如果并行执行没有顺序的多个任务,无论是beginUniqueWork还是beginWith方法都可以接受一个List

3. 使用约束

假设我们需要将生成的图片上传到服务端,并且需要将图片存储到本地,显然,我们需要设备网络条件良好并且有存储空间,这时候,我们可以给WorkRequest指明约束条件:

        // 构建约束条件
        val constraints = Constraints.Builder()
            .setRequiresBatteryNotLow(true) // 非电池低电量
            .setRequiredNetworkType(NetworkType.CONNECTED) // 网络连接的情况
            .setRequiresStorageNotLow(true) // 存储空间足
            .build()

        // 储存照片
        val save = OneTimeWorkRequestBuilder()
            .setConstraints(constraints)
            .addTag(TAG_OUTPUT)
            .build()
        continuation = continuation.then(save)

可以指明的约束条件有:电池电量充电网络条件存储延迟等,具体的可以使用的时候查看接口。

以下则是我们Demo中的具体使用:

class MeModel(val userRepository: UserRepository) : ViewModel() {
    //... 
    private val workManager = WorkManager.getInstance()
    val user = userRepository.findUserById(AppPrefsUtils.getLong(BaseConstant.SP_USER_ID))

    internal fun applyBlur(blurLevel: Int) {
       //... 创建任务链

        var continuation = workManager
            .beginUniqueWork(
                IMAGE_MANIPULATION_WORK_NAME,
                ExistingWorkPolicy.REPLACE,
                OneTimeWorkRequest.from(CleanUpWorker::class.java)
            )

        for (i in 0 until blurLevel) {
            val builder = OneTimeWorkRequestBuilder()
            if (i == 0) {
                builder.setInputData(createInputDataForUri())
            }
            continuation = continuation.then(builder.build())
        }

        // 构建约束条件
        val constraints = Constraints.Builder()
            .setRequiresBatteryNotLow(true) // 非电池低电量
            .setRequiredNetworkType(NetworkType.CONNECTED) // 网络连接的情况
            .setRequiresStorageNotLow(true) // 存储空间足
            .build()

        // 储存照片
        val save = OneTimeWorkRequestBuilder()
            .setConstraints(constraints)
            .addTag(TAG_OUTPUT)
            .build()
        continuation = continuation.then(save)

        continuation.enqueue()
    }

    private fun createInputDataForUri(): Data {
        val builder = Data.Builder()
        imageUri?.let {
            builder.putString(KEY_IMAGE_URI, imageUri.toString())
        }
        return builder.build()
    }

    //... 省略
}

第四步 取消任务

如果想取消所有的任务workManager.cancelAllWork(),当然如果想取消我们上面执行的唯一任务,需要我们上面的唯一任务名:

class MeModel(val userRepository: UserRepository) : ViewModel() {
    fun cancelWork() {
        workManager.cancelUniqueWork(IMAGE_MANIPULATION_WORK_NAME)
    }
}

第五步 观察任务状态

任务状态的变化过程:

即学即用Android Jetpack - WorkManger_第7张图片
状态观测
图片来自于: How to use WorkManager with RxJava
其中, SUCCEEDEDFAILEDCANCELLED都属于任务已经完成。观察任务状态需要使用到 LiveData

class MeModel(val userRepository: UserRepository) : ViewModel() {
    //... 省略
    private val workManager = WorkManager.getInstance()
    val user = userRepository.findUserById(AppPrefsUtils.getLong(BaseConstant.SP_USER_ID))

    init {
        outPutWorkInfos = workManager.getWorkInfosByTagLiveData(TAG_OUTPUT)
    }

    // ...省略
}

当图片处理的时候,程序弹出加载框,图片处理完成,程序会将图片路径保存到User里的headImage并存储到数据库中,任务状态观测参见MeFragment中的onSubscribeUi方法:

class MeFragment : Fragment() {
    private val TAG by lazy { MeFragment::class.java.simpleName }
    // 选择图片的标识
    private val REQUEST_CODE_IMAGE = 100
    // 加载框
    private val sweetAlertDialog: SweetAlertDialog by lazy {
        SweetAlertDialog(requireContext(), SweetAlertDialog.PROGRESS_TYPE)
            .setTitleText("头像")
            .setContentText("更新中...")
            /*
            .setCancelButton("取消") {
                model.cancelWork()
                sweetAlertDialog.dismiss()
            }*/
    }

    // MeModel懒加载
    private val model: MeModel by viewModels {
        CustomViewModelProvider.providerMeModel(requireContext())
    }

    override fun onCreateView(
        inflater: LayoutInflater, container: ViewGroup?,
        savedInstanceState: Bundle?
    ): View? {
        // Data Binding
        val binding: FragmentMeBinding = FragmentMeBinding.inflate(inflater, container, false)
        initListener(binding)
        onSubscribeUi(binding)
        return binding.root
    }

    /**
     * 初始化监听器
     */
    private fun initListener(binding: FragmentMeBinding) {
        binding.ivHead.setOnClickListener {
            // 选择处理的图片
            val chooseIntent = Intent(
                Intent.ACTION_PICK,
                MediaStore.Images.Media.EXTERNAL_CONTENT_URI
            )
            startActivityForResult(chooseIntent, REQUEST_CODE_IMAGE)
        }
    }

    /**
     * Binding绑定
     */
    private fun onSubscribeUi(binding: FragmentMeBinding) {
        model.user.observe(this, Observer {
            binding.user = it
        })

        // 任务状态的观测
        model.outPutWorkInfos.observe(this, Observer {
            if (it.isNullOrEmpty())
                return@Observer

            val state = it[0]
            if (state.state.isFinished) {
                // 更新头像
                val outputImageUri = state.outputData.getString(KEY_IMAGE_URI)
                if (!outputImageUri.isNullOrEmpty()) {
                    model.setOutputUri(outputImageUri)
                }
                sweetAlertDialog.dismiss()
            }
        })
    }

    /**
     * 图片选择完成的回调
     */
    override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) {
        if (resultCode == Activity.RESULT_OK) {
            when (requestCode) {
                REQUEST_CODE_IMAGE -> data?.let { handleImageRequestResult(data) }
                else -> Log.d(TAG, "Unknown request code.")
            }
        } else {
            Log.e(TAG, String.format("Unexpected Result code %s", resultCode))
        }
    }

    /**
     * 图片选择完成的处理
     */
    private fun handleImageRequestResult(intent: Intent) {
        // If clipdata is available, we use it, otherwise we use data
        val imageUri: Uri? = intent.clipData?.let {
            it.getItemAt(0).uri
        } ?: intent.data

        if (imageUri == null) {
            Log.e(TAG, "Invalid input image Uri.")
            return
        }

        sweetAlertDialog.show()
        // 图片模糊处理
        model.setImageUri(imageUri.toString())
        model.applyBlur(3)
    }
}

写完以后,动图的效果就会出现了。

三、更多

选择适合自己的Worker

谷歌提供了四种Worker给我们使用,分别为:自动运行在后台线程的Worker、结合协程的CoroutineWorker、结合RxJava2RxWorker和以上三个类的基类的ListenableWorker

由于本文使用的Kotlin,故打算简单的介绍CoroutineWorker,其他的可以自行探索。

我们使用ShoeWorker来从文件中读取鞋子的数据并完成数据库的插入工作,使用方式基本与Worker一致:

class ShoeWorker(
    context: Context,
    workerParams: WorkerParameters
) : CoroutineWorker(context, workerParams) {

    private val TAG by lazy {
        ShoeWorker::class.java.simpleName
    }

    // 指定Dispatchers
    override val coroutineContext: CoroutineDispatcher
        get() = Dispatchers.IO

    override suspend fun doWork(): Result = coroutineScope {
        try {
            applicationContext.assets.open("shoes.json").use {
                JsonReader(it.reader()).use {
                    val shoeType = object : TypeToken>() {}.type
                    val shoeList: List = Gson().fromJson(it, shoeType)

                    val shoeDao = RepositoryProvider.providerShoeRepository(applicationContext)
                    shoeDao.insertShoes(shoeList)
                    for (i in 0..2) {
                        for (shoe in shoeList) {
                            shoe.id += shoeList.size
                        }
                        shoeDao.insertShoes(shoeList)
                    }
                    Result.success()
                }

            }
        } catch (ex: Exception) {
            Log.e(TAG, "Error seeding database", ex)
            Result.failure()
        }
    }
}

四、总结

即学即用Android Jetpack - WorkManger_第8张图片
总结

可以发现,大部分的后台任务处理, WorkManager都可以胜任,这也是我们需要学习 WorkManger的原因。本次WorkManger学习完毕,本人水平有限,难免有误,欢迎指正。
Over~

参考文章:

《Android Jetpack - 使用 WorkManager 管理后台任务》
《[译] 从Service到WorkManager》
《官方文档:Guide to background processing》
《谷歌实验室》
《官方文档:WorkManager》
《WorkManager Basics》

如果觉得本文不错,可以查看Android Jetpack系列的其他文章:

第一篇:《即学即用Android Jetpack - Navigation》
第二篇:《即学即用Android Jetpack - Data Binding》
第三篇:《即学即用Android Jetpack - ViewModel & LiveData》
第四篇:《即学即用Android Jetpack - Room》
第五篇:《即学即用Android Jetpack - Paging》

你可能感兴趣的:(即学即用Android Jetpack - WorkManger)