协程消除回调:从复杂逻辑到简洁实现

384 阅读5分钟

在软件开发过程中,我们常常会遇到各种复杂的逻辑场景,尤其是在涉及到存储权限获取、弹窗交互以及网络请求等操作时,传统的回调方式往往会导致代码嵌套层级过深,使得代码的可维护性和可读性大打折扣。本文将深入探讨如何利用协程来解决这类问题,并对比 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 相比,协程在处理异步操作和错误处理方面具有独特的优势,能够使代码更加符合人类的思维逻辑,降低维护成本。在未来的开发中,我们应充分利用协程的特性,探索更多在不同场景下的应用,进一步提升代码质量和开发效率。

希望本文能够帮助读者理解协程在消除回调方面的强大功能,并在实际项目中得到应用和推广。