[譯] 當發佈安卓開源庫時我但願知道的東西

當發佈安卓開源庫時我但願知道的東西

一切要從安卓開發者開發本身的「超酷炫應用」開始提及,他們中的大多數會在這個過程當中遇到一系列問題,而他們中的一些人,會提出可能的解決方案。。javascript

事情是這樣的,若是你和我同樣認爲這個問題足夠重要,而且沒有已知的解決方案,那麼我將以模塊化的方法抽象整個解決方案,這就是一個安卓庫了。這樣之後當我再次遇到這個問題時,我就能夠很輕鬆的重用這個解決方案了了html

到目前爲止一切都好。如今你有一個庫了,也許只是拿來自用,或者你認爲別人也會遇到這個問題,而後你對外發布了這個庫(開源代碼)。我相信(更確切的說看上去是這樣)不少人認爲這就算大功告成了。前端

錯了! 這一點是大多數人一般弄錯的地方。你的安卓庫將被一些不在你身邊的開發者使用,他們只是想用你的庫來解決一樣的問題。你的庫的 API 設計的越好,它被使用的機率就越大,由於它不會讓使用者感到困惑。從一開始就應該明確的是,爲了讓他人順利地開始使用這個庫,你須要作些什麼。java

爲何會發生這種事?react

開發者在第一次發佈安卓庫的時候一般不會注意 API 的設計,至少他們中的大多數都不會。倒不是由於不聞不問,而是由於他們都只是新手,又沒有一個能夠參考的 API 設計規範。以前我也陷入了一樣的僵局,因此我能夠理解找不到相關資料的沮喪。android

我恰好作了一個開源庫(你能夠在這個地址查看)因此有一些經驗。我給出了一個對於每個 Android API 庫的開發者來講,都應該牢記的簡要列表(它們中的一部分一樣適用於通用的 API 設計)。ios

須要注意的是,個人列表並不完善。它只包含了我遇到過而且但願在一開始就明確的一些問題,當我有了新的經驗後我也會來更新這篇博客。git

在咱們正式開始以前,個全部人在構建安卓庫時都會面臨的最基本問題,那就是:github

你爲何要建立一個安卓庫?

額……後端

好吧,不管什麼時候都不是非要建立一個庫。在開始以前好好想一想它能給你帶來什麼價值。問問本身下面幾個問題:

有沒有現成的解決方案?

若是你回答是有,那麼考慮下使用已有的解決方案吧。

若是現有方案沒法完美解決你的問題,即便在這種狀況下,最好也是從 fork 代碼開始,修改它以解決你的問題。

向現有的庫中提交(Pull Request)你所作的修補,對你來講將是一個很好的加分點,同時也會讓整個社區從中受益。

若是你的回答是沒有,那麼就能夠開始編寫安卓庫了。以後與世界分享你的成果以便別人也可使用它。

你的 artifact 有哪些打包方式

在開始以前,你須要決定以什麼樣的方式向開發者發佈你的 artifact。

讓我在這裏解釋一下這篇博客中的一些概念。先解釋下 artifact

在通用軟件術語中,artifact 是在軟件開發過程當中產出的一些東西,能夠是相關文檔或者一個可執行文件。
在 Maven 術語中,artifact 是編譯的輸出,jar, war, arr 或者別的可執行文件。

讓咱們看下可選項

  • Library Project:你必須獲取代碼並連接到你的工程裏。這是最靈活的方式,你能夠修改它的代碼,但也引入了與上游更改同步的問題。
  • JAR:Java Archive 是一個專門將不少 Java 類以及元數據放到一塊兒的包文件。
  • AAR:Android Archive 相似於 JAR,但有些額外的功能。和 JAR 不一樣,AAR 能夠存儲安卓資源和 manifest 文件,這容許你分享諸如佈局和 drawable 等資源文件。

咱們有了 artifact 了,而後呢?這些 artifact 應該放在哪裏呢?

開玩笑……

