android 读取work文件夹,即学即用Android Jetpack - WorkManger

前言

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

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

68e720b8a939

列表页

68e720b8a939

详情页

68e720b8a939

喜欢页

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

目录

一、介绍

友情提示

官方文档:WorkManager

谷歌实验室:官方教程

官方案例:android-workmanager

以及强力安利:

WorkManger介绍视频:中文官方介绍视频(主要是小姐姐好看~)

1. 定义

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

WorkManager, a compatible, flexible and simple library for deferrable background work.

WorkManger是一个可兼容、灵活和简单的延迟后台任务。

2. 选择WorkManager的理由

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

版本兼容性强,向后兼容至API 14。

可以指定约束条件,比如可以选择必须在有网络的条件下执行。

可定时执行也可单次执行。

监听和管理任务状态。

多个任务可使用任务链。

保证任务执行,如当前执行条件不满足或者App进程被杀死,它会等到下次条件满足或者App进程打开后执行。

支持省电模式。

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

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

68e720b8a939

选择方式图片来自:官方文档

关于一些后台任务的知识,我推荐你阅读:[译] 从Service到WorkManager,很好的一篇文章。

二、实战

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

68e720b8a939

效果

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

第一步 添加依赖

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的执行流程,以便于我们能够更好的使用:

68e720b8a939

WorkerManger图片来自:谷歌工程师的博客

主要分为三步:

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

当约束条件满足的情况下,Internal TaskExecutor告诉WorkFactory生成Worker。

后台任务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)

}

}

第五步 观察任务状态

任务状态的变化过程:

68e720b8a939

状态观测图片来自于:How to use WorkManager with RxJava

其中,SUCCEEDED、FAILED和CANCELLED都属于任务已经完成。观察任务状态需要使用到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、结合RxJava2的RxWorker和以上三个类的基类的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()

}

}

}

四、总结

68e720b8a939

总结

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

Over~

参考文章:

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

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值