在软件开发过程中,我们常常会遇到各种复杂的逻辑场景,尤其是在涉及到存储权限获取、弹窗交互以及网络请求等操作时,传统的回调方式往往会导致代码嵌套层级过深,使得代码的可维护性和可读性大打折扣。本文将深入探讨如何利用协程来解决这类问题,并对比 RxJava 的实现方式,展现协程在简化代码结构方面的优势。
一、复杂逻辑背景与 RxJava 实现的困境
在最近的项目需求中,存在这样一个复杂的业务逻辑:用户设置简历头像的过程涉及到多个关键步骤,包括更新头像操作、存储权限检查、图片加载以及相册保存等,并且每个步骤在原工程中都是通过回调函数来处理的。若直接采用传统的回调方式编写代码,必然会形成多层嵌套的回调结构,这无疑会增加代码的复杂性和维护难度。
graph TD;
start["开始"] --> setAvatar["设置简历头像"];
setAvatar -->|成功| hasPermission{是否有相册存储权限?};
setAvatar -->|失败| toastError["toast提示用户:操作失败,请重试"];
hasPermission -->|是| saveToAlbum["将图片保存至相册"];
hasPermission -->|否| requestPermission["弹出系统弹窗请求权限"];
saveToAlbum -->|结果| saveResult{保存是否成功?};
requestPermission -->|同意| saveToAlbum;
requestPermission -->|不同意| denyAction{用户点击去设置?};
saveResult -->|成功| closePopup["关闭证件照弹窗"];
saveResult -->|失败| keepPopupOpen["不关闭证件照弹窗"];
denyAction -->|是| goToSettings["跳转设置页"];
denyAction -->|否| unifiedClose["关闭证件照弹窗并提示已设为简历头像"];
goToSettings --> unifiedClose;
toastError --> End["结束"];
closePopup --> End;
keepPopupOpen --> End;
unifiedClose --> End;
为了应对这一问题,我们首先尝试使用 RxJava 来解决。以下是使用 RxJava 实现的第一版代码:
disposables += updateUserAvatar(resumeId, currentSelectPhoto).flatMap {
if (it) {
requestWritePermission()
} else {
ToastUtils.showToast("操作失败,请重试")
//通常来说这里 throw 一个特定的 Exception 即可,强行走到 onError
Observable.just(Result.failure(StopFlowException("updateUserAvatar is failure")))
}
}.flatMap {
if (it.getOrNull() == true) {
// true有权限
loadSelectedPhoto(currentSelectPhoto)//加载图片
} else {
//拒绝权限
Observable.just(Result.failure(it.exceptionOrNull()?: Exception("result is failure")))
}
}.flatMap { bitmapResult ->
val bitmap = bitmapResult.getOrNull()
if (bitmap == null) {
Observable.just(Result.failure(bitmapResult.exceptionOrNull()?: Exception("bitmap is null")))
} else {
Observable.zip(
itemWidthSubject,
Observable.just(bitmap)
) { itemWidth, b ->
Result.success(DownloadPhotoBean(itemWidth, b))
}
}
}.subscribe({ downloadBeanResult ->
val downloadBean = downloadBeanResult.getOrNull()
isActionClickExecuting = false
if (downloadBean!= null) {
if (isShowing) {
dismiss()
}
val aiBitmap = downloadBean.bitmap
try {
val waterBitmap = AlbumHelper.addWatermark(
aiBitmap
)
val addAlbumSuccess = AlbumHelper.addBitmapToAlbum(
mContext,
waterBitmap
)
if (addAlbumSuccess) {
ToastUtils.showToast("保存并设置成功")
} else {
ToastUtils.showToast("设置头像成功,保存相册失败,请重试")
}
} catch (e: Exception) {
ToastUtils.showToast("设置头像成功,保存相册失败,请重试")
}
} else {
if (downloadBeanResult.exceptionOrNull() is StopFlowException) {
//nothing
} else {
if (isShowing) {
dismiss()
}
ToastUtils.showToast("已设为简历头像")
}
}
}, {
isActionClickExecuting = false
})
在上述代码中,updateUserAvatar 方法的结果通过 flatMap 操作符进行处理,如果更新头像成功则继续请求写权限,否则根据情况进行错误处理并发送特定的失败结果。后续对于权限获取结果、图片加载结果以及最终的组合操作结果都通过类似的 flatMap 操作符和 subscribe 方法进行链式处理。
然而,仔细分析这段代码会发现一些问题。在 RxJava 中,subscribe({},{}) 方法是整个操作链条的终点,上游发送的信息最终都在这个终点进行处理。若要提前结束流程并走到终点,通常需要手动抛出一个异常,但这种方式在捕获机制不完善或存在错误统计指标时容易引发错乱。而且,如果不使用抛异常的方式,就需要在中间层层层传递特殊标识来提前结束流程,例如上述代码中使用 Observable.just 发送特殊异常的方式,这使得中间层代码变得复杂,因为它本不应过多关注提前结束流程的逻辑。
二、协程的解决方案与优势
为了克服 RxJava 实现中的这些问题,我们引入协程来重构代码。首先,我们创建了一个 MutableSharedFlow 来保存测量 view 宽度的结果,因为它本身也是一个回调操作,使用 SharedFlow 可以方便地在协程中进行数据共享和处理。
private val itemWidthFlow = MutableSharedFlow<Int>(replay = 1, onBufferOverflow = BufferOverflow.DROP_OLDEST)
以下是使用协程实现的核心代码:
scope.launch {
val updateAvatarSuccess = withContext(Dispatchers.IO) {
updateUserAvatar(resumeId, currentSelectPhoto)
}
if (updateAvatarSuccess) {
onUpdateAvatarSuccess(currentSelectPhoto)
val hasPermission = requestWritePermission()
if (hasPermission == RequestPermissionResult.SUCCESS) {
try {
val bitmap = loadSelectedPhoto(currentSelectPhoto)!!
val itemWidth = itemWidthFlow.first()
val downloadBean = DownloadPhotoBean(itemWidth, bitmap)
val saveSuccess = handleDownloadSuccess(downloadBean)
if (saveSuccess) {
if (isShowing) {
dismiss()
}
ToastUtils.showToast("设置头像成功,已保存到相册")
} else {
ToastUtils.showToast("设置头像成功,保存相册失败,请重试")
}
} catch (e: Exception) {
ToastUtils.showToast("设置头像成功,保存相册失败,请重试")
}
} else {
if (isShowing) {
dismiss()
}
ToastUtils.showToast("已设置为简历头像")
}
} else {
if (isShowing) {
dismiss()
}
ToastUtils.showToast("操作失败,请重试")
}
isActionClickExecuting = false
}
在协程版本中,updateUserAvatar 函数在 IO 协程上下文中执行,其结果直接通过条件判断进行处理,而不是像 RxJava 那样通过复杂的操作符链。requestWritePermission 函数也被定义为一个挂起函数,通过 suspendCancellableCoroutine 来暂停和恢复协程执行,根据权限获取结果进行相应处理,避免了回调嵌套和手动异常处理的复杂性。
private suspend fun requestWritePermission(): RequestPermissionResult = suspendCancellableCoroutine { continuation ->
PermissionsManager.getInstance().requestPermissionsIfNecessaryForResult(
fragment,
arrayOf("android.permission.WRITE_EXTERNAL_STORAGE"),
object : GoSettingPermissionsResultAction() {
override fun onGranted() {
continuation.resume(RequestPermissionResult.SUCCESS)
}
override fun onDenied(permission: String?) {
continuation.resume(RequestPermissionResult.DENY)
}
override fun onSetting(permission: Array<out String>?) {
continuation.resume(RequestPermissionResult.SETTING)
}
})
通过协程的方式,代码结构变得更加清晰和简洁。不再需要复杂的操作符链来处理异步操作结果,而是通过顺序的条件判断和挂起函数调用,使得代码的逻辑流程更加直观,易于理解和维护。
三、总结与展望
在处理复杂逻辑时,协程为我们提供了一种强大的工具来消除回调嵌套,简化代码结构。与 RxJava 相比,协程在处理异步操作和错误处理方面具有独特的优势,能够使代码更加符合人类的思维逻辑,降低维护成本。在未来的开发中,我们应充分利用协程的特性,探索更多在不同场景下的应用,进一步提升代码质量和开发效率。
希望本文能够帮助读者理解协程在消除回调方面的强大功能,并在实际项目中得到应用和推广。