【思货】AndroidX+协程+Retrofit-我的新思考,请走开,所有的Rx请求库!

8,681 阅读12分钟

前排提醒,本文会涉及到AndroidX中一些扩展,如果你看懵逼了,请评论区留言,我下次再写文章讲解,但应该不会影响对代码语义的理解。

本文的代码可以查看demo

这是一次由协程、RetrofitLiveData,以及 Google demo 而引发的网络框架思考。

为什么又有了这次思考呢,那是因为我用过了太多的网络框架。就比如我这个菜 🐔,用过OkGo、用过 RxJava 版本的、我之前的协程版本,等等,发现都无法夺得芳心。

首先,我抛出一个本质问题,为什么我们要用第三方封装的网络开源库???

大家思考几秒钟。

从我来说就是为了:简单、方便、安全

你们想想,是不是这样?

自从用上了Kotlin大礼包后,我上次已经对协程的网络请求,进行过一次思考,参见思货-kotlin 协程优雅的与 Retrofit 缠绵-正文,这篇文章中的网络封装方式优雅么?留给你们回答。从调用的角度来说,也许还算优雅吧。

随着时间的推移,对 Google 官方的示例项目Github Browser中网络框架部分的代码进行反复阅读,在 N 个月后的某一天,我突然顿悟了精髓。

精髓是啥,就是KISS 原则

KISS 原则

KISS是英文“Keep it Simple and Stupid”的缩写,意思是“保持简单和愚蠢”,这个缩写词还有其他变体,如:“保持简短和简洁(Keep it short and simple)”、“保持愚蠢的简单(Keep it stupid simple)”和”保持简单直白(Keep it simple and straightforward)。

它传递的核心信息是一致的:尽可能的简单、从简。东西越少,越安全易维护。

这是飞机制造商洛马公司的工程师提出的(军迷肯定都熟悉,天朝还有个成洛马,[偷笑])

飞行员哥哥们,肯定最能明白 KISS 原则。

那么我再提出个问题:

对于Retrofit的使用中,我们能不能同时做到一下几点:

1、不使用任何Retrofit的 CallAdapterFactory,例如:RxJava2CallAdapterFactoryLiveDataCallAdapterFactory

2、在不使用 CallAdapterFactory 的情况下,也不使用RetrofitCall接口回掉的原始方法;

3、代码采用顺序流程,简单易懂,只是用扩展方法就完成了封装;

4、网络请求自动关闭,没有泄露风险;

5、除了KotlinAndroidXRetrofit这三个官方库,不使用任何第三方库!

同时满足以上条件,你们觉得可能么?

不 BB 了,那我们就开始吧。。。

需要导入的包

// 包含协程的 Activity lifecycle 扩展
implementation "androidx.lifecycle:lifecycle-runtime-ktx:2.2.0"

// 协程网络请求倒入的包
implementation "com.squareup.retrofit2:retrofit:2.7.1"
// json 转换
implementation "com.squareup.retrofit2:converter-moshi:2.7.1"

// kotlin协程
implementation "org.jetbrains.kotlinx:kotlinx-coroutines-android:1.3.3"
implementation group: 'org.jetbrains.kotlin', name: 'kotlin-reflect', version: "$kotlin_version"

可以看到,这里使用的包,全部来自于三个官方:

  • squareup的 retrofit 网络框架,以及moshi转化器,如果你喜欢Gson,那么换成 Gson 的转化器包(注:在kotlin中推荐使用moshi,对 Kt 非常友好。Gson不支持 Kt 中data class的默认参数!!!无解,还有其他坑,后续有空再写个文章
  • kotlin官方的协程包
  • AndroidX官方的lifecycle扩展,里面包含了 Activity、Fragment 等协程的封装

Retrofit 中的协程的使用(如果你会了,请略过)

在新版的Retrofit中,是支持直接使用的,这里我就简单介绍下,一笔带过,不做过多介绍,网上有更多详细教程。

Retrofit 的构建

fun getRetrofit(): Retrofit {
    // 正常的构建 Retrofit ,没有区别
    val builder = OkHttpClient.Builder()

    return Retrofit.Builder()
        .baseUrl("https://api.apiopen.top")
        .addConverterFactory(MoshiConverterFactory.create()) // json转换器
        .client(builder.build())
        .build()
}

Api

interface NewsApi {
    /**
     * 接口需要加上 [suspend] !
     * 返回值,直接就是你的数据类型,不需要再包装其他的东西了
     */
    @GET("/getWangYiNews")
    suspend fun getNews(): NewsBean
}

Activity 中简单的使用

class MainActivity : AppCompatActivity() {

    private lateinit var viewBinding: ActivityMainBinding

    private val mAdapter = MainRvAdapter()

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        viewBinding = ActivityMainBinding.inflate(layoutInflater)
        setContentView(viewBinding.root)

        viewBinding.recyclerView.adapter = mAdapter

        // 网络请求开始

        // 使用 activity 的 Scope 创建协程,可以感知生命周期,自动销毁
        lifecycleScope.launch {
            try {
                // 正常的创建api,没有任何区别
                val newsApi = getRetrofit().create(NewsApi::class.java)

                // 这里直接调用api里的方法就好了,不需要自己进行任何异步操作
                val newsBean: NewsBean = newsApi.getNews()

                // 直接刷新界面,因为这里是UI线程的协程
                mAdapter.setList(newsBean.list)

                // 以上就是获取数据的方法,三行关键代码。Retrofit 也不再需要设置 Rx 等等那些转化器了。
            } catch (e: Throwable) {
                // 这里处理网络错误
                if (e is HttpException) {
                    // http 状态码
                    when (e.code()) {
                        400 -> {
                        }
                        500 -> {
                        }
                    }
                } else if (e is SocketTimeoutException) {
                    // 连接超时
                } else if (e is SocketException ||
                    this is UnknownHostException ||
                    this is SSLException
                ) {
                    // 各种其他网络错误...
                }
            }
        }

    }
}

