Coroutines + Retrofit + MVVM 简单实现

前言

为什么使用协程

  • 协程比线程小

    我: 为什么已经有Rx了还要在这里用协程?

    某同事: 因为协程比线程小,可以开很多个。

    我:…… 为什么比线程小?

  我维基了一下,确实有说比线程更小。

  但是看了一些源码,也是线程池 + 线程实现的,这时就开始有了疑惑,为什么同样是线程,怎么就说是比线程小的东西呢?

  直到看到了Benny大佬的文章 协程为什么被称为『轻量级线程』解释,我清晰了。通过测验,确实启动成千上万个协程也不会出现OOM或者其他问题。

  • 当然最主要的还是代码上的体验
    现在Kotlin越来越普遍,各种inline函数,操作符也都基本可以替换Rx的常用操作符了。所以在写代码上体验还是相对比较好的。

    (备注:个人觉得协程小不小对于Android开发真的没多大区别,最主要还是写代码和代码美观性)

一、添加依赖

implementation"com.squareup.retrofit2:retrofit:2.6.2"
implementation"com.squareup.retrofit2:converter-gson:2.6.2"

// Coroutines 
implementation 'org.jetbrains.kotlinx:kotlinx-coroutines-core:1.3.2'
implementation 'org.jetbrains.kotlinx:kotlinx-coroutines-android:1.3.2'

记得使用retrofit 2.6.0 或以上

二、准备网络请求接口

  • 创建接口
    interface GitApi {
        @GET("/users/{username}/{module}")
        suspend fun repos(
            @Path("username") username: String,
            @Path("module") module: String,
            @Query("page") currPage: Int
        ): List<RepoInfo>
    
        @GET("/search/repositories")
        suspend fun searchRepos(
            @Query("q") key: String,
            @Query("sort") sort: String? = "updated",
            @Query("order") order: String? = "desc",
            @Query("page") currPage: Int
        ): SearchResponse
    }
  • 创建Retrofit实例
    fun buildRetrofit(): Retrofit {
         builder.addInterceptor(HttpLoggingInterceptor(object : HttpLoggingInterceptor.Logger {
             override fun log(message: String) {
                 Timber.d(message)
             }
         }).apply {
             level = HttpLoggingInterceptor.Level.BODY
         }).addInterceptor(object : Interceptor {
             override fun intercept(chain: Interceptor.Chain): Response {
                 val userCredentials = "$username:$password"
                 val basicAuth =
                     "Basic ${String(Base64.encode(userCredentials.toByteArray(), Base64.DEFAULT))}"
                 val original = chain.request()
                 val requestBuilder = original.newBuilder()
                     .header("Authorization", basicAuth.trim { it <= ' ' })
                 val request = requestBuilder.build()
                 return chain.proceed(request)
             }
         })
         return Retrofit.Builder()
             .client(builder.build())
             .baseUrl(BASE_URL)
             .addConverterFactory(GsonConverterFactory.create())
             .build()
     }

三、 在ViewModel中使用

kotlinx.coroutines.CoroutineScope 接口中的注释有这么一句话

image.png
所以启动一个协程一定需要一个Scope,也就是说每个suspend函数都有一个CoroutineScope,如果没有一定会报错。而Android的ViewModel中有扩展了一个viewModelScope,并且跟lifecycle绑定了,所以在ViewModel的onCleared方法上会自动帮我们cancel掉这个viewModelScope的所有Jobs。
图1
为了方便使用,封装了一个BaseViewModel

open class BaseViewModel : ViewModel() {
    fun <T> request(
        onError: (error: Throwable) -> Unit = {}, // 不需要处理Error可以不传
        execute: suspend CoroutineScope.() -> T
    ) {
        viewModelScope.launch(errorHandler { onError.invoke(it) }) {
            launch(Dispatchers.IO) {
                execute()
            }
        }
    }

    private fun errorHandler(onError: (error: Throwable) -> Unit): CoroutineExceptionHandler {
        return CoroutineExceptionHandler { _, throwable ->
            Timber.d(throwable)
            onError.invoke(throwable)
        }
    }
}

使用继承 BaseViewModel 调用 request 方法即可。

class RepoViewModel(
    private val userRepo: UserDataSource,
    private val gitApi: GitApi
) : BaseViewModel() {
    private val _reposResult = BaseLiveData<List<RepoInfo>>()
    val repoResult: BaseLiveData<List<RepoInfo>>
        get() = _reposResult

    fun fetchRepos(module: String) {
        request {
            userRepo.currUser()?.let {
                val result = gitApi.repos(it.nickname, module, 1)
                _reposResult.update(result)
            }
        }
    }
}

OR

fun fetchRepos(module: String) {
    request(
        onError = {
            // handle error
        },
        execute = {
            userRepo.currUser()?.let {
                val result = gitApi.repos(it.nickname, module, 1)
                _reposResult.update(result)
            }
        }
    )
}

剩下的基本是LiveData和Fragment之间的订阅上的逻辑实现了。

结语

最近在学习,了解也不是很深,欢迎评论补充和提建议。 学习项目地址 Dithub