前言

API 层就是网络层,是一个 App 必不可少的模块。我从 12 年开始做安卓开发,从这些年的开发经验中对 API 层的实践进行一些总结,内容方面主要是围绕 HttpClient 的选择,响应处理的编程模型和通知 UI 数据更新的最佳方式。

以下内容仅仅是个人观点,与实际内容如有出入,烦请指出;若喷,请轻点。

SDK 中的 Http Client

标题中的 Http Client 是一个泛指,可能与某个 http 请求库重名,它泛指所有的 http 请求客户端。

SDK 中的 client 有 2 个:HttpURLConnection和 Apache 的HttpClient库。

在最早的时候(大概 Android1.x 开始),SDK 把 Java 的HttpURLConnection照搬过来。但是HttpURLConnection很底层,用起来非常麻烦。你发一个 Get 请求还要操作流,没有 20 行代码下不来,上传文件要自己拼multi-part块,而且这个类在 Android2.2 之前还有内存泄漏的 Bug。

估计谷歌自己也不想用,就将 Apache 的HttpClient库内置到 SDK 中了。在易用性上确实简洁不少,也实现了像marti-part这种编码,不用我们手动拼了。但是缺点是太面向对象了,代码比较臃肿。发送 Post 请求,再加点 Header,就要创建很多的对象,代码量依然下不来。于是当时诞生了很多针对 HttpClient 进行封装的类库,我用的最多的就是android-async-httpxutil。Android5.0 之后,SDK 将 Apache 的HttpClient移除了。

当然也有针对HttpURLConnection进行封装的类库,比如谷歌自家的Volley。Volley 的性能优秀,且内置图片加载功能。当时风光过一阵,直到现在我仍然能看到有许多三方库 http 使用 Volley 来做。Volley 的缺点是部分 Http 功能不完善,比如默认不能发送 Post 请求,需要手写一些代码;不支持重定向。

现代化的 Http Client

Http Client 的话题还没有说完,上面说到谷歌在 2013 年的 IO 大会上推了自家的 Volley;但是会议上出现了一个小插曲:

当谷歌的开发者在介绍 Volley 的时候,下面的某个听众喊道:

"I prefer OkHttp。"

当时引得众人大笑,介绍的人员值得很无奈的回了一句:"Yeah, I like OkHttp too."

然后 OkHttp 就火了,好像 Volley 的介绍是为了让人们知道 OkHttp。

为什么 OkHttp 火?

  • 它功能完善:Http 编码,协议和 Http Verb 的完全支持,Http Cache 的完美支持
  • 它性能优越:它既没有基于HttpURLConnection,也没有基于HttpClient;自己用 socket 重新实现了一套。内置连接池,会重用连接,会选择最佳的 Host,让网络延时降到最低
  • 它竟然支持拦截器这种现代化的网络功能
  • 它 API 简洁

OkHttp 是目前 Android 和 Java 平台最优秀的 Http Client,没有之一。同时也诞生了基于 OkHttp 进行封装的三方库,比如:OkhttputilsOkGo,它们使用起来都非常简单。 如果你喜欢注解,可以试试同一个团队出品的Retrofit

顺便普及一下人员信息:

  • Square 公司:美国的一家做支付的公司,Okhttp 和 Retrofit 的出品团队,团队有个大牛叫JakeWharton

  • JakeWharton: Android 界的顶尖大牛,现在去了谷歌,在做 Kotlin 方面的工作。很多人知道他写了 ButterKnife,OkHttp,Retrofit,但是可能不知道当年谷歌团队的support-v4包还没有支持属性动画的时候,人人都用他的NineOldAndroid类库来做属性动画;当年谷歌团队的support-v7包还没有出现的时候,人人都用它的ActionBarSherlock来做 ActionBar。真正的是一个人撑起一片天。

响应处理的编程模型

在 Client 的选择上,OkHttp 是最佳选择。但是在响应处理的编程模型上,目前所有的 Client 都提供了 Callback 的模型来处理响应,用伪代码表示就是:

XXClient client = new XXClient();
client.url("https://github.com/li-xiaojun")
      .header("a", "b")
      .params("c", "d")
      .post(new HttpCallback<Bean>(){
          public void onError(IOException e){
              //do something
          }
          public void onSuccess(Bean bean){
              //do something
          }
      });
复制代码

回调的模型在代码复杂的时候回陷入Callback Hell的问题,当然你可以用抽取方法来重构,也可以用 RxJava 来打平回调的层级;但在可读性方面仍然没有同步的代码看上去漂亮。来看一个同步模型的代码:

Bean bean = client.url("https://github.com/li-xiaojun")
      .header("a", "b")
      .params("c", "d")
      .<Bean>post(); //异步请求
Result bean = process(baen);
saveDB(bean);//异步操作
复制代码