好了,基础使用就是如此,代码注释中也写了基本意思了。看完以后是不是头皮发麻,需要缓缓?

有小伙伴要问了,代码中的lifecycleScope是怎么来的?

lifecycleScopeandroidx.lifecycle:lifecycle-runtime-ktx:2.2.0扩展包里面的,官方已经实现了一个生命周期感知的协程作用域,可以直接开启协程,并在页面关闭的时候自动销毁。

lifecycleScope扩展

lifecycleScope,你还可以这么用:

// 直接开启一个协程,最常见的
lifecycleScope.launch {
}

// 在生命周期走到 Create 以后,开启此协程内容
lifecycleScope.launchWhenCreated {
}

// 在生命周期走到 Start 以后,开启此协程内容
lifecycleScope.launchWhenStarted {
}

// 在生命周期走到 Resume 以后,开启此协程内容
lifecycleScope.launchWhenResumed {
}

有同学肯定要 What f**k?这都什么玩意,没见过啊。没想到吧,嘿嘿,Google 如此重视协程。(有兴趣的去看看源码,此处不展开了)

好了不扯远了,继续我们的网络请求。

上面那种基本用法,虽然非常简单、清晰、易读,但在项目里肯定不优雅,直接用是不行的。

利用 DSL 封装

大致内容与我之前写的思货-kotlin 协程优雅的与 Retrofit 缠绵-正文类似,不在嚼舌根子了。(代码写在 demo 中,可以自行查看)

直接使用方式如下:

lifecycleScope.retrofit<NewsBean> {
    api = api.getNews()

    onComplete {
    }

    onSuccess { bean ->
    }

    onFailed { error, code ->
    }
}

如果你不需要什么设计模式,什么 MVVM,就想在 Activity 直接请求网络,那么使用 DSL 的封装可以解决大部分的网络请求方式了

本文重点:配合 LiveData 封装

需要增加导入的包

// LiveData的包
implementation "androidx.lifecycle:lifecycle-livedata-ktx:2.2.0"

// activity的扩展
implementation "androidx.activity:activity-ktx:1.1.0"

MVVM的设计模式中(我们这里只讨论ViewModel这一层),如果在ViewModel中直接使用 DSL 封装的方式,然后再转换为 LiveData 的值可以么,当然可以。不过我觉得这很多此一举。

在 Google 官方的示例项目Github Browser中,ViewModel拿到的只是网络结果的LiveData对象,并且官方使用的协程是与LiveDataCallAdapterFactory转换器配合的,在Retrofit未支持协程的情况下,这是比较好的一个解决方案。但是到了现在,恕我直言,这是一个过时的写法,极其不优雅,陈平大佬敢于质疑古典经济学理论,我们为啥不可以质疑 Google 的 demo,就因为他是外国人写的,就因为他是 Google 写的?

ViewModel 到底需要什么?

抛开 Google 的写法,看本质,看本质,看本质,Google 的 demo 中,就是告诉了我们一个思想,ViewModel需要的是一个LiveData封装的网络数据,就这么简单,

那么在没有LiveDataCallAdapterFactory的情况下,我们怎么封装

- 最容易想到的方式

初期,我有仿写过 Google 的 demo 不下 4 次,每一次仿写都有新的进步,最后发现其实都是一种写法:把网络请求的结果,放到 LiveData 中。

简化的代码模型如下:


