热门标签 | HotTags
当前位置:  开发笔记 > 编程语言 > 正文

Retrofit+Kotlin+MVVM的网络请求框架的封装尝试

篇首语:本文由编程笔记#小编为大家整理,主要介绍了Retrofit+Kotlin+MVVM的网络请求框架的封装尝试相关的知识,希望对你有一定的参考价值。

篇首语:本文由编程笔记#小编为大家整理,主要介绍了Retrofit + Kotlin + MVVM 的网络请求框架的封装尝试相关的知识,希望对你有一定的参考价值。






1、前言

之前在学习郭霖《第一行代码》时按部就班地写过一个彩云天气 App,对里面的网络请求框架的封装印象非常深刻,很喜欢这种 Retrofit + Kotlin + 协程的搭配使用。随后也在自己的项目里参考了这部分的代码。但随着代码的深入编写和功能的复杂,原来的框架已经无法满足我的使用了。原主要有如下的痛点:


  • 缺少失败的回调
  • 显示加载中动画比较麻烦

后面我自己试着努力去封装一个简单易用的框架,可惜个人能力有限,自己封装的框架总是不如人意。好在还有很多优秀的博客和代码可供参考。在此基础上,对彩云天气 App中的网络请求框架做了一些修改,尽可能地做到简单易用。以请求玩安卓的登录接口为例(用户名和密码是我自己申请的,见代码),页面上有一个按钮,点击按钮后就发起登录请求。

先来看看发起请求后的回调怎么写:

viewModel.loginLiveData.observeState(this)
onStart
LoadingDialog.show(activity)
Log.d(TAG, "请求开始")

onSuccess
Log.d(TAG, "请求成功")
showToast("登录成功")
binding.tvResult.text = it.toString()

onEmpty
showToast("数据为空")

onFailure
Log.d(TAG, "请求失败")
showToast(it.errorMsg.orEmpty())
binding.tvResult.text = it.toString()

onFinish
LoadingDialog.dismiss(activity)
Log.d(TAG, "请求结束")


回调一共有五种,会在下文详细介绍。这里采用了DSL的写法,如果你喜欢传统的写法,可以调用另外一个扩展方法observeResponse(),由于它最后一个参数就是请求成功的回调,所以借助 Lambda 表达式的特性,可以简洁地写成如下的形式:

viewModel.loginLiveData.observeResponse(this)
binding.tvResult.text = it.toString()

如果还需要其他回调,可以使用具名参数加上,如下所示:

viewModel.loginLiveData.observeResponse(this, onStart =
LoadingDialog.show(this)
, onFinish =
LoadingDialog.dismiss(activity)
)
binding.tvResult.text = it.toString()


2、框架搭建

开始之前必须说明,这个框架是基于《第一行代码》(第三版)中的彩云天气 App的,它的架构图如下所示,如果你阅读过《第一行代码》或者谷歌的相关文档,那么想必对此不会陌生。


2.1 添加依赖库

//简化在 Activity 中声明 ViewModel 的代码
implementation "androidx.activity:activity-ktx:1.3.1"
// lifecycle
def lifecycle_version = "2.3.1"
implementation "androidx.lifecycle:lifecycle-viewmodel-ktx:$lifecycle_version"
implementation "androidx.lifecycle:lifecycle-livedata-ktx:$lifecycle_version"
// retrofit2
def retrofit_version = "2.9.0"
implementation "com.squareup.retrofit2:retrofit:$retrofit_version"
implementation "com.squareup.retrofit2:adapter-rxjava2:$retrofit_version"
implementation "com.squareup.retrofit2:converter-gson:$retrofit_version"
implementation 'io.reactivex.rxjava2:rxandroid:2.1.1'
// okhttp
def okhttp_version = "4.8.1"
implementation "com.squareup.okhttp3:okhttp:$okhttp_version"
//日志拦截器
implementation('com.github.ihsanbal:LoggingInterceptor:3.1.0')
exclude group: 'org.json', module: 'json'


2.2 Retrofit构建器

Retrofit构建器这里做了分层,基类做了一些基本的配置,子类继承后可以添加新的配置,并配置自己喜欢的日志拦截器。