显然同步模型会更具可读性,哪怕你异步逻辑再复杂,可读性都不会减少一点。如何能让同步的代码发送异步的请求呢?

Java 可以用 Future 来实现,更优雅的是 Kotlin 的协程。使用 Kotlin 协程的代码看起来像这样:

GlobalScope.launch {
    Bean bean = client.url("https://github.com/li-xiaojun")
          .header("a", "b")
          .params("c", "d")
          .<Bean>post().await(); // 异步请求
    Result bean = process(baen);// 非异步
    saveDB(bean).await();// 异步操作
}
复制代码

Kotlin 的 Coroutine 和其他语言的协程一样,拥有 2 大优点:更好的调度性能,异步代码变同步。这里不会讨论协程如何使用,只是用到了协程;如果要学习协程,最好的资源就是 Kotlin 官方网站。

如何通知 UI 数据更新

如果你的 API 层写在 UI 中,完全没有这个问题,但这显然不具有任何维护性和可扩展性。当我们将 API 单独抽出一个层(一般是 MVP 的 P 层)的时候,数据获取和处理的代码合 UI 分离了,必然面临这个问题。

一般有 3 种处理方式:

  • 自定义 Callback
  • 使用 EventBus
  • 使用 LiveData

用自定义 Callback 的方式编写的代码看起来像这样:

class LoginPresenter{
    fun login(username: String, psw: String, listener: OnLoginListener){
        GlobalScope.launch {
            Bean bean = client.url("https://github.com/li-xiaojun")
                  .header("a", "b")
                  .params("c", "d")
                  .<Bean>post().await() // 异步请求
            if(bean!=null){
                listener.onLoginSuccess(bean)
            }else{
                listener.onError(...)
            }
        }
    }
}
复制代码

这种方式的需要每个逻辑都要自定义一个回调,代码量巨大,且丑陋,不可取。

使用 EventBus 来通知 UI,代码写起来想这样:

class LoginPresenter{
    const EventLoginSuccess = "EventLoginSuccess"
    const EventLoginFail = "EventLoginFail"
<span class="hljs-function"><span class="hljs-keyword">fun</span> <span class="hljs-title">login</span><span class="hljs-params">(username: <span class="hljs-type">String</span>, psw: <span class="hljs-type">String</span>)</span></span>{
    GlobalScope.launch {
        Bean bean = client.url(<span class="hljs-string">"https://github.com/li-xiaojun"</span>)
              .header(<span class="hljs-string">"a"</span>, <span class="hljs-string">"b"</span>)
              .params(<span class="hljs-string">"c"</span>, <span class="hljs-string">"d"</span>)
              .&lt;Bean&gt;post().await() <span class="hljs-comment">//异步请求</span>
        <span class="hljs-keyword">if</span>(bean!=<span class="hljs-literal">null</span>){
            EventBus.<span class="hljs-keyword">get</span>().post(new Event(EventLoginSuccess, bean))
        }<span class="hljs-keyword">else</span>{
            EventBus.<span class="hljs-keyword">get</span>().post(new Event(EventLoginFail, <span class="hljs-literal">null</span>))
        }
    }
}

}

复制代码

可以看到,EventBus 的方式让我们不用去定义大量的回调,换了种方式去定义大量的 Event 标识。当项目复杂后,可能有上百个 Event 标识,并不容易管理。所以这种方式不是最佳的方式。

LiveData 的方式代码写起来像这样:

class LoginPresenter{
    var loginData = MutableLiveData<Bean>()
<span class="hljs-function"><span class="hljs-keyword">fun</span> <span class="hljs-title">login</span><span class="hljs-params">(username: <span class="hljs-type">String</span>, psw: <span class="hljs-type">String</span>)</span></span>{
    GlobalScope.launch {
        Bean bean = client.url(<span class="hljs-string">"https://github.com/li-xiaojun"</span>)
              .header(<span class="hljs-string">"a"</span>, <span class="hljs-string">"b"</span>)
              .params(<span class="hljs-string">"c"</span>, <span class="hljs-string">"d"</span>)
              .&lt;Bean&gt;post().await() <span class="hljs-comment">//异步请求</span>
        loginData.postValue(bean)
    }
}

}

复制代码

可以看到,LiveData 的方式可以让我们避免去定义回调和 Event 的标识,写法上更简洁。更重要的是,LiveData 天然能观察 UI 生命周期变化,能避免一些内存泄漏,以及在最佳时刻更新 UI。

MVP 和 MVVM

客户端主要和 UI 打交道,最高效的架构一定是 MVVM;前端的 Vue 和 React 已经完全证实了这一点。

Android 上的 MVVM 主要有 3 种实现:

  • LiveData 和 ViewModel
  • DataBinding
  • 基于 Kotlin 代理去实现 VM 层

其中 DataBinding 需要学习一些特定语法,和前端的 Vue 很像,而且因为用了反射,在复杂的更新频率高的界面会有一点性能问题;不过也是很不错的一种选择。