fun CoroutineScope.getHttpLiveData(): LiveData<NewsBean> {
  // 先定义LiveData
  val requestLive = MutableLiveData<NewsBean>()

  this.launch(Dispatchers.Main) {
     // 创建api
     val newsApi = getRetrofit().create(NewsApi::class.java)

     // 调用api的方法
     val newsBean: NewsBean = newsApi.getNews()

     // 把值给LiveData
     requestLive.value = newsBean
  }

  return requestLive
}

好了,这种方式的写法,看着好像没什么问题啊。确实,代码逻辑上来说确实可行没毛病,但是这,这,这就是差点内味,感觉这个 LiveData 是拼凑出来的,我想要是更存粹,更原汁原味的方式。

- liveData 方式(终于到主题了)

注意,我说的是liveData,不是LiveData,开头是小写,不是大写。

完了,有的同学又头晕了,“这 TM 什么鬼啊???你 TM 能说人话么?”

且听我慢慢到来。

大写开头的LiveData,是一个抽象类,是所有 LiveData 的基本类,是一个实实在在的类。

而小写的liveData,是AndroidX包中提供一个方法,注意,这是一个方法,会生成一个CoroutineLiveData,这是自带协程的 LiveData,我当时看到这个都震惊了。

本菜鸡也是在不经意间,发现的系统提供的方法,看了相关源码以后,发现,这简直就是解决问题的完美模式。

下面开始起飞,简化的模型如下:

fun CoroutineScope.getHttpLiveData(): LiveData<NewsBean> {
    // 使用协程的 coroutineContext 去生成一个LiveData
    return liveData(this.coroutineContext) {
        // 创建api
        val newsApi = getRetrofit().create(NewsApi::class.java)
        // 调用api的方法
        val newsBean: NewsBean = newsApi.getNews()
        // 提交数据
        emit(newsBean)
    }
}

核心思想就上面几行代码,好了,完事了,我们接下来可以继续抽象 Api,封装网络请求了。

“停车停车!等等,我 TM 好懵,这是什么操作,方向盘我焊死了,你不说清楚一个都别想走”。

别急别急,为了不打断思绪,liveData方法的讲解我放到后面再说,我们先继续封装。

初步封装成型

开始之前,我们要先准备好东西

定义一个 RequestStatus,用于区分网络请求状态
enum class RequestStatus {
    START,
    SUCCESS,
    COMPLETE,
    ERROR
}

四个状态,对应着网络请求的开始、成功、完成、失败

