注册

一步步封装实现自己的网络请求框架 3.0

一、ReactiveHtt

协程这个概念已经出现很多年了,但 Kotlin 协程是在 2018 年才发布了 1.0 版本,被 Android 开发者所熟知还要再往后一段时间,协程的意义不是本篇文章所应该探讨的,但如果你去了解下协程能给我们带来的开发效益,我相信你是会喜欢上它的

闲话说完,这里先贴上 3.0 版本的 GitHub 链接:ReactiveHttp

3.0 版本的技术栈已更新为了 Kotlin + Jetpack + Coroutines + Retrofit,已托管到 jitpack.io,感兴趣的读者可以直接远程导入依赖

allprojects {
repositories {
maven { url 'https://jitpack.io' }
}
}

dependencies {
implementation 'com.github.leavesC:ReactiveHttp:latest_version'
}


二、能给你带来什么

ReactiveHttp 能够给你带来什么?

  • 更现代化的技术栈。Kotlin + Jetpack + Retrofit 现在应该是大多数 Android 开发者选用的最基础组件了,Kotlin 协程会相对比较少人接触,但我觉得协程也是未来的主流方向之一,毕竟连 Retrofit 也原生支持 Kotlin 协程了,本库会更加符合大多数开发者的现实需求
  • 极简的设计理念。ReactiveHttp 目前仅包含十二个 Kotlin 文件,设计上遵循约定大于配置的理念,大多数配置项都可以通过方法复写的形式来实现自定义
  • 极简的使用方式。只需要持有一个 RemoteDataSource 对象,开发者就可以在任意地方发起异步请求和同步请求了。此外,进行网络请求 Callback 自然是必不可少的,ReactiveHttp 提供了多个事件回调:onStart、onSuccess、onSuccessIO、onFailed、onFailToast、onCancelled、onFinally 等,按需声明即可,甚至可以完全不用实现
  • 支持通用性的自动化行为。对于网络请求来说,像 showLoading、dismissLoading、showToast 等行为是具有通用性的,我们肯定不希望每个网络请求都要来手动调用方法触发以上操作,而是希望能够在发起网络请求的过程中自动完成。ReactiveHttp 就提供了自动完成以上通用性 UI 行为的功能 ,且每个操作均和生命周期相绑定,避免了常见的内存泄漏和 NPE 问题,并提供了交由外部使用者来自定义各种其它行为的入口
  • 极低的接入成本。ReactiveHttp 并不强制要求外部必须继承于任何 BaseViewModel 或者是 BaseActivity/BaseFragment,外部只要通过实现 IUIActionEventObserverIViewModelActionEvent 两个接口即可享受 ReactiveHttp 带来的各个益处。当然,如果你不需要 ReactiveHttp 的自动化行为的话,也可以不实现任何接口
  • 支持多个(两个/三个)接口同时并发请求,在网络请求成功时同步回调,所以理论上多个接口的总耗时就取决于耗时最长的那个接口,从而缩短用户的等待时间,提升用户体验

ReactiveHttp 不能带给你什么?

  • ReactiveHttp 本身的应用领域是专注于接口请求的,所以对于接口的返回值形式带有强制约束,且没有封装文件下载、文件上传等功能
  • 肯定有,但还没想到

ReactiveHttp 目前已经在我司项目上稳定运行一年多了,在这过程中我也是在逐步优化,使其能够更加适用于外部不同环境的需求,到目前为止我觉得也是已经足够稳定了,希望对你有所帮助 😇😇

三、架构说明

现在应该有百分之九十以上的 Android 客户端的网络请求是直接或间接依靠 OkHttp 来完成的吧?本文所说的网络请求框架就是指在 OkHttp 之上所做的一层封装。原生的 OkHttp 在使用上并不方便,甚至可以说是有点繁琐。Retrofit 在易用性上有所提升,但是如果直接使用的话也并不算多简洁。所以我们往往都是会根据项目架构自己来对 OkHttp 或者 Retrofit 进行多一层封装,ReactiveHttp 的实现出发点即是如此