private const val TIME_OUT_LENGTH = 8L
private const val BASE_URL = "https://www.wanandroid.com/"
abstract class BaseRetrofitBuilder
private val okHttpClient: OkHttpClient by lazy
val builder = OkHttpClient.Builder()
.callTimeout(TIME_OUT_LENGTH, TimeUnit.SECONDS)
.connectTimeout(TIME_OUT_LENGTH, TimeUnit.SECONDS)
.readTimeout(TIME_OUT_LENGTH, TimeUnit.SECONDS)
.writeTimeout(TIME_OUT_LENGTH, TimeUnit.SECONDS)
.retryOnConnectionFailure(true)
initLoggingInterceptor()?.also
builder.addInterceptor(it)

handleOkHttpClientBuilder(builder)
builder.build()

private val retrofit = Retrofit.Builder()
.baseUrl(BASE_URL)
.addConverterFactory(GsonConverterFactory.create())
.client(okHttpClient)
.build()
fun <T> create(serviceClass: Class<T>): T &#61; retrofit.create(serviceClass)
inline fun <reified T> create(): T &#61; create(T::class.java)
/**
* 子类自定义 OKHttpClient 的配置
*/

abstract fun handleOkHttpClientBuilder(builder: OkHttpClient.Builder)
/**
* 配置日志拦截器
*/

abstract fun initLoggingInterceptor(): Interceptor?

RetrofitBuilder&#xff1a;

private const val LOG_TAG_HTTP_REQUEST &#61; "okhttp_request"
private const val LOG_TAG_HTTP_RESULT &#61; "okhttp_result"
object RetrofitBuilder : BaseRetrofitBuilder()
override fun handleOkHttpClientBuilder(builder: OkHttpClient.Builder)
override fun initLoggingInterceptor()&#61; LoggingInterceptor
.Builder()
.setLevel(Level.BASIC)
.log(Platform.INFO)
.request(LOG_TAG_HTTP_REQUEST)
.response(LOG_TAG_HTTP_RESULT)
.build()


2.3 全局异常处理

请求时可能会遇到诸如网络断开、Json 解析失败等意外情况&#xff0c;如果我们每次请求都要处理一遍这些异常&#xff0c;那也未免太麻烦了。正确的做法是把异常集中到一起处理。

创建一个定义各种异常的枚举类&#xff1a;

enum class HttpError(val code: Int, val message: String)
UNKNOWN(-100,"未知错误"),
NETWORK_ERROR(1000, "网络连接超时&#xff0c;请检查网络"),
JSON_PARSE_ERROR(1001, "Json 解析失败")
//······

创建一个文件&#xff0c;在里面定义一个全局方法&#xff0c;用于处理各种异常&#xff1a;

