Android開發中API層的最佳實踐

前言

API層就是網絡層,是一個App必不可少的模塊。我從12年開始作安卓開發,從這些年的開發經驗中對API層的實踐進行一些總結,內容方面主要是圍繞HttpClient的選擇,響應處理的編程模型和通知UI數據更新的最佳方式。前端

如下內容僅僅是我的觀點,與實際內容若有出入,煩請指出;若噴,請輕點。android

SDK中的Http Client

標題中的Http Client是一個泛指,可能與某個http請求庫重名,它泛指全部的http請求客戶端。git

SDK中的client有2個:HttpURLConnection和Apache的HttpClient庫。github

在最先的時候(大概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() //異步請求
            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。

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>()
    
    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)
    }
}

複製代碼

最後給你們分享一份很是系統和全面的Android進階技術大綱及進階資料,及面試題集

想學習更多Android知識,請加入Android技術開發交流 7520 16839

進羣與大牛們一塊兒討論,還可獲取Android高級架構資料、源碼、筆記、視頻

包括 高級UI、Gradle、RxJava、小程序、Hybrid、移動架構、React Native、性能優化等全面的Android高級實踐技術講解性能優化架構思惟導圖,和BATJ面試題及答案!

羣裏免費分享給有須要的朋友,但願可以幫助一些在這個行業發展迷茫的,或者想系統深刻提高以及困於瓶頸的朋友,在網上博客論壇等地方少花些時間找資料,把有限的時間,真正花在學習上,因此我在這免費分享一些架構資料及給你們。但願在這些資料中都有你須要的內容。

Android高級技術大綱,以及系統進階視頻,及面試題和答案 面試題及答案 Android高級技術大綱 Android 進階視頻資料

相關文章
相關標籤/搜索