此外,现在大多数项目都引用到了 Jetpack 这一套组件来实现 MVVM 架构的吧?ReactiveHttp 将 Jetpack 和 Retrofit 关联了起来,使得网络请求过程更加符合“响应式”概念,并提供了更加可靠的生命周期安全性和自动化行为

一般的网络请求框架是只专注于完成网络请求并透传出结果,ReactiveHttp 不太一样,ReactiveHttp 在这个基础上还实现了将网络请求和 ViewModel 以及 Activity/Fragment 相绑定的功能,ReactiveHttp 希望做到的是能够尽量完成大多数的通用行为,且每个层次均不强依赖于特定父类

Google 官方曾推出过一份最佳应用架构指南。当中,每个组件仅依赖于其下一级的组件。ViewModel 是关注点分离原则的一个具体实现,是作为用户数据的承载体处理者而存在的,Activity/Fragment 仅依赖于 ViewModel,ViewModel 就用于响应界面层的输入和驱动界面层变化,Repository 用于为 ViewModel 提供一个单一的数据来源及数据存储域,Repository 可以同时依赖于持久性数据模型和远程服务器数据源

ReactiveHttp 的设计思想类似于 Google 推荐的最佳应用架构指南

  • BaseRemoteDataSource 作为数据提供者处于最下层,只用于向上层提供数据,提供了多个同步请求和异步请求方法,和 BaseReactiveViewModel 之间依靠 IUIActionEvent 接口来联系
  • BaseReactiveViewModel 作为用户数据的承载体和处理者,包含了多个和网络请求事件相关的 LiveData 用于驱动界面层的 UI 变化,和 BaseReactiveActivity 之间依靠 IViewModelActionEvent 接口来联系
  • BaseReactiveActivity 包含与系统和用户交互的逻辑,其负责响应 BaseReactiveViewModel 中的数据变化,提供了和 BaseReactiveViewModel 进行绑定的方法

上文有说到,ReactiveHttp 提供了在网络请求过程中自动完成 showLoading、dismissLoading、showToast 等行为的能力。首先,BaseRemoteDataSource 在网络请求过程中会通过 IUIActionEvent 接口来通知 BaseReactiveViewModel 需要触发的行为,从而连锁触发 ShowLoadingLiveData、DismissLoadingLiveData、ShowToastLiveData 值的变化,BaseReactiveActivity 就通过监听 LiveData 值的变化来完成 UI 层操作

四、惯常做法

以下步骤应该是大部分应用目前进行网络请求时的惯常做法了

服务端返回给移动端的数据使用具有特定格式的 Json 来进行通信,用整数 status 来标明本次请求是否成功,在失败时则直接 showToast(msg)data则需要用泛型来声明了,最终就对应移动端的一个泛型类,类似于 HttpWrapBean

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

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

interface ApiService {

@POST("api1")
fun api1(): ObservableInt>>

@GET("api2")
fun api2(): Call>

}


然后在 interface 中声明 Api 接口,这也是使用 Retrofit 的惯常用法。根据项目中的实际情况,开发者可能是使用 Call 或者 Observable 作为每个接口返回值的最外层的数据包装类,然后再使用 HttpWrapBean 来作为具体数据类的包装类

然后项目中使用的是 RxJava,那么就需要像以下这样来完成网络请

val retrofit = Retrofit.Builder()
.baseUrl("https://xxx.com")
.addConverterFactory(GsonConverterFactory.create())
.addCallAdapterFactory(RxJava2CallAdapterFactory.create())
.build()
val service = retrofit.create(ApiService::class.java)
val call: ObservableInt>> = service.api1()
call.subscribe(object : ConsumerInt>> {
override fun accept(userBean: HttpWrapBean?) {

}

}, object : Consumer {
override fun accept(t: Throwable?) {

}
})


五、简单入门

ReactiveHttp 在使用上会比上面给出的例子简单很多,下面就来看下通过 ReactiveHttp 如何完成网络请求

ReactiveHttp 需要知道网络请求的结果,但不知道外部会使用什么字段名来标识 HttpWrapBean 中的三个值,所以需要外部实现 IHttpWrapBean 接口来进行标明。例如,你可以这样来实现:

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

