三方库源码笔记(8)- Retrofit 与 LiveData 的结合使用

对于 Android Developer 来说,很多开源库都是属于开发必备的知识点,从使用方式到实现原理再到源码解析,这些都需要我们有一定程度的了解和运用能力。所以我打算来写一系列关于开源库源码解析实战演练的文章,初定的目标是 EventBus、ARouter、LeakCanary、Retrofit、Glide、OkHttp、Coil 等七个知名开源库,希望对你有所帮助 🤣🤣

在上篇文章中我讲解了 Retrofit 是如何实现支持不同的 API 返回值的。例如,对于同一个 API 接口,我们既可以使用 Retrofit 原生的 Call<ResponseBody>方式来作为返回值,也可以使用 Observable<ResponseBody>这种 RxJava 的方式来发起网络请求

/**
 * @Author: leavesCZY
 * @Github:https://github.com/leavesCZY
 */
interface ApiService {

    //Retrofit 原始请求方式
    @GET("getUserData")
    fun getUserDataA(): Call<ResponseBody>

    //RxJava 的请求方式
    @GET("getUserData")
    fun getUserDataB(): Observable<ResponseBody>

}

我们在搭建项目的网络请求框架的时候,一个重要的设计环节就是要避免由于网络请求结果的异步延时回调导致内存泄漏情况的发生,所以在使用 RxJava 的时候我们往往是会搭配 RxLifecycle 来一起使用。而 Google 推出的 Jetpack 组件一个很大的亮点就是提供了生命周期安全保障的 LiveData:从源码看 Jetpack(3)-LiveData 源码解析

LiveData 是基于观察者模式来实现的,也完全符合我们在进行网络请求时的使用习惯。所以,本篇文章就来动手实现一个 LiveDataCallAdapter,即实现以下方式的网络请求回调

interface ApiService {

    @GET("getUserData")
    fun getUserData(): LiveData<HttpWrapBean<UserBean>>

}

class MainActivity : AppCompatActivity() {

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_main)
        RetrofitManager.apiService.getUserData().observe(this, Observer {
                val userBean = it.data
        })
    }

}

一、基础定义

假设我们的项目中 API 接口的返回值的数据格式都是如下所示。通过 status 来标明本次网络请求结果是否成功,在 data 里面存放具体的目标数据

{
    "status": 200,
    "msg": "success",
    "data": {
        
    }
}

对应我们项目中的实际代码就是一个泛型类

data class HttpWrapBean<T>(val status: Int, val msg: String, val data: T) {

    val isSuccess: Boolean
        get() = status == 200

}

所以,ApiService 就可以如下定义,用 LiveData 作为目标数据的包装类

data class UserBean(val userName: String, val userAge: Int)

interface ApiService {

    @GET("getUserData")
    fun getUserData(): LiveData<HttpWrapBean<UserBean>>

}

而网络请求不可避免会有异常发生,我们还需要预定义几个 Exception,对常见的异常类型:无网络 或者 status!=200 的情况进行封装

sealed class BaseHttpException(
    val errorCode: Int,
    val errorMessage: String,
    val realException: Throwable?
) : Exception(errorMessage) {

    companion object {

        const val CODE_UNKNOWN = -1024

        const val CODE_NETWORK_BAD = -1025

        fun generateException(throwable: Throwable?): BaseHttpException {
            return when (throwable) {
                is BaseHttpException -> {
                    throwable
                }
                is SocketException, is IOException -> {
                    NetworkBadException("网络请求失败", throwable)
                }
                else -> {
                    UnknownException("未知错误", throwable)
                }
            }
        }

    }

}

/**
 * 由于网络原因导致 API 请求失败
 * @param errorMessage
 * @param realException
 */
class NetworkBadException(errorMessage: String, realException: Throwable) :
    BaseHttpException(CODE_NETWORK_BAD, errorMessage, realException)

/**
 * API 请求成功了,但 code != successCode
 * @param bean
 */
class ServerCodeNoSuccessException(bean: HttpWrapBean<*>) :
    BaseHttpException(bean.status, bean.msg, null)

/**
 * 未知错误
 * @param errorMessage
 * @param realException
 */
class UnknownException(errorMessage: String, realException: Throwable?) :
    BaseHttpException(CODE_UNKNOWN, errorMessage, realException)

而在网络请求失败的时候,我们往往是需要向用户 Toast 失败原因的,所以此时一样需要向 LiveData postValue,以此将异常情况回调出去。因为还需要一个可以根据 Throwable 来生成对应的 HttpWrapBean 对象的方法

