不久前,我用如下代码完成了 配料管理
的第一个网络请求,虽然是拾人牙慧的东西,但是也有点小兴奋,如果你用过 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协议帮我们 拼接请求报文、发起请求、收到服务器响应和预处理响应报文 的部分;而另一个就二次封装以便我们更灵活、更高效使用的部分了。前者我们 大概率/几乎 没可能自己写,所以我们能做的只有选择工作,选择更好的 官方/开源框架 ;而后者才是我们 有能力/应该 花心思解决优化以便业务开发上用起来更爽的部分。
天使应该是数一数二的元老级项目,目前为止我们在天使上维护某个接口、需要debug或者重写业务逻辑的时候,总是习惯性的搜一下 onSuccessResponse
,因为他们是我们界面搜索接口请求成功的回调,事实上,这些可以在 activity
/ fragment
上通过重写的回调方法,他们都是通过继承改造 AsyncHttpResponseHandler
,通过几次转接、中间做一下 预处理/统一错误处理
实现的。今天看来,虽然做法不太好,效果也有限制,但是那个时候我们就已经是按照这两部分来做网络架构的了,其中HTTP协议网络交互用的是 AcyncHttp
,使用API则是改造了它自带的 AsyncHttpResponseHandler
抽象类。
要体谅旧项目的维护不易,维稳第一,所以 蓝+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 。
再看回开头的那段代码,我们将其省略掉的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
一些公共的错误处理
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,这个建议我们团队重点学习,还是那句话,性价比很高。