override val httpCode: Int
get() = status

override val httpMsg: String
get() = msg

override val httpData: T
get() = data

//网络请求是否成功
override val httpIsSuccess: Boolean
get() = status == 200

}


suspend来修饰接口方法,且不需要其它的外层包装类。suspend是 kotlin 协程引入的,当用该关键字修饰接口方法时,Retrofit 内部就会使用协程的方式来完成该网络请求

interface ApiService {

@GET("config/district")
suspend fun getProvince(): HttpWrapBean>

}


ReactiveHttp 提供了 RemoteExtendDataSource 交由外部来继承实现。RemoteExtendDataSource 包含了所有的网络请求方法,外部仅需要根据实际情况来实现三个必要的字段和方法即可

  • releaseUrl。即应用的 BaseUrl
  • createRetrofit。用于创建 Retrofit,开发者可以在这里自定义 OkHttpClient
  • showToast。当网络请求失败时,通过该方法来向用户提示失败原因

例如,你可以像以下这样来实现你自己项目的专属 DataSource,当中就包含了开发者整个项目的全局网络请求配置

class SelfRemoteDataSource(iActionEvent: IUIActionEvent?) : RemoteExtendDataSource(iActionEvent, ApiService::class.java) {

companion object {

private val httpClient: OkHttpClient by lazy {
createHttpClient()
}

private fun createHttpClient(): OkHttpClient {
val builder = OkHttpClient.Builder()
.readTimeout(1000L, TimeUnit.MILLISECONDS)
.writeTimeout(1000L, TimeUnit.MILLISECONDS)
.connectTimeout(1000L, TimeUnit.MILLISECONDS)
.retryOnConnectionFailure(true)
.addInterceptor(FilterInterceptor())
.addInterceptor(MonitorInterceptor(MainApplication.context))
return builder.build()
}

}

/**
* 由子类实现此字段以便获取 release 环境下的接口 BaseUrl
*/
override val releaseUrl: String
get() = "https://restapi.amap.com/v3/"

/**
* 允许子类自己来实现创建 Retrofit 的逻辑
* 外部无需缓存 Retrofit 实例,ReactiveHttp 内部已做好缓存处理
* 但外部需要自己判断是否需要对 OKHttpClient 进行缓存
* @param baseUrl
*/
override fun createRetrofit(baseUrl: String): Retrofit {
return Retrofit.Builder()
.client(httpClient)
.baseUrl(baseUrl)
.addConverterFactory(GsonConverterFactory.create())
.build()
}

override fun showToast(msg: String) {
Toast.makeText(MainApplication.context, msg, Toast.LENGTH_SHORT).show()
}

}


之后,我们就可以依靠 SelfRemoteDataSource 在任意地方发起网络请求了,按需声明 Callback 方法。此外,由于使用到了扩展函数,所以 SelfRemoteDataSource 中可以直接调用 ApiService 中的接口方法,无需特意引用和导包

六、进阶使用

上述在使用 SelfRemoteDataSource 发起网络请求时虽然调用的是 enqueueLoading 方法,但实际上并不会弹出 loading 框,因为完成 ShowLoading、DismissLoading、ShowToast 等 UI 行为是需要 RemoteDataSource、ViewModel 和 Activity 这三者一起进行配合的,即 SelfRemoteDataSource 需要和其它两者关联上,将需要触发的 UI 行为反馈给 Activity

这可以通过直接继承于 BaseReactiveActivity 和 BaseReactiveViewModel 来实现,也可以通过实现相应接口来完成关联。当然,如果你不需要 ReactiveHttp 的各个自动化行为的话,也可以不做以下任何改动

总的来说,ReactiveHttp 具有极低的接入成本

1、BaseReactiveActivity

BaseReactiveActivity 是 ReactiveHttp 提供的一个默认 BaseActivity,其实现了 IUIActionEventObserver 接口,用于提供一些默认参数和默认行为,例如 CoroutineScope 和 showLoading。但在大多数情况下,我们自己的项目是不会去继承外部 Activity 的,而是会有一个自己实现的全局统一的 BaseActivity,所以如果你不想继承 BaseReactiveActivity 的话,可以自己来实现 IUIActionEventObserver 接口,就像以下这样