fun handleException(throwable: Throwable) &#61; when (throwable)
is UnknownHostException -> RequestException(HttpError.NETWORK_ERROR, throwable.message)
is HttpException ->
val errorModel &#61; throwable.response()?.errorBody()?.string()?.run
Gson().fromJson(this, ErrorBodyModel::class.java)
?: ErrorBodyModel()
RequestException(errorMsg &#61; errorModel.message, error &#61; errorModel.error)

is JsonParseException -> RequestException(HttpError.JSON_PARSE_ERROR, throwable.message)
is RequestException -> throwable
else -> RequestException(HttpError.UNKNOWN, throwable.message)

实际项目中遇到的异常当然不止这几个&#xff0c;这里只是作为举例写了少部分&#xff0c;实际开放中把它丰富完善即可。


2.4 回调状态监听

回调状态一共有四种&#xff1a;


  • onStart()&#xff1a;请求开始&#xff08;可在此展示加载动画&#xff09;
  • onSuccess()&#xff1a;请求成功
  • onEmpty()&#xff1a;请求成功&#xff0c;但datanull或者data是集合类型但为空
  • onFailure()&#xff1a;请求失败
  • onFinish()&#xff1a;请求结束&#xff08;可在此关闭加载动画&#xff09;

这里要注意onSuccess的标准&#xff1a;并不仅仅是 Http 请求的结果码&#xff08;status code&#xff09;等于 200&#xff0c;而且要达到Api请求成功的标准&#xff0c;以玩安卓的Api 为例&#xff0c;errorCode 为 0时&#xff0c;发起的请求才是执行成功&#xff1b;否则&#xff0c;都应该归为onFailure()的情况&#xff08;可以参考文章附带的思维导图&#xff09;。

理清楚有几种回调状态后&#xff0c;就可以实施监听了。那么在哪里监听呢&#xff1f;LiveDataobserve()方法的第二个函数可以传入Observer参数。Observer是一个接口&#xff0c;我们继承它自定义一个Oberver&#xff0c;借此我们就可以监听LiveData的值的变化。

interface IStateObserver<T> : Observer<BaseResponse<T>>
override fun onChanged(response: BaseResponse<T>?)
when (response)
is StartResponse ->
//onStart()回调后不能直接就调用onFinish()&#xff0c;必须等待请求结束
onStart()
return

is SuccessResponse -> onSuccess(response.data)
is EmptyResponse -> onEmpty()
is FailureResponse -> onFailure(response.exception)

onFinish()

/**
* 请求开始
*/

fun onStart()
/**
* 请求成功&#xff0c;且 data 不为 null
*/

fun onSuccess(data: T)
/**
* 请求成功&#xff0c;但 data 为 null 或者 data 是集合类型但为空
*/

fun onEmpty()
/**
* 请求失败
*/

fun onFailure(e: RequestException)
/**
* 请求结束
*/

fun onFinish()

接下来我们准备一个HttpRequestCallback类&#xff0c;用于实现DSL的回调形式&#xff1a;

typealias OnSuccessCallback<T> &#61; (data: T) -> Unit
typealias OnFailureCallback &#61; (e: RequestException) -> Unit
typealias OnUnitCallback &#61; () -> Unit
class HttpRequestCallback<T>
var startCallback: OnUnitCallback? &#61; null
var successCallback: OnSuccessCallback<T>? &#61; null
var emptyCallback: OnUnitCallback? &#61; null
var failureCallback: OnFailureCallback? &#61; null
var finishCallback: OnUnitCallback? &#61; null
fun onStart(block: OnUnitCallback)
startCallback &#61; block

fun onSuccess(block: OnSuccessCallback<T>)
successCallback &#61; block

fun onEmpty(block: OnUnitCallback)
emptyCallback &#61; block

fun onFailure(block: OnFailureCallback)
failureCallback &#61; block

fun onFinish(block: OnUnitCallback)
finishCallback &#61; block


然后声明新的监听方法&#xff0c;考虑到某些时候需要自定义的LiveData(比如为了解决数据倒灌的问题)&#xff0c;这里采用扩展函数的写法&#xff0c;便于扩展。

/**
* 监听 LiveData 的值的变化&#xff0c;回调为 DSL 的形式
*/

inline fun <T> LiveData<BaseResponse<T>>.observeState(
owner: LifecycleOwner,
crossinline callback: HttpRequestCallback<T>.() -> Unit
)
val requestCallback &#61; HttpRequestCallback<T>().apply(callback)
observe(owner, object : IStateObserver<T>
override fun onStart()
requestCallback.startCallback?.invoke()

override fun onSuccess(data: T)
requestCallback.successCallback?.invoke(data)

override fun onEmpty()
requestCallback.emptyCallback?.invoke()

override fun onFailure(e: RequestException)
requestCallback.failureCallback?.invoke(e)

override fun onFinish()
requestCallback.finishCallback?.invoke()

)

/**
* 监听 LiveData 的值的变化
*/

inline fun <T> LiveData<BaseResponse<T>>.observeResponse(
owner: LifecycleOwner,
crossinline onStart: OnUnitCallback &#61; ,
crossinline onEmpty: OnUnitCallback &#61; ,
crossinline onFailure: OnFailureCallback &#61; e: RequestException -> ,
crossinline onFinish: OnUnitCallback &#61; ,
crossinline onSuccess: OnSuccessCallback<T>
)
observe(owner, object : IStateObserver<T>
override fun onStart()
onStart()

override fun onSuccess(data: T)
onSuccess(data)

override fun onEmpty()
onEmpty()

override fun onFailure(e: RequestException)
onFailure(e)

override fun onFinish()
onFinish()

)


2.5 Repository 层的封装

Repository层作为数据的来源&#xff0c;有个两个渠道&#xff1a;网络请求和数据库。这里暂时只处理了网络请求。

基类Repository&#xff1a;

abstract class BaseRepository
protected fun <T> fire(
context: CoroutineContext &#61; Dispatchers.IO,
block: suspend () -> BaseResponse<T>
): LiveData<BaseResponse<T>> &#61; liveData(context)
this.runCatching
emit(StartResponse())
block()
.onSuccess
//status code 为200&#xff0c;继续判断 errorCode 是否为 0
emit(
when (it.success)
true -> checkEmptyResponse(it.data)
false -> FailureResponse(handleException(RequestException(it)))

)
.onFailure throwable ->
emit(FailureResponse(handleException(throwable)))


/**
* data 为 null&#xff0c;或者 data 是集合类型&#xff0c;但是集合为空都会进入 onEmpty 回调
*/

private fun <T> checkEmptyResponse(data: T?): ApiResponse<T> &#61;
if (data &#61;&#61; null || (data is List<*> && (data as List<*>).isEmpty()))
EmptyResponse()
else
SuccessResponse(data)


子类Repository:

object Repository : BaseRepository()
fun login(pwd: String) &#61; fire
NetworkDataSource.login(pwd)


网络请求数据源&#xff0c;在这里调用网络接口&#xff1a;

object NetworkDataSource
private val apiService &#61; RetrofitBuilder.create<ApiService>()
suspend fun login(pwd: String) &#61; apiService.login(password &#61; pwd)


2.6 ViewModel层的封装

ViewModel基本遵循了《第一行代码》中的写法&#xff0c;创建了两个LiveData。用户点击按钮时&#xff0c;loginAction的值就会发生改变&#xff0c;触发switchMap中的代码&#xff0c;从而达到请求数据的目的。

class MainViewModel : ViewModel()
private val loginAction &#61; MutableLiveData<Boolean>()
/**
* loginAction 在这里只传递布尔值&#xff0c;不传递密码&#xff0c;在实际项目中&#xff0c;会使用 DataBinding 绑定 xml 布局和 ViewModel&#xff0c;
* 不需要从 Activity 或者 Fragment 中把密码传入 ViewModel
*/

val loginLiveData &#61; loginAction.switchMap
if (it)
Repository.login("PuKxVxvMzBp2EJM")
else
Repository.login("123456")


/**
* 点击登录
*/

fun login()
loginAction.value &#61; true

fun loginWithWrongPwd()
loginAction.value &#61; false




注意&#xff1a;这种写法通常不从View向ViewModel层传递数据&#xff0c;是需要搭配DataBinding 的。如果你不想这样写&#xff0c;可以修改BaseRepository中的返回值&#xff0c;直接返回BaseResponse



3、思维导图及源码

最后&#xff0c;用一张思维导图总结本文&#xff1a;

源码地址&#xff1a;GitHub &#xff08;注意分支要选择 dev1.0&#xff09;


4、参考
  • JetpackMvvm
  • FastJetpack






推荐阅读
  • 本篇文章笔者在上海吃饭的时候突然想到的这段时间就有想写几篇关于返回系统的笔记,所以回家到之后就奋笔疾书的写出来发布了事先在网上找了很多方法,发现有 ... [详细]
  • 如何自行分析定位SAP BSP错误
    The“BSPtag”Imentionedintheblogtitlemeansforexamplethetagchtmlb:configCelleratorbelowwhichi ... [详细]
  • GetWindowLong函数
    今天在看一个代码里头写了GetWindowLong(hwnd,0),我当时就有点费解,靠,上网搜索函数原型说明,死活找不到第 ... [详细]
  • 在Android开发中,使用Picasso库可以实现对网络图片的等比例缩放。本文介绍了使用Picasso库进行图片缩放的方法,并提供了具体的代码实现。通过获取图片的宽高,计算目标宽度和高度,并创建新图实现等比例缩放。 ... [详细]
  • HDU 2372 El Dorado(DP)的最长上升子序列长度求解方法
    本文介绍了解决HDU 2372 El Dorado问题的一种动态规划方法,通过循环k的方式求解最长上升子序列的长度。具体实现过程包括初始化dp数组、读取数列、计算最长上升子序列长度等步骤。 ... [详细]
  • XML介绍与使用的概述及标签规则
    本文介绍了XML的基本概念和用途,包括XML的可扩展性和标签的自定义特性。同时还详细解释了XML标签的规则,包括标签的尖括号和合法标识符的组成,标签必须成对出现的原则以及特殊标签的使用方法。通过本文的阅读,读者可以对XML的基本知识有一个全面的了解。 ... [详细]
  • Android JSON基础,音视频开发进阶指南目录
    Array里面的对象数据是有序的,json字符串最外层是方括号的,方括号:[]解析jsonArray代码try{json字符串最外层是 ... [详细]
  • 本文详细介绍了Java中vector的使用方法和相关知识,包括vector类的功能、构造方法和使用注意事项。通过使用vector类,可以方便地实现动态数组的功能,并且可以随意插入不同类型的对象,进行查找、插入和删除操作。这篇文章对于需要频繁进行查找、插入和删除操作的情况下,使用vector类是一个很好的选择。 ... [详细]
  • Java学习笔记之面向对象编程(OOP)
    本文介绍了Java学习笔记中的面向对象编程(OOP)内容,包括OOP的三大特性(封装、继承、多态)和五大原则(单一职责原则、开放封闭原则、里式替换原则、依赖倒置原则)。通过学习OOP,可以提高代码复用性、拓展性和安全性。 ... [详细]
  • 本文介绍了一个React Native新手在尝试将数据发布到服务器时遇到的问题,以及他的React Native代码和服务器端代码。他使用fetch方法将数据发送到服务器,但无法在服务器端读取/获取发布的数据。 ... [详细]
  • 使用eclipse创建一个Java项目的步骤
    本文介绍了使用eclipse创建一个Java项目的步骤,包括启动eclipse、选择New Project命令、在对话框中输入项目名称等。同时还介绍了Java Settings对话框中的一些选项,以及如何修改Java程序的输出目录。 ... [详细]
  • 电话号码的字母组合解题思路和代码示例
    本文介绍了力扣题目《电话号码的字母组合》的解题思路和代码示例。通过使用哈希表和递归求解的方法,可以将给定的电话号码转换为对应的字母组合。详细的解题思路和代码示例可以帮助读者更好地理解和实现该题目。 ... [详细]
  • 本文介绍了lua语言中闭包的特性及其在模式匹配、日期处理、编译和模块化等方面的应用。lua中的闭包是严格遵循词法定界的第一类值,函数可以作为变量自由传递,也可以作为参数传递给其他函数。这些特性使得lua语言具有极大的灵活性,为程序开发带来了便利。 ... [详细]
  • 在重复造轮子的情况下用ProxyServlet反向代理来减少工作量
    像不少公司内部不同团队都会自己研发自己工具产品,当各个产品逐渐成熟,到达了一定的发展瓶颈,同时每个产品都有着自己的入口,用户 ... [详细]
  • Java在运行已编译完成的类时,是通过java虚拟机来装载和执行的,java虚拟机通过操作系统命令JAVA_HOMEbinjava–option来启 ... [详细]
author-avatar
手机用户2502856555
这个家伙很懒,什么也没留下!
PHP1.CN | 中国最专业的PHP中文社区 | DevBox开发工具箱 | json解析格式化 |PHP资讯 | PHP教程 | 数据库技术 | 服务器技术 | 前端开发技术 | PHP框架 | 开发工具 | 在线工具
Copyright © 1998 - 2020 PHP1.CN. All Rights Reserved | 京公网安备 11010802041100号 | 京ICP备19059560号-4 | PHP1.CN 第一PHP社区 版权所有