API层就是网络层,是一个App必不可少的模块。我从12年开始作安卓开发,从这些年的开发经验中对API层的实践进行一些总结,内容方面主要是围绕HttpClient的选择,响应处理的编程模型和通知UI数据更新的最佳方式。前端
如下内容仅仅是我的观点,与实际内容若有出入,烦请指出;若喷,请轻点。java
标题中的Http Client是一个泛指,可能与某个http请求库重名,它泛指全部的http请求客户端。android
SDK中的client有2个:HttpURLConnection
和Apache的HttpClient
库。git
在最先的时候(大概Android1.x开始),SDK把Java的HttpURLConnection
照搬过来。可是HttpURLConnection
很底层,用起来很是麻烦。你发一个Get请求还要操做流,没有20行代码下不来,上传文件要本身拼multi-part
块,并且这个类在Android2.2以前还有内存泄漏的Bug。github
估计谷歌本身也不想用,就将Apache的HttpClient
库内置到SDK中了。在易用性上确实简洁很多,也实现了像marti-part
这种编码,不用咱们手动拼了。可是缺点是太面向对象了,代码比较臃肿。发送Post请求,再加点Header,就要建立不少的对象,代码量依然下不来。因而当时诞生了不少针对HttpClient进行封装的类库,我用的最多的就是android-async-http
和xutil
。Android5.0以后,SDK将Apache的HttpClient
移除了。编程
固然也有针对HttpURLConnection
进行封装的类库,好比谷歌自家的Volley
。Volley的性能优秀,且内置图片加载功能。当时风光过一阵,直到如今我仍然能看到有许多三方库http使用Volley来作。Volley的缺点是部分Http功能不完善,好比默认不能发送Post请求,须要手写一些代码;不支持重定向。bash
Http Client的话题尚未说完,上面说到谷歌在2013年的IO大会上推了自家的Volley;可是会议上出现了一个小插曲:markdown
当谷歌的开发者在介绍Volley的时候,下面的某个听众喊道:网络
"I prefer OkHttp。"架构
当时引得众人大笑,介绍的人员值得很无奈的回了一句:"Yeah, I like OkHttp too."
而后OkHttp就火了,好像Volley的介绍是为了让人们知道OkHttp。
为何OkHttp火?
HttpURLConnection
,也没有基于HttpClient
;本身用socket从新实现了一套。内置链接池,会重用链接,会选择最佳的Host,让网络延时降到最低OkHttp是目前Android和Java平台最优秀的Http Client,没有之一。同时也诞生了基于OkHttp进行封装的三方库,好比:Okhttputils
和OkGo
,它们使用起来都很是简单。 若是你喜欢注解,能够试试同一个团队出品的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官方网站。
若是你的API层写在UI中,彻底没有这个问题,但这显然不具备任何维护性和可扩展性。当咱们将API单独抽出一个层(通常是MVP的P层)的时候,数据获取和处理的代码合UI分离了,必然面临这个问题。
通常有3种处理方式:
用自定义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() //异步请求 bean?.apply{ listener.onLoginSuccess(this) } ?: listener.onError(...) } } } 复制代码
这种方式的须要每一个逻辑都要自定义一个回调,代码量巨大,且丑陋,不可取。
使用EventBus来通知UI,代码写起来想这样:
class LoginPresenter{ const EventLoginSuccess = "EventLoginSuccess" const EventLoginFail = "EventLoginFail" fun login(username: String, psw: String){ GlobalScope.launch { Bean bean = client.url("https://github.com/li-xiaojun") .header("a", "b") .params("c", "d") .<Bean>post().await() //异步请求 if(bean!=null){ EventBus.get().post(new Event(EventLoginSuccess, bean)) }else{ EventBus.get().post(new Event(EventLoginFail, null)) } } } } 复制代码
能够看到,EventBus的方式让咱们不用去定义大量的回调,换了种方式去定义大量的Event标识。当项目复杂后,可能有上百个Event标识,并不容易管理。因此这种方式不是最佳的方式。
LiveData的方式代码写起来像这样:
class LoginPresenter{ var loginData = MutableLiveData<Bean>() fun login(username: String, psw: String){ GlobalScope.launch { Bean bean = client.url("https://github.com/li-xiaojun") .header("a", "b") .params("c", "d") .<Bean>post().await() //异步请求 loginData.postValue(bean) } } } 复制代码
能够看到,LiveData的方式可让咱们避免去定义回调和Event的标识,写法上更简洁。更重要的是,LiveData自然能观察UI生命周期变化,能避免一些内存泄漏,以及在最佳时刻更新UI。
客户端主要和UI打交道,最高效的架构必定是MVVM;前端的Vue和React已经彻底证明了这一点。
Android上的MVVM主要有3种实现:
其中DataBinding须要学习一些特定语法,和前端的Vue很像,并且由于用了反射,在复杂的更新频率高的界面会有一点性能问题;不过也是很不错的一种选择。
Kotlin自然支持属性代理,咱们能够基于Kotlin的代理语法来实现UI的动态更新,不过这个须要一些精力。
我的最喜欢的是LiveData和ViewModel。
上个小节的Presenter层显示没有处理UI生命周期变化的逻辑,好比当UI结束时,Presenter是没法得知的,从而没法去释放一些资源。你能够手动去写一些代码,可是ViewModel是最佳选择,它自然能够监视UI销毁。因此换成ViewMode的代码是这样的:
class LoginViewModel : ViewModel(){ var loginData = MutableLiveData<Bean>() fun login(username: String, psw: String){ GlobalScope.launch { Bean bean = client.url("https://github.com/li-xiaojun") .header("a", "b") .params("c", "d") .<Bean>post().await() //异步请求 loginData.postValue(bean) } } //UI销毁时执行 fun onCleard(){ //释放资源的代码 } } 复制代码
综上所述,根据我我的经验得出的最佳实践是:选择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?>() fun login(username: String, psw: String){ GlobalScope.launch { val user = "https://github.com/li-xiaojun".http() .params("phone" to "188888888", "password" to "111111") .post<User>() .await() // 为null表示请求失败 loginData.postValue(user) } } //UI销毁时执行 fun onCleard(){ //释放资源的代码 } } 复制代码
UI层的代码大概是这样:
class LoginActivity: AppCompatActivity() { fun loadData(){ loginVM.loginData.observe(this, Observe { it?.apply{ updateUI(it) } ?: toast("请求出错") }) //执行登陆 loginVM.login(username, password) } } 复制代码