定义 ResultData,用于封装网络数据
data class ResultData<T>(val requestStatus: RequestStatus,
                         val data: T?
                         val error: Throwable? = null) {
    companion object {

        fun <T> start(): ResultData<T> {
            return ResultData(RequestStatus.START, null, null)
        }

        fun <T> success(data: T?, isCache: Boolean = false): ResultData<T> {
            return ResultData(RequestStatus.SUCCESS, data, null)
        }

        fun <T> complete(data: T?): ResultData<T> {
            return ResultData(RequestStatus.COMPLETE, data, null)
        }

        fun <T> error(error: Throwable?): ResultData<T> {
            return ResultData(RequestStatus.ERROR, null, error)
        }
    }
定义一个 ApiResponse,用于区分哪种网络状态返回的数据
internal sealed class ApiResponse<T> {
    companion object {
        fun <T> create(error: Throwable): ApiErrorResponse<T> {
            return ApiErrorResponse(error)
        }

        fun <T> create(body: T?): ApiResponse<T> {
            return if (body == null) {
                ApiEmptyResponse()
            } else {
                ApiSuccessResponse(body)
            }
        }
    }
}

internal class ApiEmptyResponse<T> : ApiResponse<T>()

internal data class ApiSuccessResponse<T>(val body: T) : ApiResponse<T>()

internal data class ApiErrorResponse<T>(val throwable: Throwable) : ApiResponse<T>()
接着定义一个RequestAction类,用于 DSL,包装需要操作的方法。
open class RequestAction<ResponseType> {
    var api: (suspend () -> ResponseType)? = null

    fun api(block: suspend () -> ResponseType) {
        this.api = block
    }
}

api 我们肯定是要动态传递进去的,不能直接写死对吧。那么我们祭出 DSL 大法。

为了讲解简便,暂且我们先只定义一个 api 相关的方法,其他操作后续在加上。

万事俱备,开始整!
/**
 * DSL网络请求
 */
inline fun <ResultType> CoroutineScope.requestLiveData(
    dsl: RequestAction<ResultType>.() -> Unit
): LiveData<ResultData<ResultType>> {
    val action = RequestAction2<ResultType>().apply(dsl)
    return liveData(this.coroutineContext) {
        // 通知网络请求开始
        emit(ResultData.start<ResultType>())

        val apiResponse = try {
            // 获取网络请求数据
            val resultBean = action.api?.invoke()
            ApiResponse.create<ResultType>(resultBean)
        } catch (e: Throwable) {
            ApiResponse.create<ResultType>(e)
        }

        // 根据 ApiResponse 类型,处理对于事物
        val result = when (apiResponse) {
            is ApiEmptyResponse -> {
                null
            }
            is ApiSuccessResponse -> {
                apiResponse.body.apply {
                    // 提交成功的数据给LiveData
                    emit(ResultData.success<ResultType>(this))
                }
            }
            is ApiErrorResponse -> {
                // 提交错误的数据给LiveData
                emit(ResultData.error<ResultType>(apiResponse.throwable))
                null
            }
        }

        // 提交成功的信息
        emit(ResultData.complete<ResultType>(result))
    }
}

可以发现,所有的逻辑,都是顺序执行!简单清晰易懂,不需要处理线程转换。通过emit就可以把数据提交给 LiveData

ViewModel 里的使用

其实和 Google 的 demo 里差不多,没太大本质变化

class MainViewModel : ViewModel() {

    private val newsApi = getRetrofit().create(NewsApi::class.java)

    private val _newsLiveData = MediatorLiveData<ResultData<NewsBean>>()

    // 对外暴露的只是抽象的LiveData,防止外部随意更改数据
    val newsLiveData: LiveData<ResultData<NewsBean>>
        get() = _newsLiveData

    fun getNews() {
        // viewModelScope 是系统扩展提供的ViewModel的协程作用域
        val newsLiveData = viewModelScope.requestLiveData<NewsBean> {
            api { newsApi.getNews() }
        }

        // 监听数据变化
        _newsLiveData.addSource(newsLiveData) {
            if (it.requestStatus == RequestStatus.COMPLETE) {
                _newsLiveData.removeSource(newsLiveData)
            }
            _newsLiveData.value = it
        }
    }
}

viewModelScope 是系统扩展提供的 ViewModel 的协程作用域,会自动管理协程生命周期。

Activity 里的调用
class MainActivity : AppCompatActivity() {
    // 利用系统扩展的代理,快速生成viewModel
    private val viewModel by viewModels<MainViewModel>()

    private lateinit var viewBinding: ActivityMainBinding

    private val mAdapter = MainRvAdapter()

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        viewBinding = ActivityMainBinding.inflate(layoutInflater)
        setContentView(viewBinding.root)

        viewBinding.recyclerView.adapter = mAdapter

        // 订阅LiveData
        viewModel.newsLiveData.observe(this, Observer {
            // 根据状态,处理数据
            when (it.requestStatus) {
                RequestStatus.START -> {
                }
                RequestStatus.SUCCESS -> {
                    it.data?.let { newsBean ->
                        // 直接刷新界面
                        mAdapter.setList(newsBean.result)
                    }
                }
                RequestStatus.COMPLETE -> {
                }
                RequestStatus.ERROR -> {
                    Toast.makeText(this, "网络出错了", Toast.LENGTH_SHORT).show()
                }
            }
        })

    }

    override fun onStart() {
        super.onStart()
        // 在你需要数据的地方,调用方法获取数据
        viewModel.getNews()
    }
}

以上就是简单的封装,其中已经包括了核心的思想。小伙伴们还可以深入下去,继续进行封装(例如订阅 Livedata 的时候,状态处理可以封装下),这里我就只是作为抛砖引玉啦。 较为完整的代码可以查看demo,稍加改动即可在项目中使用了。

总结

retrofit支持了协程以后,我们完全可以抛弃掉CallAdapterFactory转换器。

总体上,我们可以看出,Google 也对于协程提供了极大的便利!不论是ActivityFragmentViewModel,都自带了生命周期感应的协程作用域,甚至还为LiveData专门设计了一个带协程作用域的CoroutineLiveData,可见Google这是在暗中使劲,推动协程上位啊。

不对于我们开发者来说,写代码越来越简单,越来越傻瓜化了,让我们开发更加注重业务层。

既然如此,我们是不是该思考下,一个网络请求框架,真的需要Rx等等其他第三方的库么?官方已经简化到如此地步了,我们何不好好利用起来,况且,官方库质量有保障啊,不会就突然停更了。

还没有上kotlin车的同学,快上车啦,还不会协程的同学,抓紧啦,要不然都跟不上AndroidX的节奏了。

题外话

我不太会写字,我就只会硬干代码,解释不详细的,还望见谅,内容中包含了很多AndroidX的官方扩展方法,很多小伙伴都没见过、没用过,如果我要一一解释,那么这篇文章就写不完了,并且也不是本文的主要内容。

如果小伙伴对于新内容有很多的疑问可以提出来,我会随缘再写一下对应的文章。

再来一遍:较为完整的代码可以查看demo

本文使用 mdnice 排版