@SuppressLint("Registered")
abstract class BaseActivity : AppCompatActivity(), IUIActionEventObserver {

protected inline fun getViewModel(
factory: ViewModelProvider.Factory? = null,
noinline initializer: (VM.(lifecycleOwner: LifecycleOwner) -> Unit)? = null
): Lazy where VM : ViewModel, VM : IViewModelActionEvent {
return getViewModel(VM::class.java, factory, initializer)
}

override val lifecycleSupportedScope: CoroutineScope
get() = lifecycleScope

override val lContext: Context?
get() = this

override val lLifecycleOwner: LifecycleOwner
get() = this

private var loadDialog: ProgressDialog? = null

override fun showLoading(job: Job?) {
dismissLoading()
loadDialog = ProgressDialog(lContext).apply {
setCancelable(true)
setCanceledOnTouchOutside(false)
//用于实现当弹窗销毁的时候同时取消网络请求
// setOnDismissListener {
// job?.cancel()
// }
show()
}
}

override fun dismissLoading() {
loadDialog?.takeIf { it.isShowing }?.dismiss()
loadDialog = nul
}


2、BaseReactiveViewModel

类似地,BaseReactiveViewModel 是 ReactiveHttp 提供的一个默认的 BaseViewModel,其实现了 IViewModelActionEvent 接口,用于接收 RemoteDataSource 发起的 UI 层行为。如果你不希望继承于 BaseReactiveViewModel 的话,可以自己来实现 IViewModelActionEvent 接口,就像以下这样

open class BaseViewModel : ViewModel(), IViewModelActionEvent {

override val lifecycleSupportedScope: CoroutineScope
get() = viewModelScope

override val showLoadingEventLD = MutableLiveData()

override val dismissLoadingEventLD = MutableLiveData()

override val showToastEventLD = MutableLiveData()

override val finishViewEventLD = MutableLiveData()

}


3、关联上

完成以上两步后,开发者就可以像如下所示这样将 RemoteDataSource、ViewModel 和 Activity 这三者给关联起来。WeatherActivity 通过 getViewModel 方法来完成 WeatherViewModel 的初始化和内部多个 UILiveData 的绑定,并在 lambda 表达式中完成对 WeatherViewModel 内部和具体业务相关的 DataLiveData 的数据监听,至此所有自动化行为就都已经绑定上了

class WeatherViewModel : BaseReactiveViewModel() {

private val remoteDataSource by lazy {
SelfRemoteDataSource(this)
}

val forecastsBeanLiveData = MutableLiveData()

fun getWeather(city: String) {
remoteDataSource.enqueue({
getWeather(city)
}) {
onSuccess {
if (it.isNotEmpty()) {
forecastsBeanLiveData.value = it[0]
}
}
}
}

}

class WeatherActivity : BaseReactiveActivity() {

private val weatherViewModel by getViewModel {
forecastsBeanLiveData.observe(this@WeatherActivity, {
showWeather(it)
})
}

override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_weather)
weatherViewModel.getWeather("adCode")
}

private fun showWeather(forecastsBean: ForecastsBean) {

}

}


七、其它

1、BaseRemoteDataSource

RemoteExtendDataSource 提供了许多个可以进行复写的方法,既可用于配置 OkHttp 的各个网络请求参数,也用于交由外部进行流程控制。例如,你可以这样来实现自己项目的 BaseRemoteDataSource