Kotlin 天然支持属性代理,我们可以基于 Kotlin 的代理语法来实现 UI 的动态更新,不过这个需要一些精力。

个人最喜欢的是 LiveData 和 ViewModel。

上个小节的 Presenter 层显示没有处理 UI 生命周期变化的逻辑,比如当 UI 结束时,Presenter 是无法得知的,从而无法去释放一些资源。你可以手动去写一些代码,但是 ViewModel 是最佳选择,它天然可以监视 UI 销毁。所以换成 ViewMode 的代码是这样的:

class LoginViewModel : ViewModel(){
    var loginData = MutableLiveData<Bean>()
<span class="hljs-function"><span class="hljs-keyword">fun</span> <span class="hljs-title">login</span><span class="hljs-params">(username: <span class="hljs-type">String</span>, psw: <span class="hljs-type">String</span>)</span></span>{
    GlobalScope.launch {
        Bean bean = client.url(<span class="hljs-string">"https://github.com/li-xiaojun"</span>)
              .header(<span class="hljs-string">"a"</span>, <span class="hljs-string">"b"</span>)
              .params(<span class="hljs-string">"c"</span>, <span class="hljs-string">"d"</span>)
              .&lt;Bean&gt;post().await() <span class="hljs-comment">//异步请求</span>
        loginData.postValue(bean)
    }
}
<span class="hljs-comment">//UI销毁时执行</span>
<span class="hljs-function"><span class="hljs-keyword">fun</span> <span class="hljs-title">onCleard</span><span class="hljs-params">()</span></span>{
    <span class="hljs-comment">//释放资源的代码</span>
}

}

复制代码

最佳实践

综上所述,根据我个人经验得出的最佳实践是:选择 OkHttp 发送请求,使用 Kotlin Coroutine 处理响应,用 LiveData 来通知 UI 更新;将这些逻辑抽象为 VM 层,具体表现为 ViewModel。

网络请求本质上不就是从一个 URL 得到一个实体类吗?这样是不是更好一些呢?

GlobalScope.launch {
    //get 请求
    val user = "https://github.com/li-xiaojun".http().get<User>().await()
    //post 请求
    val user = "https://github.com/li-xiaojun".http()
                    .headers("token" to "xxaaav34", ...)
                    .params("phone" to "188888888",
                            "file" to file,  // 上传文件
                     ...)
                    .post<User>()
                    .await()
}
复制代码

上面的代码使用我的开源库AndroidKTX就可以做到。有人说,这么简单,那支持其他请求方式,设置全局 Header,设置自定义拦截器,支持 HTTPS 吗?这些是一个网络库的基本功能,当然支持啦。

AndroidKTX的 Github 地址是:github.com/li-xiaojun/…

所以,贴下我项目中 API 层的实践代码:

class LoginViewModel : ViewModel(){
    var loginData = MutableLiveData<User?>()
<span class="hljs-function"><span class="hljs-keyword">fun</span> <span class="hljs-title">login</span><span class="hljs-params">(username: <span class="hljs-type">String</span>, psw: <span class="hljs-type">String</span>)</span></span>{
    GlobalScope.launch {
        <span class="hljs-keyword">val</span> user = <span class="hljs-string">"https://github.com/li-xiaojun"</span>.http()
                .params(<span class="hljs-string">"phone"</span> to <span class="hljs-string">"188888888"</span>, <span class="hljs-string">"password"</span> to <span class="hljs-string">"111111"</span>)
                .post&lt;User&gt;()
                .await() <span class="hljs-comment">// 为null表示请求失败</span>
        loginData.postValue(user)
    }
}
<span class="hljs-comment">//UI销毁时执行</span>
<span class="hljs-function"><span class="hljs-keyword">fun</span> <span class="hljs-title">onCleard</span><span class="hljs-params">()</span></span>{
    <span class="hljs-comment">//释放资源的代码</span>
}

}

复制代码

UI 层的代码大概是这样:

class LoginActivity: AppCompatActivity() {
<span class="hljs-function"><span class="hljs-keyword">fun</span> <span class="hljs-title">loadData</span><span class="hljs-params">()</span></span>{
    loginVM.loginData.observe(<span class="hljs-keyword">this</span>, Observe {
        <span class="hljs-keyword">if</span>(it==<span class="hljs-literal">null</span>){
            toast(<span class="hljs-string">"请求出错"</span>)
        }<span class="hljs-keyword">else</span>{
           updateUI(it) 
        }
    })    
    <span class="hljs-comment">//执行登录</span>
    loginVM.login(username, password)
}

}

复制代码

  • Android

    开放手机联盟(一个由 30 多家科技公司和手机公司组成的团体)已开发出 Android,Android 是第一个完整、开放、免费的手机平台。

    293 引用
感谢    赞同    分享    收藏    关注    反对    举报    ...