data class HttpWrapBean<T>(val status: Int, val msg: String, val data: T) {

    companion object {

        fun error(throwable: Throwable): HttpWrapBean<*> {
            val exception = BaseHttpException.generateException(throwable)
            return HttpWrapBean(exception.errorCode, exception.errorMessage, null)
        }

    }

    val isSuccess: Boolean
        get() = status == 200

}

二、LiveDataCallAdapter

首先需要继承 CallAdapter.Factory 类,在 LiveDataCallAdapterFactory 类中判断是否支持特定的 API 方法,在类型校验通过后返回 LiveDataCallAdapter

class LiveDataCallAdapterFactory private constructor() : CallAdapter.Factory() {

    companion object {

        fun create(): LiveDataCallAdapterFactory {
            return LiveDataCallAdapterFactory()
        }

    }

    override fun get(
        returnType: Type,
        annotations: Array<Annotation>,
        retrofit: Retrofit
    ): CallAdapter<*, *>? {
        if (getRawType(returnType) != LiveData::class.java) {
            //并非目标类型的话就直接返回 null
            return null
        }
        //拿到 LiveData 包含的内部泛型类型
        val responseType = getParameterUpperBound(0, returnType as ParameterizedType)
        require(getRawType(responseType) == HttpWrapBean::class.java) {
            "LiveData 包含的泛型类型必须是 HttpWrapBean"
        }
        return LiveDataCallAdapter<Any>(responseType)
    }

}

LiveDataCallAdapter 的逻辑也比较简单,如果**网络请求成功且状态码等于 200 **则直接返回接口值,否则就需要根据不同的失败原因构建出不同的 HttpWrapBean 对象

/**
 * @Author: leavesCZY
 * @Github:https://github.com/leavesCZY
 */
class LiveDataCallAdapter<R>(private val responseType: Type) : CallAdapter<R, LiveData<R>> {

    override fun responseType(): Type {
        return responseType
    }

    override fun adapt(call: Call<R>): LiveData<R> {
        return object : LiveData<R>() {

            private val started = AtomicBoolean(false)

            override fun onActive() {
                //避免重复请求
                if (started.compareAndSet(false, true)) {
                    call.enqueue(object : Callback<R> {
                        override fun onResponse(call: Call<R>, response: Response<R>) {
                            val body = response.body() as HttpWrapBean<*>
                            if (body.isSuccess) {
                                //成功状态,直接返回 body
                                postValue(response.body())
                            } else {
                                //失败状态,返回格式化好的 HttpWrapBean 对象
                                postValue(HttpWrapBean.error(ServerCodeNoSuccessException(body)) as R)
                            }
                        }

                        override fun onFailure(call: Call<R>, t: Throwable) {
                            //网络请求失败,根据 Throwable 类型来构建 HttpWrapBean
                            postValue(HttpWrapBean.error(t) as R)
                        }
                    })
                }
            }

        }
    }

}

然后在构建 Retrofit 的时候添加 LiveDataCallAdapterFactory

object RetrofitManager { 

    private val retrofit = Retrofit.Builder()
        .baseUrl("https://getman.cn/mock/")
        .addConverterFactory(GsonConverterFactory.create())
        .addCallAdapterFactory(LiveDataCallAdapterFactory.create())
        .build()

    val apiService = retrofit.create(ApiService::class.java)

}

然后就可以直接在 Activity 中发起网络请求了。当 Activity 处于后台时 LiveData 不会回调任何数据,避免了常见的内存泄漏和 NPE 问题

/**
 * @Author: leavesCZY
 * @Github:https://github.com/leavesCZY
 */
@Router(EasyRouterPath.PATH_RETROFIT)
class LiveDataCallAdapterActivity : AppCompatActivity() {

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_live_data_call_adapter)
        btn_success.setOnClickListener {
            RetrofitManager.apiService.getUserDataSuccess().observe(this, Observer {
                if (it.isSuccess) {
                    showToast(it.toString())
                } else {
                    showToast("failed: " + it.msg)
                }
            })
        }
    }

    private fun showToast(msg: String) {
        Toast.makeText(this, msg, Toast.LENGTH_SHORT).show()
    }

}

三、GitHub

LiveDataCallAdapter 的实现逻辑挺简单的,在使用上也很简单。本篇文章也算作是在了解了 Retrofit 源码后所做的一个实战 😁😁 这里也提供上述代码的 GitHub 链接:AndroidOpenSourceDemo

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