Kotlin + 协程 + Retrofit + MVVM优雅的实现网络请求

前言

最近一直闭关修炼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…

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 215,133评论 6 497
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 91,682评论 3 390
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 160,784评论 0 350
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 57,508评论 1 288
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 66,603评论 6 386
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 50,607评论 1 293
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 39,604评论 3 415
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 38,359评论 0 270
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 44,805评论 1 307
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 37,121评论 2 330
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 39,280评论 1 344
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 34,959评论 5 339
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 40,588评论 3 322
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 31,206评论 0 21
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,442评论 1 268
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 47,193评论 2 367
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 44,144评论 2 352

推荐阅读更多精彩内容