你有好幾種選擇,每種都有優缺點。讓咱們一個一個看。

本地 ARR

若是你不想將你的庫提交到任何倉庫裏,你能夠產生一個 arr 文件並直接使用它。閱讀 StackOverflow 上的一個回答學習如何實現。

簡單來講,將 arr 文件放到 libs 文件夾裏(沒有就建立),而後在 build.gradle 中添加以下代碼:

dependencies {
   compile(name:'nameOfYourAARFileWithoutExtension', ext:'aar')
 }
repositories{
      flatDir{
              dirs 'libs'
       }
 }複製代碼

隨之而來的就是不管什麼時候你想要分享你的安卓庫時你都繞不過你的 arr 文件了(這可不是分享你的安卓庫的好方式)。

儘量的避免這麼作,由於它容易引起不少問題,尤爲是代碼庫的可管理性和可維護性。
另外一個問題是這種方式沒辦法保證你的用戶使用的代碼是最新的。
更不用說整個過程漫長並且容易出現人爲錯誤,而咱們僅僅是往項目中添加一個庫。

本地/遠程 Maven 倉庫

若是你只想給本身用這個安卓庫該怎麼作? 解決辦法是部署一個本身的 artifact 倉庫(在這裏瞭解如何去作)或者使用 GitHub 或者 Bitbucket 做爲你本身的 maven 庫(在這裏)。

再次強調,這只是用來發布自用包的方法。若是你想要與他人分享,那這不是你須要的方式

這種方式的第一個問題是你的 artifact 是存放在私有倉庫裏的,爲了讓別人訪問到你的庫(library)你不得不給他們訪問整個倉庫(repository)的權限,這可能會致使安全問題。

第二個問題是別人要想用你的庫就得在他的 build.gradle 文件里加上額外的語句。

