不久前,我用如下代码完成了 配料管理 的第一个网络请求,虽然是拾人牙慧的东西,但是也有点小兴奋,如果你用过AsyckTask
你就会发现,在API设计上,AsyckTask
和下面这段代码都有前、中、后三个概念,我们通过阅读AsyckTask
的源码发现了表面上貌似理所当然的API其实内里大有文章。
start { dpseList.value?.clear() }.request {
forkApi(AhrmmService::class.java).enabledList(DevicePeripheralScaleEnabledListRequest())
}.then({
dpseList.value?.addAll(it.data)
})
其实早在 蓝+2.0.0 改版的时候,网络框架就开始改变了,回想在那之前,我们是怎么请求一个接口并获得数据做一些业务上的处理的?
HTTP协议网络交互+使用API
我认为,正常满足我们开发需求的所谓的网络架构,其实包含两个部分,一个是基于HTTP协议帮我们拼接请求报文、发起请求、收到服务器响应和预处理响应报文的部分;而另一个就二次封装以便我们更灵活、更高效使用的部分了。前者我们大概率/几乎没可能自己写,所以我们能做的只有选择工作,选择更好的官方/开源框架;而后者才是我们有能力/应该花心思解决优化以便业务开发上用起来更爽的部分。
AsyncHttpClient+AsyncHttpResponseHandler
天使 应该是数一数二的元老级项目,目前为止我们在天使上维护某个接口、需要debug或者重写业务逻辑的时候,总是习惯性的搜一下onSuccessResponse
,因为他们是我们界面搜索接口请求成功的回调,事实上,这些可以在activity
/fragment
上通过重写的回调方法,他们都是通过继承改造AsyncHttpResponseHandler
,通过几次转接、中间做一下预处理/统一错误处理实现的。今天看来,虽然做法不太好,效果也有限制,但是那个时候我们就已经是按照这两部分来做网络架构的了,其中HTTP协议网络交互用的是AcyncHttp
,使用API则是改造了它自带的AsyncHttpResponseHandler
抽象类。
OkHttp3+Retrofit2&Rxjava2
要体谅旧项目的维护不易,维稳第一,所以 蓝+2.0.0 改版的时候,团队犹如久旱逢甘霖般尽情吸收着各种新鲜技术开源框架,其中OkHttp3+Retrofit2&Rxjava2
就是网络架构上的大改动,OkHttp3
负责HTTP协议交互部分,内里严格根据Http协议定义了很多方便好用的API,虽然我们很少用到,默认配置就足以满足90%使用场景,但是一个足够强大的网络请求框架确实能给人带来自信,下面我们看纯粹使用Okhttp3
的网络请求:
OkHttpClient client = new OkHttpClient();
String run(String url) throws IOException {
Request request = new Request.Builder()
.url(url)
.build();
try (Response response = client.newCall(request).execute()) {
return response.body().string();
}
}
如果是做一个Demo当然是无所谓,但是在项目开发中我们还需要一些配置,该封装的封装,该抽象的抽象;而配套的Retrofit则能帮助我们实现业务上的分离,而且分离的方式很简洁:
/**
* 1.用户登录
*/
@POST("bm-officeAuto-admin-api/common/user/login")
fun login(@Body body: RequestLogin): Observable<ResultLogin>
可以看到,短短一个抽象方法,已经包含了method、path、请求body,响应body四部分重要的信息,最重要的是,它不需要自己实现,Retrofit通过动态代理的方式帮你创建对象处理相应的逻辑,可以说它是我见过的数一数二漂亮的API了。
紧接着就又要回到一开始就说到的前、中、后三个概念,我们用惯的理所当然的API其实内部必然隐藏着线程切换的过程,道理也很简单,网络请求是耗时操作,本来就不应该放到主线程,而数据与界面交互的部分却又是必然要放在主线程的,所以完成一个接口的请求,加载数据到界面上最少要在两个线程上切换,而怎么使得这个切换对业务开发隐藏,使得业务开发完全无感/感到舒适,就是我们设计这个API要考虑的最重要的问题,当然还有其它诸如使用灵活、配置方面的问题。
而RxJava就可以使得线程的切换,不再一昧的嵌套,而是将其铺平,使得整个线程切换过程变得非常符合直觉——一种先做什么、再做什么的流式代码结构。
mModel.login(rl)
// doOnSubscribe之后再次调用才能改变被观察者的线程
.subscribeOn(Schedulers.io())
//遇到错误时重试,第一个参数为重试几次,第二个参数为重试的间隔
.retryWhen(RetryWithDelay(3, 2))
.doOnSubscribe { mRootView.showLoading() }
.compose(BPUtil.commonNetAndLifeHandler<ResultLogin>(mRootView.getActivity(), mRootView))
.observeOn(Schedulers.io())
// 登陆成功,调用获取信息
.flatMap {
ClientStateManager.loginToken = it.token
val body = RequestBase.newInstance()
return@flatMap mModel.getUserInfo(body)
}
.compose(BPUtil.commonNetAndLifeHandler<ResultGetUserInfo>(mRootView.getActivity(), mRootView))
.doFinally {
mRootView.hideLoading()
mRootView.disableLogin(false)
}
.subscribe(object : ErrorHandleSubscriber<ResultGetUserInfo>(mErrorHandler) {
override fun onNext(userInfo: ResultGetUserInfo) {
ClientStateManager.userInfo = userInfo.user
// 去主页面
Utils.navigation(mRootView as Context, RouterHub.APP_OAMAINACTIVITY)
mRootView.killMyself()
}
})
至此,我们网络架构完成了第一个转身,其中我们的整体架构也从MVC->MVP。
OkHttp3+Retrofit2&Coroutine
再看回开头的那段代码,我们将其省略掉的API完整放出来:
@POST("md/device/peripheral/scale/enabledList")
suspend fun enabledList(@Body bean: DevicePeripheralScaleEnabledListRequest): DevicePeripheralScaleEnabledListResult
start { dpseList.value?.clear() }.request {
forkApi(AhrmmService::class.java).enabledList(DevicePeripheralScaleEnabledListRequest())
}.then(onSuccess = {
dpseList.value?.addAll(it.data)
},onException = {
false
},onError = {
},onComplete = {})
我们对比上面的RxJava的实现,来看看二者实现同样作用的相关代码的情况:
线程切换
RxJava:主动调用:subscribeOn(Schedulers.io())
,observeOn(Schedulers.io())
Coroutine:隐藏在内部通过关键字识别:suspend
省略API
一些公共的错误处理
RxJava:主动调用:compose
Coroutine:具名可选,省略则为空
其它
Coroutine更加简洁直观,试想第一看到这两段代码,你更愿意用哪一种?
RxJava功能更加强大,通用性更强,遗憾的是我们百分之八九十的使用场景都是最普通的前、中、后就可以完成了。
那么,对于一些特殊的请求我们怎么办呢?
答案是特殊问题,特殊处理,设计API的时候都会面对这种权衡取舍的问题,越想兼容更多情况,设计起来和用起来就越复杂,我认为覆盖大多数使用场景已经足矣,不应该过度优化。
举个例子,两个接口并发,同时完成才能接着往后走,这种需求就是少见中的大多数了把:
lifecycleScope.launchWhenResumed {
try {
coroutineScope {
//异常双向传递模式
loading.value?.set(true)
forkApi(DosingService::class.java).apply {
val mds = async {
materialDetails(MaterialDetailsRequest(materialCode))
}
val dolr = async {
orderList(DosingOrderListRequest(materialCode, true))
}
mdData.value?.set(mds.await().data)
listViewModel?.recordList?.value?.addAll(dolr.await().data)
}
}
} catch (e: Exception) {
} finally {
loading.value?.set(false)
}
}
这段代码讲的是,同时请求materialDetails
和orderList
,我们想并发节约时间,并且后续业务时需要两个接口同时成功才可以进行下去的。
事实上Coroutine
提供了很多强大而简洁的API,这个建议我们团队重点学习,还是那句话,性价比很高。