Kotlin + 協程 + Retrofit + MVVM優雅的實現網路請求

晴天大帥逼發表於2019-06-14

前言

最近一直閉關修煉Kotlin,說實話真香真好用,剛好公司準備交給我一個新專案,於是打算直接用Kotlin來構建專案。剛好整體架構搭建完畢了,於是把網路請求這一部分先分享給大家。這次使用到的是 協程+ retrofit +mvvm的模式,我這兒直接用一個簡單的demo來看一下具體的實現方式吧。文章只是描述實現思路,需要demo的直接跳到文末

專案配置

首先先引入所需要的依賴

        implementation 'android.arch.lifecycle:extensions:1.1.1'
        //協程
        implementation 'org.jetbrains.kotlinx:kotlinx-coroutines-core:1.1.1'
        implementation 'org.jetbrains.kotlinx:kotlinx-coroutines-android:1.1.1'
        //retrofit + okHttp3
        implementation 'com.squareup.retrofit2:retrofit:2.4.0'
        implementation 'com.squareup.retrofit2:converter-gson:2.4.0'
        implementation 'com.jakewharton.retrofit:retrofit2-kotlin-coroutines-adapter:0.9.2'
複製程式碼

實現思路

不管設計模式這些,先來一個簡單的網路請求,就retrofit的基本實現,看看需要哪些步驟

1.建立retrofit

~~~
    val retrofit = Retrofit.Builder()
                .baseUrl(RetrofitClient.BASE_URL)
                .addConverterFactory(GsonConverterFactory.create())
                .addCallAdapterFactory(CoroutineCallAdapterFactory())
                .build()
~~~
複製程式碼

2.建立service介面

~~~
    interface RequestService {
        @GET("wxarticle/chapters/json")
        fun getDatas() : Call<DataBean>
    }
~~~
複製程式碼

3.發起請求

~~~
    val service = retrofit.create(RequestService::class.java)
    service.getDatas().enqueue(object : Callback<DataBean> {
        override fun onFailure(call: retrofit2.Call<DataBean>, t: Throwable) {
            TODO("not implemented") //To change body of created functions use File | Settings | File Templates.
        }
        override fun onResponse(call: retrofit2.Call<DataBean>, response: Response<DataBean>) {
            TODO("not implemented") //To change body of created functions use File | Settings | File Templates.
        }
    })
~~~
複製程式碼

這只是描述了一個retrofit的簡單請求方式,實際專案中基本上都會封裝之後再使用,也為了提高程式碼的可讀性,降低各部分的耦合性, 通俗點來說,只有各司其職才能把工作幹好嘛,接下來我們們就圍繞著各司其職來一個一個實現

協程實現

接下來把上面的請求換成協程的方式來實現

1.建立RetrofitClient

object為了使RetrofitClient 只能有一個例項
~~~
    object RetrofitClient {
        val BASE_URL =  "https://wanandroid.com/"
        val reqApi by lazy {
            val retrofit = Retrofit.Builder()
                    .baseUrl(BASE_URL)
                    .addConverterFactory(GsonConverterFactory.create())
                    .addCallAdapterFactory(CoroutineCallAdapterFactory())
                    .build()
            return@lazy retrofit.create(RequestService::class.java)
        }
    }
~~~
複製程式碼

2.建立service介面類

~~~
interface RequestService {
    @GET("wxarticle/chapters/json")
    fun getDatas() : Deferred<DataBean>
}
~~~
複製程式碼

因為我們後續會使用到協程,所以這兒將Call換成了Deferred

3.發起請求

~~~
    GlobalScope.launch(Dispatchers.Main) {
        withContext(Dispatchers.IO){
          val dataBean = RetrofitClient.reqApi.getDatas().await()
        }
        //更新ui
    }
~~~
複製程式碼

上面用到了協程,這兒只講述他的應用了,具體的移步官方文件進一步瞭解。 網路請求在協程中,並且在IO排程單元,所以不用擔會阻塞主執行緒

協程 + ViewModel + LiveData實現

