上篇文章讲了MVVM入门,网络请求部分非常简单和原始,本篇则是上一篇的进阶,主要讲解如何在vm中使用协程结合Retrofit进行网络框架的封装。
GitHub完整版:https://github.com/baiyuliang/MVVM
Retrofit自不必说,非常优秀的网络请求框架,说到Retrofit就不得不提RxJava,
RxJava是什么?
官方定义:一个在jvm上使用可观测的序列来组成异步的,基于事件的程序的库,它具有良好的链式编程风格,以及强大的异步处理能力,在近几年的移动开发中异常火爆,常结合Retrofit,以及MVP设计模式组成移动开发架构,搜索一下就会有大量相关文章呈现在你的眼前,不可否认,它是那么的优秀!Rxjava最重要的思想,就是观察者模式,我敢打赌,到现在还会有很多同学仍然没有理解观察者,订阅者这些到底是什么玩意,即便你已经使用RxJava几年了。我在这里只简单介绍下,你就会很好的理解了:
首先,定义接口(一个简单的例子):
ApiService.java
@GET("test")
Observable<HttpResult> test(@QueryMap HashMap<String, String> options);
我们可以看到这个方法的返回值Observable,我们可以将其简单的理解为被观察者,这个被观察者 就是接口请求方法,观察者需要观察的就是你这个方法请求的结果的变动。
当我们用Presenter去请求接口时,会有一个中间过程,一般用来传参,大致类似于:
TestNetUtil.java
public Observable<HttpResult> test(String id) {
LinkedHashMap<String, String> params = new LinkedHashMap<>();
params.put("id", id);
return ApiUtil.getInstance().getApiService().test(params);
}
注意看返回值,仍然是Observable,继续往下走就是在Presenter中调用了,大致类似于这样:
TestPresenter
public void test(String id) {
TestNetUtil.newInstance().test(id).subscribe(new Observer<HttpResult>() {
@Override
public void onSubscribe(Disposable d) {
}
@Override
public void onNext(HttpResult httpResult) {
}
@Override
public void onError(Throwable e) {
}
@Override
public void onComplete() {
}
});
}
这样就完成了一个接口请求,注意上面的方法,TestNetUtil.newInstance().test(id)我们知道,其返回值是Observable,我们说过这是一个被观察者,就是接口请求本身,subscribe订阅,该方法传入了一个Observer对象,Observer就是观察者,这个观察者订阅了(subscribe)被观察者(TestNetUtil.newInstance().test(id)),被观察者一旦接口请求结果完成,就会通知被观察者去处理结果,因为我订阅了你,你有任何变动我都会知道,这就是RxJava的观察者模式,而ViewModel中也是一样的道理!
好了,继续,本篇博客将不会去讲Rxjava与协程孰优孰劣,只讲如何使用协程,像RxJava一样,更高效的完成网络数据请求,当然,具体什么是协程,本篇也不会细讲,大家可自行查询相关文档!
网络请求框架封装第一步(老生常谈),创建ApiService:
interface ApiService {
@GET("banner/json")
suspend fun getBanner(): BaseResult<List<BannerBean>>
@GET("article/listproject/{page}/json")
suspend fun getArticleList(@Path("page") page: Int): BaseResult<ArticleListBean>
}
注意suspend ,这个是协程的重要关键字,简单的理解就是,它的作用等同于一个方法的回调(Callbak),举个例子:
fun test1():String{
var result
//...
return result
}
fun test2(param:String){
var result
//...
//需要使用test1的返回的结果
return result
}
看这两个方法,方法2需要使用方法1返回的结果,如果两个都是异步请求,通常的写法如下:
fun test(){
test1(object:CallBack(param){//请求完成回调
test2(param)//在回调中调用方法2,如果有方法3.4...,则进入了回调地狱
})
}
而使用suspend:则只需
suspend fun test(){
var param=test1()
var result=test2(param)
var xx=tests(result)
}
即可,大家可以搜索更多相关文章去深入理解,现在只要记住在接口方法前要加上这个字段!
第二步,配置Retrofit:
class RetrofitClient {
companion object {
fun getInstance() = SingletonHolder.INSTANCE
private lateinit var retrofit: Retrofit
}
private object SingletonHolder {
val INSTANCE = RetrofitClient()
}
init {
retrofit = Retrofit.Builder()
.client(getOkHttpClient())
.addConverterFactory(GsonConverterFactory.create())
.baseUrl(BASE_URL)
.build()
}
private fun getOkHttpClient(): OkHttpClient {
return OkHttpClient.Builder()
.connectTimeout(10, TimeUnit.SECONDS)
.writeTimeout(10, TimeUnit.SECONDS)
.addInterceptor(LoggingInterceptor())
.build()
}
fun create(): ApiService = retrofit.create(ApiService::class.java)
}
第三步,封装接口调用工具类HttpUtil:
class HttpUtil {
private val mService by lazy { RetrofitClient.getInstance().create() }
suspend fun getBanner() = mService.getBanner()
suspend fun getArticleList(page: Int) = mService.getArticleList(page)
/**
* 单例模式
*/
companion object {
@Volatile
private var httpUtil: HttpUtil? = null
fun getInstance() = httpUtil ?: synchronized(this) {
httpUtil ?: HttpUtil().also { httpUtil = it }
}
}
}
第四步,经过以上三步,其实已经初步完成了对Retrofit的封装,我们来在ViewModel中试着调用一下:
class MainViewModel : ViewModel() {
//MutableLiveData<>传入的数据类型,要与Service中BaseResult<>保证一致
var bannerData = MutableLiveData<List<BannerBean>>()
var articlesData = MutableLiveData<ArticleListBean>()
fun getBannerTest() {
viewModelScope.launch {
withContext(Dispatchers.IO) {//异步请求接口
val result = HttpUtil.getInstance().getBanner()
withContext(Dispatchers.Main) {
if (result.errorCode == 0) {//请求成功
bannerData.value = result.data
}
}
}
}
}
fun getArticleListTest(page:Int) {
viewModelScope.launch {
withContext(Dispatchers.IO) {//异步请求接口
val result = HttpUtil.getInstance().getArticleList(page)
withContext(Dispatchers.Main) {
if (result.errorCode == 0) {//请求成功
articlesData.value = result.data
}
}
}
}
}
}
虽然已经封装了HttpUtil,但我们可以看到,这样调用,每个方法中都要写launch ,以及对结果的处理,这并不是一个理想的网络框架封装,我们要做的是将launch部分提取出来,并对结果统一处理,那么写一个BaseViewModel,将网络请求过程放在BaseViewModel里面,不失为一个好办法。但是,要想写一个统一的网络请求方法,具体该怎么写呢?说到这里,就不得不说Kotlin语法的一个新特性,它可以将一个函数作为一个参数,传入另一个函数,跟js一毛一样,太方便了,举个例子:
fun test() {
log("test")
}
fun test2(t: () -> Unit) {
t()
}
test2({test()})
调用结果会执行log(“test”)!
-> Unit表示该方法无返回值;
-> 具体数据类型,则该方法返回该数据类型数据;
例如:
fun test() :String{
log("test")
return "result"
}
fun test2(t: () -> String) {
var result=t()
log(result)
}
调用 test2({test()})结果:
test
result
因此,我们可以将接口请求,也就是“HttpUtil.getInstance().getBanner()”这一部分作为一个参数传入统一请求方法中,具体如下:
fun launch(api: suspend CoroutineScope.() -> Unit) {
viewModelScope.launch {
withContext(Dispatchers.IO) {//异步请求接口
val result = api()
withContext(Dispatchers.Main) {
//处理result
}
}
}
}
调用的时候,只需:
launch { HttpUtil.getInstance().getBanner() }
是不是很简单,但还没有完成,因为我们没有对返回结果result进行处理,好办,再传入一个success方法作为结果处理回调:
fun <T>launch(
api: suspend CoroutineScope.() -> BaseResult<T>,
success:CoroutineScope.(T) -> Unit
) {
viewModelScope.launch {
withContext(Dispatchers.IO) {//异步请求接口
val result = api()
withContext(Dispatchers.Main) {
result.data?.let { success(it) }
}
}
}
}
调用:
launch({
HttpUtil.getInstance().getBanner()
},{
bannerData.value=it
})
注意泛型T,就是BaseResult中的data:
open class BaseResult<T> {
val errorMsg: String? = null
val errorCode: Int = 0
val data: T? = null
}
第二个参数方法,也就是success:
{
bannerData.value=it
}
中的it就是返回的data,貌似到此,ViewModel中的网络请求已经足够简单了,但是还不够理想,因为我连success回调都不想写(下面是最终完整版方法):
BaseViewModel:
open class BaseViewModel : ViewModel() {
val httpUtil by lazy { HttpUtil.getInstance() }
var isShowLoading = MutableLiveData<Boolean>()//是否显示loading
var errorData = MutableLiveData<ErrorResult>()//错误信息
private fun showLoading() {
isShowLoading.value = true
}
private fun dismissLoading() {
isShowLoading.value = false
}
private fun showError(error: ErrorResult) {
errorData.value = error
}
private fun error(errorResult: ErrorResult) {
showError(ErrorResult(errorResult.code, errorResult.errMsg))
}
/**
* 注意此方法传入的参数:api是以函数作为参数传入
* api:即接口调用方法
* error:可以理解为接口请求失败回调
* ->数据类型,表示方法返回该数据类型
* ->Unit,表示方法不返回数据类型
*/
fun <T> launch(
api: suspend CoroutineScope.() -> BaseResult<T>,//请求接口方法,T表示data实体泛型,调用时可将data对应的bean传入即可
liveData: MutableLiveData<T>,
isShowLoading: Boolean = false
) {
if (isShowLoading) showLoading()
viewModelScope.launch {
try {
withContext(Dispatchers.IO) {//异步请求接口
val result = api()
withContext(Dispatchers.Main) {
if (result.errorCode == 0) {//请求成功
liveData.value = result.data
} else {
error(ErrorResult(result.errorCode, result.errorMsg))
}
}
}
} catch (e: Throwable) {//接口请求失败
error(ErrorUtil.getError(e))
} finally {//请求结束
dismissLoading()
}
}
}
}
MainViewModel :
class MainViewModel : BaseViewModel() {
//MutableLiveData<>传入的数据类型,要与Service中BaseResult<>保证一致
var bannerData = MutableLiveData<List<BannerBean>>()
var articlesData = MutableLiveData<ArticleListBean>()
fun getBanner() {
launch({ httpUtil.getBanner() }, bannerData)
}
fun getArticleList(page: Int) {
launch({ httpUtil.getArticleList(page) }, articlesData, true)
}
}
到此,算是完成了一个真正具有使用价值的网络请求框架!
项目下载链接:https://download.csdn.net/download/baiyuliang2013/12361150