Android开发之MVVM模式实践:协程与网络请求的结合
Posted 码农 小生
tags:
篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了Android开发之MVVM模式实践:协程与网络请求的结合相关的知识,希望对你有一定的参考价值。
前言
大家好,我是小益!在经过前两章对协程的介绍后,我们终于又回到了 MVVM的封装 。协程在android开发中最常用的场景应该是网络请求了,其次是一些使用 Thread 的场景,本章内容我们将着重介绍如何将协程与网络请求结合。
一、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<CommonListDto<Article>>
}
interface 的改造非常简单,仅仅是在函数前加上 suspend 修饰。
ApiResponse
abstract class HttpResponse<T>(val code: Int, val msg: String, val data: T?) {
abstract fun isSuccess(): Boolean
}
class ApiResponse<T>(code: Int, msg: String, data: T?) : HttpResponse<T>(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 <T> getService(service: Class<T>): 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 <T : Any, D : Any> requestSafely(
apiInterface: Class<T>,
call: suspend (service: T) -> HttpResponse<D>
): ParseResult<D> {
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<out T : Any> {
/* 请求成功,返回成功响应 */
data class Success<out T : Any>(val data: T?) : ParseResult<T>()
/* 请求成功,返回失败响应 */
data class Failure(val code: Int, var msg: String? = null) :
ParseResult<Nothing>()
/* 请求失败,抛出异常 */
data class ERROR(val ex: Throwable, val error: HttpError) : ParseResult<Nothing>()
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<T> {
this.successBlock = successBlock
return this
}
/**
* 设置网络请求失败处理
*/
fun doFailure(failureBlock: (suspend (code: Int, msg: String?) -> Unit)?): ParseResult<T> {
this.failureBlock = failureBlock
return this
}
/**
* 设置网络请求异常处理
*/
fun doError(errorBlock: (suspend (ex: Throwable, error: HttpError) -> Unit)?): ParseResult<T> {
this.errorBlock = errorBlock
return this
}
/**
* 设置网络请求取消处理
*/
fun doCancel(cancelBlock: (suspend () -> Unit)?): ParseResult<T> {
this.cancelBlock = cancelBlock
return this
}
suspend fun procceed() {
when (this) {
is Success<T> -> 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<Article>()
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<Article>()
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<T>(){
override fun onSuccess(data: T?) {
}
override fun onFailure(code: Int, msg: String) {
}
override fun onError(ex: Throwable, error: HttpError) {
}
})
上述的代码形式应该是大部分网络请求回调的形式了,下面我们改造一下:
suspend fun <T : SimpleAppGetCallback<E>, E> T.await(url:String) =
suspendCoroutine<ParseResult<E>> { coroutine ->
HttpClient.getInstance().addGetDataCallback(url, object :SimpleAppGetCallback<E>(){
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<E>> (即一个协程, ParseResult 是前面内容中提到的类),在 suspendCoroutine 中进行了完整的回调式网络请求,并在回调中使用 coroutine.resume() 方法将请求的结果传递给协程,最后看一下使用:
SimpleAppGetCallback<UserInfo>().await("https://www.liyisite.com")
.doSuccess{ }
.doFailure{ code,msg -> }
.doError{ ex, error ->}
.procceed()
可以看到使用方法与我们定义的协程式请求几乎一样。
四、小结
本章内容主要是介绍协程在Android开发中的实际应用。因为文章主要是关于MVVM架构的,一些协程的特性并未详细讲解,比如父协程取消,未执行完毕的子协程也会被取消等等。
以上是关于Android开发之MVVM模式实践:协程与网络请求的结合的主要内容,如果未能解决你的问题,请参考以下文章