上篇文章講了MVVM入門,網絡請求部分非常簡單和原始,本篇則是上一篇的進階,主要講解如何在vm中使用協程結合Retrofit進行網絡框架的封裝。
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