class BaseRemoteDataSource(iActionEvent: IUIActionEvent?) : RemoteExtendDataSource(iActionEvent, ApiService::class.java) {

companion object {

private val httpClient: OkHttpClient by lazy {
createHttpClient()
}

private fun createHttpClient(): OkHttpClient {
val builder = OkHttpClient.Builder()
.readTimeout(1000L, TimeUnit.MILLISECONDS)
.writeTimeout(1000L, TimeUnit.MILLISECONDS)
.connectTimeout(1000L, TimeUnit.MILLISECONDS)
.retryOnConnectionFailure(true)
.addInterceptor(FilterInterceptor())
.addInterceptor(MonitorInterceptor(MainApplication.context))
return builder.build()
}
}

/**
* 由子类实现此字段以便获取 release 环境下的接口 BaseUrl
*/
override val releaseUrl: String
get() = HttpConfig.BASE_URL_MAP

/**
* 允许子类自己来实现创建 Retrofit 的逻辑
* 外部无需缓存 Retrofit 实例,ReactiveHttp 内部已做好缓存处理
* 但外部需要自己判断是否需要对 OKHttpClient 进行缓存
* @param baseUrl
*/
override fun createRetrofit(baseUrl: String): Retrofit {
return Retrofit.Builder()
.client(httpClient)
.baseUrl(baseUrl)
.addConverterFactory(GsonConverterFactory.create())
.build()
}

/**
* 如果外部想要对 Throwable 进行特殊处理,则可以重写此方法,用于改变 Exception 类型
* 例如,在 token 失效时接口一般是会返回特定一个 httpCode 用于表明移动端需要去更新 token 了
* 此时外部就可以实现一个 BaseException 的子类 TokenInvalidException 并在此处返回
* 从而做到接口异常原因强提醒的效果,而不用去纠结 httpCode 到底是多少
*/
override fun generateBaseException(throwable: Throwable): BaseHttpException {
return if (throwable is BaseHttpException) {
throwable
} else {
LocalBadException(throwable)
}
}

/**
* 用于由外部中转控制当抛出异常时是否走 onFail 回调,当返回 true 时则回调,否则不回调
* @param httpException
*/
override fun exceptionHandle(httpException: BaseHttpException): Boolean {
return true
}

/**
* 用于将网络请求过程中的异常反馈给外部,以便记录
* @param throwable
*/
override fun exceptionRecord(throwable: Throwable) {
Log.e("SelfRemoteDataSource", throwable.message ?: "")
}

/**
* 用于对 BaseException 进行格式化,以便在请求失败时 Toast 提示错误信息
* @param httpException
*/
override fun exceptionFormat(httpException: BaseHttpException): String {
return when (httpException.realException) {
null -> {
httpException.errorMessage
}
is ConnectException, is SocketTimeoutException, is UnknownHostException -> {
"连接超时,请检查您的网络设置"
}
else -> {
"请求过程抛出异常:" + httpException.errorMessage
}
}
}

override fun showToast(msg: String) {
Toast.makeText(MainApplication.context, msg, Toast.LENGTH_SHORT).show()
}

}


此外,开发者可以直接在自己的 BaseViewModel 中声明一个 BaseRemoteDataSource 变量实例,所有子 ViewModel 都全局统一使用同一份 DataSource 配置。如果有某些特定接口需要使用不同的 BaseUrl 的话,也可以再多声明一个 BaseRemoteDataSource

open class BaseViewModel : BaseReactiveViewModel() {

/**
* 正常来说单个项目中应该只有一个 RemoteDataSource 实现类,即全局使用同一份配置
* 但父类也应该允许子类使用一个独有的 RemoteDataSource,即允许子类复写此字段
*/
protected open val remoteDataSource by lazy {
BaseRemoteDataSource(this)
}

}


2、BaseHttpException

BaseHttpException 是 ReactiveHttp 对网络请求过程中发生的各类异常情况的包装类,任何透传到外部的异常信息均会被封装为 BaseHttpException 类型。BaseHttpException 有两个默认子类,分别用于表示服务器异常和本地异

/**
* @param errorCode 服务器返回的错误码 或者是 HttpConfig 中定义的本地错误码
* @param errorMessage 服务器返回的异常信息 或者是 请求过程中抛出的信息,是最原始的异常信息
* @param realException 用于当 code 是本地错误码时,存储真实的运行时异常
*/
open class BaseHttpException(val errorCode: Int, val errorMessage: String, val realException: Throwable?) : Exception(errorMessage) {

companion object {

/**
* 此变量用于表示在网络请求过程过程中抛出了异常
*/
const val CODE_ERROR_LOCAL_UNKNOWN = -1024520

}

/**
* 是否是由于服务器返回的 code != successCode 导致的异常
*/
val isServerCodeBadException: Boolean
get() = this is ServerCodeBadException

/**
* 是否是由于网络请求过程中抛出的异常(例如:服务器返回的 Json 解析失败)
*/
val isLocalBadException: Boolean
get() = this is LocalBadException

}

