Android 開發中 API 層的最佳實踐

2021-02-15 掘金開發者社區
前言

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-http和 xutil。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火?

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

         }

     });

回調的模型在代碼複雜的時候回陷入 CallbackHell的問題,當然你可以用抽取方法來重構,也可以用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地址是:https://github.com/li-xiaojun/AndroidKTX

所以,貼下我項目中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)

   }

}

相關焦點

  • API設計最佳實踐
    API 可以讓外部應用訪問您的資源API 擴展了應用程式的功能API 允許開發者重用應用邏輯API 是獨立於平臺的,它們傳遞數據不受請求平臺的影響在大多數實際場景中,數據模型 已經存在,但由於我們將討論 API 設計最佳實踐,我將從頭開始說起。
  • Android組件化開發思想與實踐
    實際開發中一般是模塊化與組件化相結合的方式。(1)提高復用性避免重複造輪子,不同的項目可以共用同一組件,提高開發效率,降低維護成本。(2)項目按功能拆分成組件,組件之間做到低耦合、高內聚,有利於代碼維護,某個組件需要改動,不會影響到其他組件。
  • 系統設計:API 接口的最佳實踐
    在大多數實際場景中,數據模型 已經存在,但由於我們將討論 API 設計最佳實踐,我將從頭開始說起。https://api.domain.com/authors https://api.domain.com/authors/{id}/books這有助於新的開發人員快速了解你的 API 是什麼,以及如何遍歷你的數據模型。
  • Android 存儲空間的最佳實踐 (上)
    分區存儲改變了應用在外置存儲中保存和訪問文件的方式,為了幫您遷移應用並支持分區存儲,我們概括了常見用例的最佳實踐並分享給大家。本文分為上下兩篇,分別為您介紹處理媒體文件和非媒體文件的用例和最佳實踐,供您參考。這部分內容描述了處理媒體文件 (如視頻、圖片、音頻文件) 的一些常見用例,並概要說明了應用可以使用的方法。
  • Android開發最佳實踐
    gt;</LinearLayout>作為一個經驗法則,android:layout_****屬性應該在 layout XML 中定義,同時其它屬性android:**** 應放在 styler XML中。
  • Android 開發最佳實踐
    ><LinearLayout xmlns:android="http://schemas.android.com/apk/res/android" xmlns:tools="http://schemas.android.com/tools" android:layout_width="match_parent" android:layout_height="match_parent
  • 一文詳解 API 設計最佳實踐
    在大多數實際場景中,數據模型 已經存在,但由於我們將討論 API 設計最佳實踐,我將從頭開始說起。https://api.domain.com/authors https://api.domain.com/authors/{id}/books這有助於新的開發人員快速了解你的 API 是什麼,以及如何遍歷你的數據模型。
  • Android 存儲空間的最佳實踐 (下)
    分區存儲改變了應用在外置存儲中保存和訪問文件的方式,為了幫您遷移應用並支持分區存儲,我們概括了常見用例的最佳實踐並分享給大家。在我們過去的文章推送裡已經向您介紹了處理媒體類文件的常見用例和最佳實踐,本篇將繼續帶您了解處理非媒體文件的用例和最佳實踐,供您參考。這部分內容描述了處理非媒體文件的一些常見用例,並概要說明了應用可以使用的方法。
  • ASP.NET Core Web API 最佳實踐指南
    在這一份指南中,關於開發 .NET Core Web API 項目,我們將敘述一些我們認為會是最佳實踐的方式。進而讓我們的項目變得更好和更加具有可維護性。現在,讓我們開始想一些可以應用到 ASP.NET Web API 項目中的一些最佳實踐。
  • Android 遊戲開發工具包熱門問題解答
    Android Studio 性能剖析器https://developer.android.google.cn/studio/profile/android-profilerAndroid GPU Inspectorhttps://developer.android.google.cn/agiAndroid Performance Tuner
  • RESTful API 最佳實踐
    想要更好的理解RESTful API首先需要理解如下概念:REST:REST(Representational State Transfer)這個詞,是Roy Thomas Fielding在他2000年的博士論文中提出的,翻譯成中文大意為表現層狀態傳輸。
  • Android夜間模式最佳實踐
    <LinearLayout xmlns:android="http://schemas.android.com/apk/res/android" android="@+id/main_screen" android:layout_width="match_parent" android:layout_height="match_parent" android
  • Android開發:最詳細的 Toolbar 開發實踐總結
    【回復「1024」,送你一個特別推送】最詳細的 Toolbar 開發實踐總結
  • Android 路由框架ARouter最佳實踐
    隨著業務量的增長,客戶端必然隨之膨脹,開發人員的工作量越來越大,比如64K問題,比如協作開發問題。App一般都會走向組件化、插件化的道路,而組件化、插件化的前提就是解耦,那麼我們首先要做的就是解耦頁面之間的依賴關係。簡化代碼。數行跳轉代碼精簡成一行代碼。其他...
  • Android 開發最佳實踐,讓你少走彎路
    不再推薦使用Eclipse和ADT開發,因為谷歌在2015年年末結束了對ADT的支持https://android-developers.googleblog.com/2015/06/an-update-on-eclipse-android-developer.html,並呼籲開發者儘快遷移到Android Studio。
  • API設計與開發之最佳實踐
    導讀:本文來詳解REST API設計中參數和查詢字符串使用的最佳實踐。
  • 實際生產中的 Android Lint實踐分享
    擴展性強,支持開發自定義Lint規則。配套工具完善,Android Studio、Android Gradle插件原生支持Lint工具。Lint專為Android設計,原生提供了幾百個實用的Android相關檢查規則。有Google官方的支持,會和Android開發工具一起升級完善。
  • Android TV開發
    開始創建TV電視應用程式的開發與手機和平板電腦具有相同的結構,這就意味著你可以稍微修改你現在已經開發好的程序,然後運行在電視上面,或者根據你已經掌握的android開發知識去創建開發TV應用如果你已經開發有一個android的應用,你可以使用現有的程序架構通過添加對android TV的支持並重新設計一個用戶界面。
  • 簡訊驗證碼最佳實踐
    對於簡訊驗證碼,前陣子,看到騷窩洞見分享了一篇簡訊驗證碼的文章(https://insights.thoughtworks.cn/sms-authentication-login-api/),感覺可以作為一個最佳實踐了,老早就決定按照文中觀點實踐了,奈何那陣一直996,沒時間,直到最近,才忙裡偷閒動手整理。
  • Flutter Boost 混合開發實踐與源碼解析( Android )
    簡介Flutter Boost 是閒魚團隊開發的一個 Flutter 混合開發框架,項目背景可以看看閒魚的這篇文章:碼上用它開始Flutter混合開發——FlutterBoost。文章中主要講述了多引擎存在一些實際問題,所以閒魚目前採用的混合方案是共享同一個引擎的方案。