前言
即學(xué)即用Android Jetpack系列Blog的目的是通過(guò)學(xué)習(xí)Android Jetpack
完成一個(gè)簡(jiǎn)單的Demo监徘,本文是即學(xué)即用Android Jetpack系列Blog的第六篇。
經(jīng)過(guò)前面幾篇博客的學(xué)習(xí)吧碾,我們的Demo已經(jīng)基本成型凰盔,先上圖:
這里我得提一下,鞋子的數(shù)據(jù)不是從網(wǎng)絡(luò)請(qǐng)求中獲取的倦春,這個(gè)時(shí)候小王就舉手了户敬,那鞋子的數(shù)據(jù)是哪里來(lái)的呢落剪?其實(shí)很簡(jiǎn)單,數(shù)據(jù)是從
assets
目錄下的json
讀取出來(lái)的尿庐,通常情況下著榴,從文件讀取數(shù)據(jù)都不會(huì)放在主線程中執(zhí)行,所以呢,我們Demo中的數(shù)據(jù)初始化當(dāng)然也沒(méi)有在主線程執(zhí)行了,這時(shí)貌虾,就得請(qǐng)出我們今天的主角——WorkManager
,它是我們能夠在后臺(tái)執(zhí)行數(shù)據(jù)初始化的原因问麸。
語(yǔ)言:Kotlin
我的Demo:https://github.com/mCyp/Hoo
目錄
一、介紹
友情提示
官方文檔:WorkManager
谷歌實(shí)驗(yàn)室:官方教程
官方案例:android-workmanager
以及強(qiáng)力安利:
WorkManger介紹視頻:中文官方介紹視頻(主要是小姐姐好看~)
1. 定義
通過(guò)一開(kāi)始粗略的介紹钞翔,我們已經(jīng)了解到严卖,WorkManager
是用來(lái)執(zhí)行后臺(tái)任務(wù)的,正如官方介紹:
WorkManager, a compatible, flexible and simple library for deferrable background work.
WorkManger是一個(gè)可兼容布轿、靈活和簡(jiǎn)單的延遲后臺(tái)任務(wù)哮笆。
2. 選擇WorkManager的理由
Android中處理后臺(tái)任務(wù)的選擇挺多的,比如Service
汰扭、DownloadManager
稠肘、AlarmManager
、JobScheduler
等萝毛,那么選擇WorkManager
的理由是什么呢项阴?
- 版本兼容性強(qiáng),向后兼容至API 14笆包。
- 可以指定約束條件环揽,比如可以選擇必須在有網(wǎng)絡(luò)的條件下執(zhí)行。
- 可定時(shí)執(zhí)行也可單次執(zhí)行庵佣。
- 監(jiān)聽(tīng)和管理任務(wù)狀態(tài)歉胶。
- 多個(gè)任務(wù)可使用任務(wù)鏈。
- 保證任務(wù)執(zhí)行巴粪,如當(dāng)前執(zhí)行條件不滿足或者App進(jìn)程被殺死通今,它會(huì)等到下次條件滿足或者App進(jìn)程打開(kāi)后執(zhí)行。
- 支持省電模式验毡。
3. 多線程任務(wù)如何選擇衡创?
后臺(tái)任務(wù)會(huì)消耗設(shè)備的系統(tǒng)資源,如若處理不當(dāng)晶通,可能會(huì)造成設(shè)備電量的急劇消耗,給用戶帶來(lái)糟糕的體驗(yàn)哟玷。所以狮辽,選擇正確的后臺(tái)處理方式是每個(gè)開(kāi)發(fā)者應(yīng)當(dāng)注意的一也,如下是官方給的選擇方式:
關(guān)于一些后臺(tái)任務(wù)的知識(shí),我推薦你閱讀:[譯] 從Service到WorkManager喉脖,很好的一篇文章椰苟。
二、實(shí)戰(zhàn)
本次的實(shí)戰(zhàn)來(lái)自于我上面的介紹的官方例子树叽,最終我將它添加進(jìn)我的Demo里面:
如圖所見(jiàn)舆蝴,我們要做的就是選取一張圖片,將圖片做模糊處理题诵,之后顯示在我們的頭像上洁仗。
第一步 添加依賴
ext.workVersion = "2.0.1"
dependencies {
// ...省略
implementation "androidx.work:work-runtime-ktx:$rootProject.workVersion"
}
第二步 自定義Worker
構(gòu)建Worker
之前,我們有必要了解一下WorkManger
中重要的幾個(gè)類:
類 | 作用 |
---|---|
Worker |
需要繼承Worker 性锭,并復(fù)寫doWork() 方法赠潦,在doWork() 方法中放入你需要在后臺(tái)執(zhí)行的代碼。 |
WorkRequest |
指后臺(tái)工作的請(qǐng)求草冈,你可以在后臺(tái)工作的請(qǐng)求中添加約束條件 |
WorkManager |
真正讓Worker 在后臺(tái)執(zhí)行的類 |
除了這幾個(gè)重要的類她奥,我們?nèi)孕枇私?code>WorkManger的執(zhí)行流程,以便于我們能夠更好的使用:
主要分為三步:
-
WorkRequest
生成以后怎棱,Internal TaskExecutor
將它存入WorkManger
的數(shù)據(jù)庫(kù)中哩俭,這也是為什么即使在程序退出之后,WorkManger
也能保證后臺(tái)任務(wù)在下次啟動(dòng)后條件滿足的情況下執(zhí)行拳恋。 - 當(dāng)約束條件滿足的情況下携茂,
Internal TaskExecutor
告訴WorkFactory
生成Worker
。 - 后臺(tái)任務(wù)
Worker
執(zhí)行诅岩。
下面開(kāi)始我們的構(gòu)建Worker讳苦,為了生成一張模糊圖片,我們需要:清除之前的緩存路徑吩谦、圖片模糊的處理和圖片的生成鸳谜。我們可以將這三個(gè)步驟分為三個(gè)后臺(tái)任務(wù),三個(gè)后臺(tái)任務(wù)又分別涉及到無(wú)變量情況式廷、往外傳參和讀取參數(shù)這三種情況:
通常情況
/**
* 清理臨時(shí)文件的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 {
// 刪除邏輯
// ...代碼省略
// 成功時(shí)返回
Result.success()
} catch (exception: Exception) {
// 失敗時(shí)返回
Result.failure()
}
}
}
輸出參數(shù)
/**
* 模糊處理的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()
}
}
}
讀取參數(shù)
/**
* 存儲(chǔ)照片的Worker
*/
class SaveImageToFileWorker(ctx:Context,parameters: WorkerParameters):Worker(ctx,parameters) {
//...
override fun doWork(): Result {
//...
return try {
// 獲取從外部傳入的參數(shù)
val resourceUri = inputData.getString(KEY_IMAGE_URI)
//... 存儲(chǔ)邏輯
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 {
// 失敗時(shí)返回
Result.failure()
}
} catch (exception: Exception) {
// 異常時(shí)返回
Result.failure()
}
}
}
第三步 創(chuàng)建WorkManger
這一步還是挺簡(jiǎn)單的咐扭,MeModel
中單例獲取:
class MeModel(val userRepository: UserRepository) : ViewModel() {
//...
private val workManager = WorkManager.getInstance()
// ...
}
第四步 構(gòu)建WorkRequest
WorkRequest
可以分為兩類:
名稱 | 作用 |
---|---|
PeriodicWorkRequest |
多次滑废、定時(shí)執(zhí)行的任務(wù)請(qǐng)求蝗肪,不支持任務(wù)鏈 |
OneTimeWorkRequest |
只執(zhí)行一次的任務(wù)請(qǐng)求,支持任務(wù)鏈 |
1. 執(zhí)行一個(gè)任務(wù)
我們以OneTimeWorkRequest
為例蠕趁,如果我們只有一個(gè)任務(wù)請(qǐng)求薛闪,這樣寫就行:
val request = OneTimeWorkRequest.from(CleanUpWorker::class.java)
workManager.enqueue(request)
2. 執(zhí)行多個(gè)任務(wù)
但是,這樣寫顯然不適合我們當(dāng)前的業(yè)務(wù)需求俺陋,因?yàn)槲覀冇腥齻€(gè)Worker
豁延,并且三個(gè)Worker
有先后順序昙篙,因此我們可以使用任務(wù)鏈:
// 多任務(wù)按順序執(zhí)行
workManager.beginWith(
mutableListOf(
OneTimeWorkRequest.from(CleanUpWorker::class.java)
))
.then(OneTimeWorkRequestBuilder<BlurWorker>().setInputData(createInputDataForUri()).build())
.then(OneTimeWorkRequestBuilder<SaveImageToFileWorker>().build())
.enqueue()
等等,假設(shè)我多次點(diǎn)擊圖片更換頭像诱咏,提交多次請(qǐng)求苔可,由于網(wǎng)絡(luò)等原因(雖然我們的Demo沒(méi)有網(wǎng)絡(luò)數(shù)據(jù)請(qǐng)求部分),最后返回的很有可能不是我們最后一次請(qǐng)求的圖片袋狞,這顯然是糟糕的焚辅,不過(guò),WorkManger
能夠滿足你的需求苟鸯,保證任務(wù)的唯一性:
// 多任務(wù)按順序執(zhí)行
workManager.beginUniqueWork(
IMAGE_MANIPULATION_WORK_NAME, // 任務(wù)名稱
ExistingWorkPolicy.REPLACE, // 任務(wù)相同的執(zhí)行策略 分為REPLACE同蜻,KEEP,APPEND
mutableListOf(
OneTimeWorkRequest.from(CleanUpWorker::class.java)
))
.then(OneTimeWorkRequestBuilder<BlurWorker>().setInputData(createInputDataForUri()).build())
.then(OneTimeWorkRequestBuilder<SaveImageToFileWorker>().build())
.enqueue()
無(wú)順序多任務(wù)
這里有必要提一下倔毙,如果并行執(zhí)行沒(méi)有順序的多個(gè)任務(wù)埃仪,無(wú)論是beginUniqueWork
還是beginWith
方法都可以接受一個(gè)List<OneTimeWorkRequest>
。
3. 使用約束
假設(shè)我們需要將生成的圖片上傳到服務(wù)端陕赃,并且需要將圖片存儲(chǔ)到本地卵蛉,顯然,我們需要設(shè)備網(wǎng)絡(luò)條件良好并且有存儲(chǔ)空間么库,這時(shí)候傻丝,我們可以給WorkRequest
指明約束條件:
// 構(gòu)建約束條件
val constraints = Constraints.Builder()
.setRequiresBatteryNotLow(true) // 非電池低電量
.setRequiredNetworkType(NetworkType.CONNECTED) // 網(wǎng)絡(luò)連接的情況
.setRequiresStorageNotLow(true) // 存儲(chǔ)空間足
.build()
// 儲(chǔ)存照片
val save = OneTimeWorkRequestBuilder<SaveImageToFileWorker>()
.setConstraints(constraints)
.addTag(TAG_OUTPUT)
.build()
continuation = continuation.then(save)
可以指明的約束條件有:電池電量
、充電
诉儒、網(wǎng)絡(luò)條件
葡缰、存儲(chǔ)
和延遲
等,具體的可以使用的時(shí)候查看接口忱反。
以下則是我們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) {
//... 創(chuàng)建任務(wù)鏈
var continuation = workManager
.beginUniqueWork(
IMAGE_MANIPULATION_WORK_NAME,
ExistingWorkPolicy.REPLACE,
OneTimeWorkRequest.from(CleanUpWorker::class.java)
)
for (i in 0 until blurLevel) {
val builder = OneTimeWorkRequestBuilder<BlurWorker>()
if (i == 0) {
builder.setInputData(createInputDataForUri())
}
continuation = continuation.then(builder.build())
}
// 構(gòu)建約束條件
val constraints = Constraints.Builder()
.setRequiresBatteryNotLow(true) // 非電池低電量
.setRequiredNetworkType(NetworkType.CONNECTED) // 網(wǎng)絡(luò)連接的情況
.setRequiresStorageNotLow(true) // 存儲(chǔ)空間足
.build()
// 儲(chǔ)存照片
val save = OneTimeWorkRequestBuilder<SaveImageToFileWorker>()
.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()
}
//... 省略
}
第四步 取消任務(wù)
如果想取消所有的任務(wù)workManager.cancelAllWork()
泛释,當(dāng)然如果想取消我們上面執(zhí)行的唯一任務(wù),需要我們上面的唯一任務(wù)名:
class MeModel(val userRepository: UserRepository) : ViewModel() {
fun cancelWork() {
workManager.cancelUniqueWork(IMAGE_MANIPULATION_WORK_NAME)
}
}
第五步 觀察任務(wù)狀態(tài)
任務(wù)狀態(tài)的變化過(guò)程:
其中温算,
SUCCEEDED
怜校、FAILED
和CANCELLED
都屬于任務(wù)已經(jīng)完成。觀察任務(wù)狀態(tài)需要使用到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)
}
// ...省略
}
當(dāng)圖片處理的時(shí)候注竿,程序彈出加載框茄茁,圖片處理完成,程序會(huì)將圖片路徑保存到User
里的headImage
并存儲(chǔ)到數(shù)據(jù)庫(kù)中巩割,任務(wù)狀態(tài)觀測(cè)參見(jiàn)MeFragment
中的onSubscribeUi
方法:
class MeFragment : Fragment() {
private val TAG by lazy { MeFragment::class.java.simpleName }
// 選擇圖片的標(biāo)識(shí)
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
}
/**
* 初始化監(jiān)聽(tīng)器
*/
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
})
// 任務(wù)狀態(tài)的觀測(cè)
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()
}
})
}
/**
* 圖片選擇完成的回調(diào)
*/
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)
}
}
寫完以后裙顽,動(dòng)圖的效果就會(huì)出現(xiàn)了。
三宣谈、更多
選擇適合自己的Worker
谷歌提供了四種Worker
給我們使用愈犹,分別為:自動(dòng)運(yùn)行在后臺(tái)線程的Worker
、結(jié)合協(xié)程的CoroutineWorker
蒲祈、結(jié)合RxJava2
的RxWorker
和以上三個(gè)類的基類的ListenableWorker
甘萧。
由于本文使用的Kotlin
萝嘁,故打算簡(jiǎn)單的介紹CoroutineWorker
梆掸,其他的可以自行探索扬卷。
我們使用ShoeWorker
來(lái)從文件中讀取鞋子的數(shù)據(jù)并完成數(shù)據(jù)庫(kù)的插入工作,使用方式基本與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<List<Shoe>>() {}.type
val shoeList: List<Shoe> = 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()
}
}
}
四酸钦、總結(jié)
可以發(fā)現(xiàn)怪得,大部分的后臺(tái)任務(wù)處理,
WorkManager
都可以勝任卑硫,這也是我們需要學(xué)習(xí)WorkManger
的原因徒恋。本次WorkManger學(xué)習(xí)完畢,本人水平有限欢伏,難免有誤入挣,歡迎指正。Over~
參考文章:
《Android Jetpack - 使用 WorkManager 管理后臺(tái)任務(wù)》
《[譯] 從Service到WorkManager》
《官方文檔:Guide to background processing》
《谷歌實(shí)驗(yàn)室》
《官方文檔:WorkManager》
《WorkManager Basics》
??如果覺(jué)得本文不錯(cuò)硝拧,可以查看Android Jetpack
系列的其他文章:
第一篇:《即學(xué)即用Android Jetpack - Navigation》
第二篇:《即學(xué)即用Android Jetpack - Data Binding》
第三篇:《即學(xué)即用Android Jetpack - ViewModel & LiveData》
第四篇:《即學(xué)即用Android Jetpack - Room》
第五篇:《即學(xué)即用Android Jetpack - Paging》