/**
* API 请求成功了,但 code != successCode
* @param errorCode
* @param errorMessage
*/
class ServerCodeBadException(errorCode: Int, errorMessage: String) : BaseHttpException(errorCode, errorMessage, null) {

constructor(bean: IHttpWrapBean<*>) : this(bean.httpCode, bean.httpMsg)

}

/**
* 请求过程抛出异常
* @param throwable
*/
class LocalBadException(throwable: Throwable) : BaseHttpException(CODE_ERROR_LOCAL_UNKNOWN, throwable.message?: "", throwable)


有时候开发者需要对某些异常情况进行特殊处理,此时就可以来实现自己的 BaseHttpException 子类。例如,在 token 失效时接口一般是会返回特定一个 httpCode 用于表明移动端需要去更新 token 了,此时开发者就可以实现一个 BaseHttpException 的子类 TokenInvalidException 并在 BaseRemoteDataSource 中进行返回,从而做到接口异常原因强提醒的效果,而不用去纠结 httpCode 到底是多少

class TokenInvalidException : BaseHttpException(CODE_TOKEN_INVALID, "token已失效", null)

open class BaseRemoteDataSource(iActionEvent: IUIActionEvent?) : RemoteExtendDataSource(iActionEvent, ApiService::class.java) {

companion object {

private val httpClient: OkHttpClient by lazy {
createHttpClient()
}

private fun createHttpClient(): OkHttpClient {
val builder = OkHttpClient.Builder()
.readTimeout(1000L, TimeUnit.MILLISECONDS)
.writeTimeout(1000L, TimeUnit.MILLISECONDS)
.connectTimeout(1000L, TimeUnit.MILLISECONDS)
.retryOnConnectionFailure(true)
.addInterceptor(FilterInterceptor())
.addInterceptor(MonitorInterceptor(MainApplication.context))
return builder.build()
}
}

/**
* 由子类实现此字段以便获取 release 环境下的接口 BaseUrl
*/
override val releaseUrl: String
get() = "https://restapi.amap.com/v3/"

/**
* 允许子类自己来实现创建 Retrofit 的逻辑
* 外部无需缓存 Retrofit 实例,ReactiveHttp 内部已做好缓存处理
* 但外部需要自己判断是否需要对 OKHttpClient 进行缓存
* @param baseUrl
*/
override fun createRetrofit(baseUrl: String): Retrofit {
return Retrofit.Builder()
.client(httpClient)
.baseUrl(baseUrl)
.addConverterFactory(GsonConverterFactory.create())
.build()
}

/**
* 如果外部想要对 Throwable 进行特殊处理,则可以重写此方法,用于改变 Exception 类型
* 例如,在 token 失效时接口一般是会返回特定一个 httpCode 用于表明移动端需要去更新 token 了
* 此时外部就可以实现一个 BaseException 的子类 TokenInvalidException 并在此处返回
* 从而做到接口异常原因强提醒的效果,而不用去纠结 httpCode 到底是多少
*/
override fun generateBaseException(throwable: Throwable): BaseHttpException {
if (throwable is ServerCodeBadException && throwable.errorCode == BaseHttpException.CODE_TOKEN_INVALID) {
return TokenInvalidException()
}
return if (throwable is BaseHttpException) {
throwable
} else {
LocalBadException(throwable)
}
}

/**
* 用于由外部中转控制当抛出异常时是否走 onFail 回调,当返回 true 时则回调,否则不回调
* @param httpException
*/
override fun exceptionHandle(httpException: BaseHttpException): Boolean {
return httpException !is TokenInvalidException
}

override fun showToast(msg: String) {
Toast.makeText(MainApplication.context, msg, Toast.LENGTH_SHORT).show()
}
}


1 个评论

感谢大佬,正好需要这方面的东西

要回复文章请先登录注册