目前我们主app的网络请求,在请求完成后普遍采用callback的形式,回调给主线程执行对应的操作。
以下是一个页面内最常见的网络请求写法(按照目前封装的最简写法,已包含数据解析操作):
java
复制代码
private fun requestData() {
onPageLoadingCompleted(LoadCompleteType.LOADING) // 请求前显示loading状态
val url = MainUrlConstants.getInstanse().hotCommentUniversalUrl // 定义请求地址
val params = mapOf("param1" to "hello", "param2" to "world") // 定义请求参数
CommonRequestM.getData(url, params, CommentModel::class.java, object : IDataCallBack<CommentModel> {
override fun onSuccess(data: CommentModel?) { // 成功回调
if (!canUpdateUi()) { // 这一步判断页面是否已销毁,如果已销毁就不应该继续执行UI操作
return
}
onPageLoadingCompleted(LoadCompleteType.OK) // 请求完成,停止loading状态
refreshUI(data) // ..接下来执行刷新UI操作
}
override fun onError(code: Int, message: String?) { // 失败回调
if (!canUpdateUi()) { // 同成功回调,做同样的页面状态判断
return
}
onPageLoadingCompleted(LoadCompleteType.OK) // 同成功回调,也做停止loading操作
CustomToast.showFailToast(message)
}
})
}
注意到在这样一个请求框架里面,有两项操作在成功和失败的回调方法里重复执行了,一项是确认页面是否销毁的 canUpdateUi() 判断 ,另一项是只要请求结束都要执行的公共方法 onPageLoadingCompleted(LoadCompleteType.OK)
相信写惯了这样的回调式代码的同学,对于这样的流水线式写法已经习以为常了,那么我们有没有办法简化这样的重复性强的代码呢?
下面看一下改造为协程后的写法:
java
复制代码
private fun requestData() {
onPageLoadingCompleted(LoadCompleteType.LOADING)
mMainScope.launch { // 创建一个协程
val url = MainUrlConstants.getInstanse().hotCommentUniversalUrl // 定义请求地址
val params = mapOf("param1" to "hello", "param2" to "world") // 定义请求参数
val reqResult = CoroutineRequest.getData(url, params, CommentModel::class.java) // 使用挂起函数切到IO线程执行
onPageLoadingCompleted(LoadCompleteType.OK) // 挂起函数返回结果后不需要判断canUpdateUi,取消loading操作只要写一次
if (reqResult.isSuccess()) { // 成功回调
refreshUI(reqResult.model)
} else { // 失败回调
CustomToast.showFailToast(reqResult.msg)
}
}
}
对比一下原来的写法和采用协程后的写法,发现明显简洁了很多,原因一是没有了两个回调,改为了更加清晰直接的顺序执行流程,取消loading这样的公共代码只需写一次;原因二是没有了两次canUpdateUi判断。
下面分析为什么能做如上的简化。
java
复制代码
mMainScope.launch {
....
}
这里的mMainScope是一个自己定义在页面内的基于主线程的协程作用域,在页面销毁后取消该作用域,也就做到了取消了基于mMainScope创建的所有协程
javascript
复制代码
private val mMainScope = MainScope()
java
复制代码
override fun onDestroy() {
super.onDestroy()
mMainScope.cancel()
}
下面重点看下为什么在请求完成后,如果页面已经destroy了,不用判断canUpdateUi
java
复制代码
val reqResult = CoroutineRequest.getData(url, params, CommentModel::class.java) // 使用挂起函数切到IO线程执行
// 不需要判断canUpdateUi
onPageLoadingCompleted(LoadCompleteType.OK)
进入CoroutineRequest
java
复制代码
object CoroutineRequest {
suspend fun <T> getData(url: String?, params: Map<String, String>?, cls: Class<T>): SyncRequestResult<T> {
return withContext(Dispatchers.IO) {
CommonRequestM.getDataSync(url, params, cls)
}
}
}
注意到这个getData方法加了suspend关键字,并且实现体里面使用了withContext方法把当前协程切到IO线程调度执行,这就是协程里所谓的“挂起函数”。挂起函数不同于普通函数,进入挂起函数后,所在的协程会挂起,让出CPU执行权,直到挂起函数返回结果后继续执行后面的代码。并且挂起函数有个特性,一旦挂起函数执行完成后,会先判断协程是否已取消,如果已取消就不会执行后面的代码了。我们在页面onDestroy时已经取消了该协程,故而getData返回后,onPageLoadingCompleted开始的代码就不会执行了,也就不需要canUpdateUi这一判断了。
另外,我们在这里不再使用回调的写法,而是看上去类似于简单的“取数”写法,按顺序执行代码逻辑,因而在“取数”完成后,多个回调的公共部分可以一次性执行,而后再判断网络返回结果成功与否执行不同的逻辑。
(注:此处的挂起函数,另一个方案是使用suspendCoroutine,直接基于原有的回调改造执行流程。这里为了充分利用协程可以自由切换所在线程的特性故绕过回调)
java
复制代码
@NonNull
public static <T> SyncRequestResult<T> getDataSync(String url, Map<String, String> params, Class<T> cls) {
Request request;
try {
Request.Builder builder = BaseBuilder.urlGet(url, params, true);
request = CommonRequestM.getInstanse().addHeader(builder, params, url, null).build();
} catch (XimalayaException e) {
return new SyncRequestResult<>(BaseCall.ERROR_CODE_DEFALUT, BaseCall.getDefaultNetErrorContent(), null);
}
SyncRequestResult<String> syncResult = doSyncNoCallback(request, url, params, null); //注意这行,调用了无回调的同步请求方法
return new SyncRequestResult<>(syncResult.getRet(), syncResult.getMsg(), parseData(cls, syncResult.getModel()));
}
这里在CommonRequestM里加了一个不带线程切换逻辑的,且没有callback的请求方法,这里返回的是完整的请求结果,包括ret、msg(错误情况返回),以及解析后的data数据(成功才返回)。这样根据这个完整的数据model,按需处理成功或者失败的情况。
java
复制代码
data class SyncRequestResult<T>(
val ret: Int,
val msg: String? = null,
val model: T? = null
) {
fun isSuccess(): Boolean {
return ret == 0
}
}
在实际开发当中,我们发送的请求很多都不需要处理错误情况,只是做简单的数据获取。由于上面的数据类的model字段仅在成功时返回,这时我们可以省略isSuccess()判断,进一步简化代码,类似于下面这种
javascript
复制代码
mMainScope.launch {
val url = MainUrlConstants.getInstanse().hotCommentUniversalUrl
val params = mapOf("param1" to "hello", "param2" to "world")
val resultModel = CoroutineRequest.getData(url, params, CommentModel::class.java).model
refreshUI(resultModel)
}
用协程处理同时发起的多个请求
在开发中有时会遇到这样的场景,比如我们要依赖多个请求返回的结果来共同决定如何刷新UI,这时会怎么处理?
如果采用常规的回调式写法,由于各个请求是完全独立的,我们并不知道什么时候所有请求都回调完成,在每个请求返回时也不知道其他的请求返回了啥。这种情况下我们的代码逻辑就会变得极度复杂。
这时可能最简单的做法是挨个发起请求,上一个请求完成再执行下一个,在最后一个请求完成后刷新UI就没有问题,但是这会明显拉长请求时间,影响用户体验。
这时我们协程的 组合挂起函数 就派上用场了。
以下是在月票弹窗里的一个实际案例,月票弹窗初始化时需要同时请求限时福利信息接口,决定显示几个tab,以及请求月票信息接口,决定tab内的显示内容。为了保证页面渲染的流畅性,这里放弃了先后请求的做法,而是同时请求,等两个接口都返回后确定渲染内容。
java
复制代码
private fun requestMainData() {
mLoadingView.visibility = View.VISIBLE
mMainScope.launch {
val preVoteRequest = async(Dispatchers.IO) { requestPreVoteInfoSync() } // 请求1,创建子协程,立即发起
val welfareRequest = async(Dispatchers.IO) { requestWelfareInfoSync() } // 请求2,创建另一个子协程,立即发起
val preVoteInfo = preVoteRequest.await().model // 调用await方法会挂起外层launch创建的协程,等待请求1完成继续往下执行
val welfareInfo = welfareRequest.await().model // 由于两个请求同时发起,如果请求2先于请求1返回,await会立即返回,否则继续挂起直到请求2完成
mLoadingView.visibility = View.GONE // 走到这一行时,两个请求的返回结果都已经拿到,接下来就是拿着这两份数据执行UI刷新逻辑
initViewPager(welfareInfo != null)
if (preVoteInfo != null) {
// tab内渲染代码
} else {
showNetworkError()
}
}
}
private fun requestPreVoteInfoSync(): SyncRequestResult<PreVoteInfo> {
val params = mapOf("albumId" to "$mAlbumId")
return CommonRequestM.getDataSync(
"${UrlConstants.getInstanse().monthlyVotePreInfo()}${System.currentTimeMillis()}",
params, PreVoteInfo::class.java
)
}
private fun requestWelfareInfoSync(): SyncRequestResult<MonthlyTicketAlbumWelfareVo> {
val url = MainUrlConstants.getInstanse().queryListenWelfare(mAlbumId)
val params: MutableMap<String, String> = ArrayMap()
// ..省略设置请求参数代码
return CommonRequestM.getDataSync(url, params, MonthlyTicketAlbumWelfareVo::class.java)
}
这里使用了两个async函数,创建了两个预期带返回结果的子协程,分别在这两个async函数里发起请求。这两个子协程内的请求代码,可以认为是同时开始执行的。等两个await()挂起都完成时,这两个请求无论先后肯定都已经返回了。这样就做到了组合多个请求的返回结果。
继续改进
注意到我们在每个页面需要手动创建协程作用域,以及手动取消作用域
java
复制代码
private val mMainScope = MainScope()
override fun onDestroy() {
super.onDestroy()
mMainScope.cancel()
}
其实我们可以引入Kotlin的lifecycle扩展库,这样在Activity或Fragment内,可以改用LifecycleOwner的lifecycleScope扩展属性,取代MainScope,与页面的生命周期直接绑定,自动执行上述的创建和取消协程作用域的代码
java
复制代码
implementation("androidx.lifecycle:lifecycle-runtime-ktx:2.3.1")
java
复制代码
lifecycleScope.launch {
// 执行协程内处理代码
}