上面也只是簡單的實現,只不過是換成了協程,在專案中,還可以進一步封裝,方便使用前面也提到了MVVM,所以還用到了Android 新引入的元件架構之ViewModel和LiveData,先看ViewModel的實現

class ScrollingViewModel  : ViewModel() {
    private val TAG = ScrollingViewModel::class.java.simpleName
    private val datas: MutableLiveData<DataBean> by lazy { MutableLiveData<DataBean>().also { loadDatas() } }
    private val repository = ArticleRepository()
    fun getActicle(): LiveData<DataBean> {
        return datas
    }
    private fun loadDatas() {
        GlobalScope.launch(Dispatchers.Main) {
            getData()
        }
        // Do an asynchronous operation to fetch users.
    }
    private suspend fun getData() {
        val result = withContext(Dispatchers.IO){
//            delay(10000)
            repository.getDatas()
        }
       datas.value = result
    }
}
複製程式碼

ViewModel將作為View與資料的中間人,Repository專職資料獲取,下面看一下Repository的程式碼,用來發起網路請求獲取資料

 class ArticleRepository {
     suspend fun getDatas(): DataBean {
          return RetrofitClient.reqApi.getDatas().await()
      }
  }
複製程式碼

在Activity中程式碼如下

    private fun initData() {
        model.getActicle().observe(this, Observer{
            //獲取到資料
            toolbar.setBackgroundColor(Color.RED)
        })
    }
複製程式碼

後續優化

1.記憶體洩漏問題解決方案

結和了各位大佬們的意見,將使用GlobalScope可能會出現記憶體洩漏的問題進行了優化。因為在協程進行請求的過程中,若此時ViewModel銷燬,裡面的協程正在請求的話,將無法銷燬,出現記憶體洩漏,所以在ViewModel onCleared 裡面,即使結束協程任務,參考程式碼如下。

 open class BaseViewModel : ViewModel(), LifecycleObserver{
    private val viewModelJob = SupervisorJob()
    private val uiScope = CoroutineScope(Dispatchers.Main + viewModelJob)
    //執行在UI執行緒的協程
    fun launchUI( block: suspend CoroutineScope.() -> Unit) {
        try {
            uiScope.launch(Dispatchers.Main) {
                block()
            }
        }catch (e:Exception){
            e.printStackTrace()
        }
    }
    override fun onCleared() {
        super.onCleared()
        viewModelJob.cancel()
    }
}
複製程式碼

當然,最好的方式是使用viewModelScope,但是我在引入該包的時候,會報錯,由於最近比較忙暫時還沒來得急解決,後續問題有時間我也會繼續修改,還望各位大佬能幫忙指點

2.優化請求程式碼

先看下之前的請求程式碼

private suspend fun getData() {
        val result = withContext(Dispatchers.IO){
//            delay(10000)
            repository.getDatas()
        }
       datas.value = result
    }
複製程式碼

每一次都需要寫個withContext(),實際運用中,感覺有點不方便,於是乎想了一下,怎麼才能給他封進請求方法裡面? 程式碼如下

open class BaseRepository {
    suspend fun <T : Any> request(call: suspend () -> ResponseData<T>): ResponseData<T> {
        return withContext(Dispatchers.IO){ call.invoke()}
    }
}
複製程式碼

通過在BaseRepository裡面寫了一個專門的請求方法,這樣每次只需執行request就行了 請求參考如下

class ArticleRepository : BaseRepository() {
    suspend fun getDatas(): ResponseData<List<Data>> {
       return request {
           delay(10000)
           Log.i(ScrollingViewModel::class.java.simpleName,"loadDatas1 run in  ${Thread.currentThread().name}")
           RetrofitClient.reqApi.getDatas().await() }
    }
}
複製程式碼

注:這個 delay(10000)只是我測試用的,意思是休眠當前協程,防止萌新在自己專案中加上了,還是有必要說一下的

再看看ViewModel中就太簡單了

class ScrollingViewModel : BaseViewModel() {
    private val TAG = ScrollingViewModel::class.java.simpleName
    
    private val datas: MutableLiveData<List<Data>> by lazy { MutableLiveData<List<Data>>().also { loadDatas() } }
    