allprojects {
    repositories {
        ...
        maven { url '
        http://url.to_your_hosted_artifactory_instance.maven_repository' }
    }
}複製代碼

說實話這樣比較麻煩,而咱們都但願事情簡單一點。這種方式在發佈安卓庫的時候比較迅速可是爲別人的使用增長了額外步驟。

Maven Central, Jcenter 或 JitPack

如今最簡單的發佈方式是經過 JitPack,你可能會想去試試。JitPack 從你的公開 git 倉庫中拉取代碼,check out 最新的 release 代碼,編譯並生成 artifact,最後將它發佈到它本身的 maven 庫中。

可是它和 local/remote 倉庫存在一樣的問題,要使用的話必須在根 build.gradle 中添加額外內容。

allprojects {
    repositories {
        ...
        maven { url 'https://www.jitpack.io' }
    }
}複製代碼

你能夠從這兒瞭解該如何發佈你的安卓庫至 JitPack。

另外一個選擇就是 Maven Central 或者 Jcenter

我我的建議你使用 Jcenter,由於它有着完善的文檔和良好的管理,同時它也是安卓項目的默認倉庫(除非誰改了默認選項)。

若是你發佈到 Jcenter,bintray 公司提供將庫同步到 Maven Central 的選項。一旦成功發佈到 Jcenter 上,在 build.gradle 中加上以下代碼就能夠很方便的使用了。

dependencies {
      compile 'com.github.nisrulz:awesomelib:1.0'
  }複製代碼

你能夠在這兒瞭解如何發佈你的安卓庫至 Jcenter。

基礎的東西說完了,如今讓咱們來討論一下在編寫安卓庫的時候須要注意的問題。

避免多參數

每一個安卓庫一般都須要用一些參數來進行初始化,爲了達到這個目的,你可能會在構造函數或者新建一個 init 方法來接受這些參數。這麼作的時候請考慮如下問題

向 init() 方法傳遞超過 2-3 個參數會讓使用者感到頭大。 由於很難記住每一個參數的用處和順序,這也爲將 int 型數據傳給了 String 類型的參數之類的錯誤埋下了隱患。

// 不要這麼作
void init(String apikey, int refresh, long interval, String type);

// 這樣作
void init(ApiSecret apisecret);複製代碼

ApiSecret 是一個實體類,定義以下

public class ApiSecret {
    String apikey;
    int refresh;
    long interval;
    String type;

    // constructor

    /* you can define proper checks(such as type safety) and * conditions to validate data before it gets set */

    // setter and getters
}複製代碼

或者你可使用 建造者模式

你能夠閱讀這篇文章以瞭解更多建造者模式的知識。JOSE LUIS ORDIALES這篇文章裏深刻討論了該如何在你的代碼中實現建造者模式。

易用性

當構建你的安卓庫時,請關注庫的易用性和暴露出的方法,它們應該具備如下特色:

  • 符合直觀

安卓庫中的代碼作了些什麼都應該以某種形式反饋給使用者,能夠是日誌輸出,也能夠是視圖的變化,這根據庫的類型來決定。若是它作了一些難以理解的事,那麼對開發者來講這個庫就沒有起做用。你的代碼應該按照使用者想的那樣來工做,即便使用者沒有查看文檔。

  • 一致性

代碼應該易於理解,同時避免在版本迭代的過程當中發生劇烈的變化。遵循 sematic versioning

  • 易於使用,難以誤用

就實現與首次使用而言,它應該是易於理解的。暴露給用戶的方法應該通過充分的檢查以保證用戶只會用它幹它應該作的事情,避免方法被用戶錯誤使用。在某些須要用到的東西不存在的時候,提供合理的默認設置和處理方案。公開的方法應該通過充分的檢查以保證用戶不會。

簡而言之

簡單。

最小化權限

在每一個開發者都在向用戶申請不少的權限時,你得停下來想想你是否是真的須要這些額外的權限。這一點尤爲須要注意。

  • 儘量的請求更少的權限。
  • 使用 Intent 讓專用程序爲你工做並返回結果。
  • 基於你得到的權限啓用你的功能。避免由於權限不足致使的崩潰。能夠的話,在請求權限以前先讓用戶知道你爲何須要這些權限。儘可能在沒有得到權限的時候進行功能回退。

經過以下方式檢查是否具備某個權限。

public boolean hasPermission(Context context, String permission) {
  int result = context.checkCallingOrSelfPermission(permission);
  return result == PackageManager.PERMISSION_GRANTED;
}複製代碼

有些開發者可能會說他是真的須要某個特定權限,在這種狀況下該怎麼辦呢?庫代碼應該對全部須要這個功能的應用是通用的。若是你須要某個危險權限來獲取某些數據,而這些數據是庫的使用者能夠提供的,那麼你就應該提供一個方法來接收這些數據。這種時候你就不該該強迫開發者去申請他不想申請的權限了。當沒有權限時,提供功能回退(沒法達到可是儘可能接近預期效果)的實現。

/* Requiring GET_ACCOUNTS permission (as a requisite to use the * library) is avoided here by providing a function which lets the * devs to get it on their own and feed it to a function in the * library. */

MyAwesomeLibrary.getEmail("username@emailprovider.com");複製代碼

最小化條件

如今,咱們有一個功能須要設備具備某種特性。一般咱們會在 manifest 文件中進行以下定義

<uses-feature android:name="android.hardware.bluetooth" />複製代碼

當你在安卓庫代碼中這麼寫的時候問題就來了,它會在構建的過程當中與應用的 manifest 文件合併,並致使那些沒有藍牙功能的設備沒法從 Play 商店中下載它。這樣會致使以前對大部分用戶可見的 app 此時卻僅僅對一部分用戶可見,就只是由於引用了你的庫。

這可不是咱們想要的。因此咱們得解決它。不要在 manifest 文件中寫 uses-feature,在運行時檢查是否有這個功能

String feature = PackageManager.FEATURE_BLUETOOTH;
public boolean isFeatureAvailable(Context context, String feature) {
 return context.getPackageManager().hasSystemFeature(feature);
}複製代碼

這種方式就不會引發 Play 商店的過濾。

做爲一個額外功能提供是當這個功能不可用時在庫代碼中不去調用相關方法或者使用替代的回調方法。這對於庫的開發者和使用者來講是一種共贏的局面。

多版本支持

如今到底有多少種版本?

若是你的庫中存在只能在特定版本中運行的代碼,你應該在低版本的設備中禁用這些代碼。

通常的作法是經過定義 minSdkVersiontargetSdkVersion 來指定支持版本。你應在在代碼中檢查版本,來決定是否啓動某個功能,或者提供回退。

// Method to check if the Android Version on device is greater than or equal to Marshmallow.
public boolean isMarshmallow(){
    return Build.VERSION.SDK_INT>= Build.VERSION_CODES.M;
}複製代碼

不要在正式版中輸出日誌

就是不要這麼作。

幾乎每次被要求去測試一個應用或者 Android Library 工程時我都會發現他們把全部在日誌裏輸出了全部東西,這但是發佈版啊。(譯註:在正式版中打印日誌是沒必要要的,可能影響性能,還可能帶來安全問題)

根據經驗,永遠不要在正式版中輸出日誌。你應該配合使用 build-variantstimber 來實現發佈版和調試版中的不一樣日誌輸出。一個更簡單的解決方案是提供一個 debuggable 標誌位來讓開發者設置以開關安卓庫中的日誌輸出。

// In code
boolean debuggable = false;
MyAwesomeLibrary.init(apisecret,debuggable);

// In build.gradle
debuggable = true複製代碼

發生錯誤的時候讓使用者知道

常常有開發者不在日誌裏輸出錯誤和異常信息,我遇到過不少次這種狀況。這讓安卓庫的使用者在調試的過程當中感到十分的頭疼。雖然上面說了不要在發佈版中輸出日誌,可是你得理解不管是在發佈版仍是調試版中錯誤和異常信息都須要輸出。若是你真的不肯意在發佈版中輸出,至少在初始化的時候提供一個方法來讓使用者啓用日誌。

void init(ApiSecret apisecret,boolean debuggable){
      ...
      try{
        ...
      }catch(Exception ex){
        if(debuggable){
          // This is printed only when debuggable is true
          ex.printStackTrace();
        }
      }
      ....
}複製代碼

當你的安卓庫崩潰的時候要馬上向用戶顯示異常,而不是掛起並作一些處理。避免寫一些會阻塞主進程的代碼。

當發生錯誤時及時退出並禁用功能

個人意思是當你的代碼掛掉後,嘗試進行檢查和處理,從而使這些有問題的代碼僅僅會致使你提供的庫中的一些功能被禁用而不是讓整個APP崩潰。

捕獲特定的異常

接上一條建議,你能夠看到上面那段代碼裏我使用了 try-catch 語句。Catch 語句只是簡單的捕獲了全部的 Exception 。一個異常與另外一個異常之間並無什麼太大的區別。所以,必需要根據手頭的需求捕獲特定類型的異常。好比:NULLPointerException, SocketTimeoutException, IOException 等等。

對網絡情況差的狀況進行處理

這很重要,嚴肅點!

若是你的安卓庫須要進行網絡請求,一個很容易忽視的狀況就是網速較慢或者請求無相應。

據我觀察,開發者總會假設網絡暢通。舉個例子吧,你的安卓庫須要從服務器上獲取配置文件來進行初始化。若是你忽略了在網絡狀態差的時候無法下載配置文件,那麼你的代碼就可能由於獲取不了配置文件而崩潰。若是你進行了網絡狀態檢查並進行處理,那麼就能爲你的庫的使用者省不少事。

儘量的批量處理你的網絡請求,避免屢次請求。這可以節省不少電量,再看下這個

經過將 JSONXML 轉成 Flatbuffers 來節省數據傳輸量。

閱讀更多有關網絡管理的知識

避免將大型庫做爲依賴

這一點不須要太多的解釋。就像安卓開發者都知道的那樣,一個安卓應用最多隻能有 65k 方法。若是你依賴了一個大型的庫,那麼會對使用你的庫的應用帶來兩個不指望的影響。

  1. 你會讓應用的方法數將會大大增長,即便你的庫只有不多一些方法,可是你依賴的庫中的方法也被算上了。
  2. 若是由於引入你的庫而致使方法數達到了 65k,那麼應用開發者不得不去使用 multi-dex。相信我,沒人想用 multi-dex 的。
    在這種狀況下,爲了解決一個問題你引入了一個更大的問題,你的庫的使用者將會轉而去使用別的庫。

避免引用不是必需的庫

我以爲這應該時一條你們都知道的規則了,是否是?不要讓你的安卓庫由於引入了不須要的庫而膨脹。可是須要注意的是即便你須要依賴,讓你的用戶傳遞性地下載這些依賴(由於用了你的庫而不得不去下載另外一個庫)。好比,那些沒有和你的庫綁定的依賴。
那麼如今的問題就是若是沒有和咱們的庫綁定那麼咱們如何去使用它?

答案很簡單,要求用戶在編譯的時候提供你須要的依賴。可能不是每一個用戶都須要這個依賴提供的方法,對於這些用戶來講,若是你找不到這些依賴,你只須要禁用某些方法就好了。對於那些須要的用戶,它們會在 build.gradle 提供依賴。

如何實現它? 檢查 classpath

private boolean hasOKHttpOnClasspath() {
   try {
       Class.forName("com.squareup.okhttp3.OkHttpClient");
       return true;
   } catch (ClassNotFoundException ex) {
       ex.printStackTrace();
   }
   return false;
}複製代碼

接下來,你可使用 provided(Gradle v2.12 或更低)或者 compileOnly(Gradle v2.12+)(閱讀完整內容),以便在編譯時獲取依賴庫內定義的類。

dependencies {
   // for gradle version 2.12 and below
   provided 'com.squareup.okhttp3:okhttp:3.6.0'

   // or for gradle version 2.12+
   compileOnly 'com.squareup.okhttp3:okhttp:3.6.0'

}複製代碼

還有要注意的是,只有當依賴是單純的 Java 依賴的時候你才能使用這種控制依賴的方法。好比,若是你在編譯時引入安卓庫,你就無法引用它的依賴庫或者資源文件,這些都必須在編譯前被加入。只有依賴是一個純 Java 依賴(僅僅由 Java 類組成)時,才能夠經過在編譯的過程當中加入 ClassPath 來使用。

不要阻塞啓動過程

沒開玩笑

我指的不要應用一啓動就馬上初始化你的安卓庫。這麼作會下降應用的啓動速度,即便應用什麼都沒作就只是初始化了你的庫。

解決辦法是不要在主線程裏進行初始化工做,能夠新建一個線程,更好的辦法是使用 Executors.newSingleThreadExecutor() 讓線程數量保持惟一。

另外一個解決辦法是根據須要初始化你的安卓庫,好比只有在使用到的時候加載/初始化它們。

優雅地移除方法和功能

不要在版本迭代的過程當中移除 public 方法,這會致使使用你的庫的應用沒法使用,而開發者並不知道什麼致使了這個問題。

解決方案:使用 @Deprecated 來標註方法並給出在將來版本的棄用計劃。

使你的代碼可測試

肯定你的代碼裏有測試實例,這不是一個規則,而是一個常識,你應該在你的每個應用和庫中這麼作。

使用 Mock 來測試你的代碼,避免 final 類,不要有靜態方法等等。

基於接口編寫你的 public API 使你的安卓庫能交換實現,反過來讓你的代碼可測試,好比,在測試的時候,你能夠很容易地提供 mock 實現。

爲每個東西編寫文檔

做爲安卓庫的建立者你很瞭解你的代碼,可是使用者不會很瞭解,除非你讓他們去閱讀你的代碼(而你永遠也不該該這麼作)。

編寫文檔,包括使用時的每一個細節,你實現的每一個功能。

  1. 建立一個 Readme.md 文件並將其放在庫的根目錄下。
  2. 爲代碼裏全部 publicjavadoc註釋。它們應該包括
  • public 方法的目的
  • 傳入的參數
  • 返回的數據
  1. 提供一個示例應用來演示這個庫的功能以及如何使用。
  2. 肯定你有一個詳細的修改日誌。放在 release 記錄裏的特殊的版本 tag 裏都比較合適。

GitHub 裏 Sensey 庫的 Release 部分截圖

這是 Senseyrelease 連接

提供一個極簡的示例應用

這都不用說了。始終提供一個最簡潔的示例程序,這是開發者在學習使用你的庫的過程當中接觸的第一個東西。它越簡單就越好理解。讓這個程序看起來花哨或者把示例代碼寫得很複雜只會背離它最初的目的,它只是一個如何使用庫的例子。

考慮加一個 License

不少時候開發者都忘了 License 這部分。這是別人決定要不要採納你的庫的一個因素。

若是你決定使用一種帶限制的協議,好比 GRL,這意味着不管誰只要修改了你的代碼那他必需要將修改提交到你的代碼庫中。這樣的限制阻礙了安卓庫的使用,開發者傾向於避免使用這樣的代碼庫。

解決辦法是使用諸如 MIT 或者 Apache 2 這樣更爲開放的協議。

在這個簡單的網站閱讀有關協議的知識,以及關於你的代碼須要的 copyright

最後,獲取反饋

是的,你聽到了!

起初,你的安卓庫是用來知足本身的需求的。一旦你發佈出去讓別人用,你將會發現大量的問題。從你的庫的使用者那裏聽取意見收集反饋。基於這些意見在保持原有目的不變的狀況下考慮增長新的功能和修復一些問題。

總結

簡而言之,你須要在編碼過程當中注意如下幾點

  • 避免多參數
  • 易用
  • 最小化權限
  • 最小化前置條件
  • 多版本支持
  • 不要在發佈版中打印日誌
  • 在崩潰的時候給使用者反饋
  • 當發生錯誤時及時退出並禁用功能
  • 捕獲特定異常
  • 處理網絡不良的狀況
  • 避免依賴大型庫
  • 除非特別須要,不要引入依賴
  • 避免阻塞啓動過程
  • 優雅地移除功能和特性
  • 讓代碼可測試
  • 完善的文檔
  • 提供極簡的示例應用
  • 考慮加個協議
  • 獲取反饋

根據經驗,你的庫應該依照 SPOIL 原則

簡單(Simple)—— 簡潔而清晰的表達

目的(Purposeful)—— 解決問題

開源(OpenSource)—— 自由訪問,免費協議

習慣(Idiamatic)—— 符合正常使用習慣

邏輯(Logical) —— 清晰有理

我在曾經某個時候從某位做者的演示裏看到這個,但我想不起來他是誰了。由於它頗有意義並以很簡潔的方式提供了圖片因此當時我記了筆記。若是你知道他是誰,在下面評論,我會將他的連接加上。

最後的思考

我但願這篇博客給那些正在開發更好的安卓庫的開發者們帶來幫助。安卓社區從開發者天天發佈的庫中得到了很大的益處。若是每一個人都開始注意他們 API 設計,學會爲用戶(其餘的安卓開發者)考慮,咱們將會迎來一個更好的生態。

這個教程是基於我開發安卓庫的經驗。我很想知道你關於這些觀點的意見。歡迎留下評論。

若是你有什麼建議或者想讓我加一些內容,請讓我知道。

Till then keep crushing code 🤓

掘金翻譯計劃 是一個翻譯優質互聯網技術文章的社區,文章來源爲 掘金 上的英文分享文章。內容覆蓋 AndroidiOSReact前端後端產品設計 等領域,想要查看更多優質譯文請持續關注 掘金翻譯計劃

相關文章
相關標籤/搜索