MVVM+Retrofit+Kotlin網絡框架封裝

上篇文章講了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

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章