前排提醒,本文会涉及到AndroidX
中一些扩展,如果你看懵逼了,请评论区留言,我下次再写文章讲解,但应该不会影响对代码语义的理解。
本文的代码可以查看demo
这是一次由协程、Retrofit
和LiveData
,以及 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,例如:RxJava2CallAdapterFactory
、LiveDataCallAdapterFactory
;
2、在不使用 CallAdapterFactory 的情况下,也不使用Retrofit
的Call
接口回掉的原始方法;
3、代码采用顺序流程,简单易懂,只是用扩展方法就完成了封装;
4、网络请求自动关闭,没有泄露风险;
5、除了Kotlin
、AndroidX
和Retrofit
这三个官方库,不使用任何第三方库!
同时满足以上条件,你们觉得可能么?
不 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
是怎么来的?
lifecycleScope
是androidx.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 也对于协程提供了极大的便利!不论是Activity
、Fragment
、ViewModel
,都自带了生命周期感应的协程作用域,甚至还为LiveData
专门设计了一个带协程作用域的CoroutineLiveData
,可见Google
这是在暗中使劲,推动协程上位啊。
不对于我们开发者来说,写代码越来越简单,越来越傻瓜化了,让我们开发更加注重业务层。
既然如此,我们是不是该思考下,一个网络请求框架,真的需要Rx
等等其他第三方的库么?官方已经简化到如此地步了,我们何不好好利用起来,况且,官方库质量有保障啊,不会就突然停更了。
还没有上kotlin
车的同学,快上车啦,还不会协程的同学,抓紧啦,要不然都跟不上AndroidX
的节奏了。
题外话
我不太会写字,我就只会硬干代码,解释不详细的,还望见谅,内容中包含了很多AndroidX
的官方扩展方法,很多小伙伴都没见过、没用过,如果我要一一解释,那么这篇文章就写不完了,并且也不是本文的主要内容。
如果小伙伴对于新内容有很多的疑问可以提出来,我会随缘再写一下对应的文章。
再来一遍:较为完整的代码可以查看demo
本文使用 mdnice 排版