    private val repository = ArticleRepository()
    fun getActicle(): LiveData<List<Data>> {
        return datas
    }
    
    private fun loadDatas() {
        launchUI {
            Log.i(TAG,"loadDatas1 run in  ${Thread.currentThread().name}")
            val result = repository.getDatas()
            Log.i(TAG,"loadDatas3 run in  ${Thread.currentThread().name}")
            datas.value = result.data
        }
        // Do an asynchronous operation to fetch users.
    }
}
複製程式碼

注意看請求部分,就兩句話,一句發起請求val result = repository.getDatas(),然後就是為我們的LiveData賦值了,看起有沒有同步程式碼的感覺,這就是協程的魅力所在,為了驗證我們的請求沒有阻塞主執行緒,我列印了日誌

06-19 12:26:35.736 13648-13648/huaan.com.mvvmdemo I/ScrollingViewModel: loadDatas start run in  main
06-19 12:26:45.743 13648-13684/huaan.com.mvvmdemo I/ScrollingViewModel: request run in  DefaultDispatcher-worker-1
06-19 12:26:46.227 13648-13648/huaan.com.mvvmdemo I/ScrollingViewModel: loadDatas end  run in  main
複製程式碼

看到了吧,各司其職,效果很棒

異常處理

搞了半天才發現沒有弄異常處理,當請求失敗之後,專案就崩潰了,這不是是我們想要的結果,由於好沒有想到更好的處理方式,只能在外面套個tyr catch 頂一頂了,參考如下

open class BaseViewModel : ViewModel(), LifecycleObserver{
    private val viewModelJob = SupervisorJob()
    private val uiScope = CoroutineScope(Dispatchers.Main + viewModelJob)
    private val error by lazy { MutableLiveData<Exception>() }
    private val finally by lazy { MutableLiveData<Int>() }
    //執行在UI執行緒的協程
    fun launchUI( block: suspend CoroutineScope.() -> Unit) {
        uiScope.launch(Dispatchers.Main) {
            try {
                block()
            }catch (e:Exception){
                error.value = e
            }finally {
                finally.value = 200
            }
        }
    }
    override fun onCleared() {
        super.onCleared()
        viewModelJob.cancel()
    }
    /**
     * 請求失敗,出現異常
     */
    fun getError(): LiveData<Exception> {
        return error
    }
    /**
     * 請求完成,在此處做一些關閉操作
     */
    fun getFinally(): LiveData<Int> {
        return finally
    }
}
複製程式碼

更新Retrofit 2.6.0

看了之前評論區大佬的建議,將Retrofit更新到了2.6.0,同時這次更新了viewModelScope 來管理協程, 專案換成了androidx,這樣一來看起來就很賞心悅目了,下面貼一點修改的地方,看一下

    //執行在UI執行緒的協程
    fun launchUI(block: suspend CoroutineScope.() -> Unit) = viewModelScope.launch {
        try {
            block()
        } catch (e: Exception) {
            error.value = e
        } finally {
            finally.value = 200
        }
    }
複製程式碼

修改為viewModelScope後就不需要像之前一樣,在onCleared取消協程了,因為這一些列操作,他已經幫我們完成了

interface RequestService {
    @GET("wxarticle/chapters/json")
   suspend fun getDatas() : ResponseData<List<Data>>
}

suspend fun getDatas(): ResponseData<List<Data>> = request {
    RetrofitClient.reqApi.getDatas()
}
複製程式碼

請求介面宣告可以直接申明為suspend 同時取消掉返回Deferred,請求方法中也可以去掉await()因為retrofit2.6.0內部可以支援協程,也就不需要我們再處理了

結語

上面只是描述了一些實現過程,具體使用還得參考demo,基本上能滿足大部分的需求,要是感興趣的小夥伴,可以下載demo參考,感覺不錯的話,順手點個贊就很滿足了。於所學不精,可能會有使用不當之處,希望各位大佬能指出不當的地方,深表感謝。

附上專案地址

github.com/qingtian521…

相關文章