前言
大家好,我是小益!在经过前两章对协程的介绍后,我们终于又回到了MVVM的封装。协程在Android开发中最常用的场景应该是网络请求了,其次是一些使用Thread
的场景,本章内容我们将着重介绍如何将协程与网络请求结合。
推荐
文章将率先在公众号「Code满满」与个人博客「李益的小站」上发布,快来关注吧!
一、viewModelScope的使用
自行创建协程
var uiScope = CoroutineScope(SupervisorJob() + Dispatchers.Main)
在上述代码中我们创建了一个协程并指定了这个协程是在主线程中工作,之后我们就可以使用前两章提到的launch
来操作了,如下:
uiScope.launch{
...
}
以上是我们创建协程的实现方式,我们可以通过指定Dispatchers
来决定协程到底在什么线程中工作,而其实Kotlin的协程核心库中也为我们提供封装好了的scope
,例如MainScope
,源码如下:
@Suppress("FunctionName")
public fun MainScope(): CoroutineScope = ContextScope(SupervisorJob() + Dispatchers.Main)
非常明显,Kotlin提供的MainScope
内部实现与我们自行创建的CoroutineScope
一模一样,MainScope
在一定程度上方便了我们创建协程。
lifecycle-viewmodel-ktx
知晓协程如何创建后,我们需要思考一个问题:协程主要的使用层是MVVM的哪一层?因为协程最主要的作用是用同步编码的方式来实现异步;既然有异步,那么直接操作UI的View层明显是不太适合使用协程的,剩下的ViewModel与Model层则都很适合添加协程封装。我们先从ViewModel开始添加协程,幸运的是Google已经考虑到了这一层,并为我们提供了相关依赖,导入方式如下:
implementation 'androidx.lifecycle:lifecycle-viewmodel-ktx:2.2.0'
在导入此依赖后,会为ViewModel
添加一个名为viewModelScope
的扩展函数,此函数会创建一个做了优化的协程,源码如下:
val ViewModel.viewModelScope: CoroutineScope
get() {
val scope: CoroutineScope? = this.getTag(JOB_KEY)
if (scope != null) {
return scope
}
return setTagIfAbsent(JOB_KEY,
CloseableCoroutineScope(SupervisorJob() + Dispatchers.Main.immediate))
}
拿到viewModelScope
后,我们就可以在BaseViewModel
添加如下代码:
abstract class BaseViewModel : ViewModel(), ViewModelLifecycle, ViewBehavior {
/**
* 在主线程中执行一个协程
*/
protected fun launchOnUI(block: suspend CoroutineScope.() -> Unit): Job {
return viewModelScope.launch(Dispatchers.Main) { block() }
}
/**
* 在IO线程中执行一个协程
*/
protected fun launchOnIO(block: suspend CoroutineScope.() -> Unit): Job {
return viewModelScope.launch(Dispatchers.IO) { block() }
}
}
二、与Retrofit的结合
目前在Android开发中,最主流的网络请求框架应该就是Retrofit+OkHttp+RxJava
这一套了。那么下面我们就使用Retrofit
来结合协程进行封装。在网络请求中,协程起的作用其实与RxJava
是一致的,所以如果在别处没有使用RxJava
的需求,此处可以不引入RxJava
,只需引入Retrofit+OkHttp
。
Interface
interface FlyInterface {
/**
* 获取文章列表
*/
@GET("article/")
suspend fun get_article_list(@Query("page_size") size: Int): ApiResponse>
}
interface
的改造非常简单,仅仅是在函数前加上suspend
修饰。
ApiResponse
abstract class HttpResponse(val code: Int, val msg: String, val data: T?) {
abstract fun isSuccess(): Boolean
}
class ApiResponse(code: Int, msg: String, data: T?) : HttpResponse(code, msg, data) {
override fun isSuccess(): Boolean {
return code == 0
}
}
ApiResponse
是上述interface
中函数的返回值,实现也非常简单。因为接口返回的数据格式一般都是统一的,例如:
{
"code": 0;
"message": "Success";
"data": {
...
}
}
所以,我们也需要将返回的数据格式用一个统一的数据模型来处理。
HttpError
我们可以事先定义一些事先常见的网络错误,方便后续使用。
enum class HttpError(val code: Int, @StringRes val message: Int) {
// 未知错误
UNKNOWN(-1, R.string.fly_http_error_unknow),
// 网络连接错误
CONNECT_ERROR(-2, R.string.fly_http_error_connect),
// 连接超时
CONNECT_TIMEOUT(-3, R.string.fly_http_error_connect_timeout),
// 错误的请求
BAD_NETWORK(-4, R.string.fly_http_error_bad_network),
// 数据解析错误
PARSE_ERROR(-5, R.string.fly_http_error_parse),
// 取消请求
CANCEL_REQUEST(-6, R.string.fly_http_cancel_request),
}
Retrofit
相信大部分同学在使用Retrofit时都会自己做二次封装的,此处就不附上详细的代码了,主要看关键代码,需要完整代码的可以去小益的Github上自行查看。
class BaseHttpClient {
......
/**
* 获取service对象
*
* @param service api所在的interface
*/
fun getService(service: Class): T {
var retrofitService: T? = serviceCache.get(service.canonicalName) as T
if (retrofitService == null) {
retrofitService = retrofitClient.create(service)
serviceCache.put(service.canonicalName, retrofitService)
}
return retrofitService!!
}
/**
* 建议调用此方法发送网络请求
* 因为协程中出现异常时,会直接抛出异常,所以使用try...catch方法捕获异常
*/
suspend fun requestSafely(
apiInterface: Class,
call: suspend (service: T) -> HttpResponse
): ParseResult {
try {
val s = getService(apiInterface)
val response = call(s)
return if (response.isSuccess()) {
ParseResult.Success(response.data)
} else {
ParseResult.Failure(response.code, response.msg)
}
} catch (ex: Throwable) {
return ParseResult.ERROR(ex, parseException(ex))
}
}
......
}
-
getService
:获取我们定义的interface
-
requestSafely
:此方法中最值得注意的是try...catch
,因为使用协程来进行网络请求时,如遇到问题会抛出异常,所以此处使用try...catch
捕获。另外,此方法也对返回的Response
做了简单的解析处理,并返回具体的ParseResult
ParseResult
sealed class ParseResult {
/* 请求成功,返回成功响应 */
data class Success(val data: T?) : ParseResult()
/* 请求成功,返回失败响应 */
data class Failure(val code: Int, var msg: String? = null) :
ParseResult()
/* 请求失败,抛出异常 */
data class ERROR(val ex: Throwable, val error: HttpError) : ParseResult()
private var successBlock: (suspend (data: T?) -> Unit)? = null
private var failureBlock: (suspend (code: Int, msg: String?) -> Unit)? = null
private var errorBlock: (suspend (ex: Throwable, error: HttpError) -> Unit)? = null
private var cancelBlock: (suspend () -> Unit)? = null
/**
* 设置网络请求成功处理
*/
fun doSuccess(successBlock: (suspend (data: T?) -> Unit)?): ParseResult {
this.successBlock = successBlock
return this
}
/**
* 设置网络请求失败处理
*/
fun doFailure(failureBlock: (suspend (code: Int, msg: String?) -> Unit)?): ParseResult {
this.failureBlock = failureBlock
return this
}
/**
* 设置网络请求异常处理
*/
fun doError(errorBlock: (suspend (ex: Throwable, error: HttpError) -> Unit)?): ParseResult {
this.errorBlock = errorBlock
return this
}
/**
* 设置网络请求取消处理
*/
fun doCancel(cancelBlock: (suspend () -> Unit)?): ParseResult {
this.cancelBlock = cancelBlock
return this
}
suspend fun procceed() {
when (this) {
is Success -> successBlock?.invoke(data)
is Failure -> failureBlock?.invoke(code, msg)
is ERROR -> {
if (this.error == HttpError.CANCEL_REQUEST) {
cancelBlock?.invoke()
} else {
errorBlock?.invoke(ex, error)
}
}
}
}
}
ParseResult
是对HttpResponse
解析后返回的类。ParseResult
解析HttpResponse
后出现三种返回:
-
Success
:继承于ParseResult
,网络请求成功并且返回的的Response状态也是成功,持有具体的Response数据 -
Failure
:继承于ParseResult
,网络请求成功但是返回的Response状态是失败,持有失败的Code码与Message -
Error
:继承于ParseResult
,网络请求异常,未成功,持有异常信息
在ParseResult
中do开头的函数都是设置对应处理的代码块,另外有个procceed
函数是真正执行响应处理。其中在对Error
处理时分为了两种情况:
- 一种是因为网络请求被取消产生的异常(经测试,网络请求取消会抛出取消异常)
- 另一种是非网络请求取消产生的异常
因为网络请求取消从一定程度上来说不应该当作错误处理,所以要分开处理;防止项目中对异常错误进行了集中处理,比如弹出toast提示,此时如果用户取消了网络请求,也弹出一个网络请求取消的提示,这样的用户体验就比较糟糕了。
具体使用
fun get_article_list() {
launchOnUI {
ApiClient.getInstance()
.requestSafely(FlyInterface::class.java) {
it.get_article_list(20)
}.doSuccess {
articleList.value = it!!.results
}
.doFailure { code, msg -> showToast(msg ?: "获取文章列表失败") }
.doError { ex, error -> showToast(error.message) }
.procceed()
}
}
此处的ApiClient
是BaseHttpClient
的子类即对Retrofit+OkHttp
的封装,并做了单例处理,整个请求流程呈现链式结构。虽然doSuccess
、doFailure
以及doError
看上去有些像回调,但其实都是同步的。我们完全可以这么写:
fun get_article_info() {
launchOnUI {
println(">>>>>> 开始")
var articles = ArrayList()
ApiClient.getInstance()
.requestSafely(FlyInterface::class.java) {
it.get_article_list(20)
}.doSuccess {
println(">>>>>> 第一次")
articles = it!!.results
}
.procceed()
ApiClient.getInstance()
.requestSafely(FlyInterface::class.java) {
it.get_article(articles[0].id)
}.doSuccess {
println(">>>>>> 第二次")
}
.procceed()
println(">>>>>> 结束")
}
}
先获取文章列表,再从文章列表中提取列表头部的文章ID用于获取文章详情,最后打印的结果为:
开始
第一次
第二次
结束
可以看出,完全是顺序执行。
请求并发
fun get_info() {
launchOnUI {
val listAsync = async {
var articles = ArrayList()
ApiClient.getInstance()
.requestSafely(FlyInterface::class.java) {
it.get_article_list(20)
}.doSuccess {
articles = it!!.results
}
.procceed()
return@async articles
}
val detailAsync = async {
var article: Article? = null
ApiClient.getInstance()
.requestSafely(FlyInterface::class.java) {
it.get_article(2)
}.doSuccess {
article = it
}
.procceed()
return@async article!!
}
val articles = listAsync.await()
val articleDetail = detailAsync.await()
}
}
使用async
实现并发,同时请求文章列表和文章详情,并获取对应的值。
三、老项目使用协程
协程很香,这毋庸置疑,但是对于一些已经使用了回调形式的网络请求的老项目来说,将所有的网络请求改为上述的协程形式是不现实的,而如果既想不改动原来的回调形式,又想使用协程,有没有办法呢?当然是有的!
首先我们看下回调形式下的网络请求:
HttpClient.getInstance().addGetDataCallback(url:String, object :SimpleAppGetCallback(){
override fun onSuccess(data: T?) {
}
override fun onFailure(code: Int, msg: String) {
}
override fun onError(ex: Throwable, error: HttpError) {
}
})
上述的代码形式应该是大部分网络请求回调的形式了,下面我们改造一下:
suspend fun , E> T.await(url:String) =
suspendCoroutine> { coroutine ->
HttpClient.getInstance().addGetDataCallback(url, object :SimpleAppGetCallback(){
override fun onSuccess(data: E?) {
coroutine.resume(ParseResult.Success(data))
}
override fun onFailure(code: Int, msg: String) {
coroutine.resume(ParseResult.Failure(code,msg))
}
override fun onError(ex: Throwable, error: HttpError) {
coroutine.resume(ParseResult.Error(code,msg))
}
})
}
上述改造中,我们对SimpleAppGetCallback
增加一个扩展函数await()
;await()
返回的是一个suspendCoroutine
(即一个协程,ParseResult是前面内容中提到的类),在suspendCoroutine
中进行了完整的回调式网络请求,并在回调中使用coroutine.resume()
方法将请求的结果传递给协程,最后看一下使用:
SimpleAppGetCallback().await("https://www.liyisite.com")
.doSuccess{ }
.doFailure{ code,msg -> }
.doError{ ex, error ->}
.procceed()
可以看到使用方法与我们定义的协程式请求几乎一样。
四、小结
本章内容主要是介绍协程在Android开发中的实际应用。因为文章主要是关于MVVM架构的,一些协程的特性并未详细讲解,比如父协程取消,未执行完毕的子协程也会被取消等等。感兴趣的同学可以自行去实践或者去看小益的Github项目【Fly-Android】。本文的全部代码已上传至Github,项目地址为:https://github.com/albert-lii/Fly-Android,欢迎大家关注!