Android网络架构演进

4,173 阅读6分钟

不久前,我用如下代码完成了 配料管理 的第一个网络请求,虽然是拾人牙慧的东西,但是也有点小兴奋,如果你用过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)
    }
}

这段代码讲的是,同时请求materialDetailsorderList,我们想并发节约时间,并且后续业务时需要两个接口同时成功才可以进行下去的。

事实上Coroutine提供了很多强大而简洁的API,这个建议我们团队重点学习,还是那句话,性价比很高。