自動產生 VersionCode 與 VersionName

透過 gradle,改良一個更好的版本訊息更新流程

Ray Yuan Liou
8 min readNov 3, 2018
「太棒了,終於可以撐過 2021 年。」 圖片來自: imgflip

編譯 Android 程式時會在 gradle build script 裡頭決定該打包好的 apk 版本相關訊息,其實就是 versionCodeversionName
versionName 是顯示給使用者看的版本號,像是 1.0、1.1 之類的經典組合;而 versionCode 是一組數字,Android 系統會透過 versionCode 的大小來判斷哪一個版本是比較新的,並且會阻止使用者降級安裝比較舊的版本。另外,Google Play 商店也是透過 versionCode 來判斷新上傳的 apk 是不是新版。

上次分享的文章:在 build APK 省去繁文縟節,透過 signingConfig 的設定可以省去一些手動設定 keyStore 位置、Alias Name 和輸入密碼等繁雜的設置時間,直接透過 build APK 指令無痛編譯。雖然看起來似乎不錯,但是應該還有很多可以調整個空間。例如每次編譯時,都需要重新更新過 versionNameversionCode 。而且,如果有測試的階段還要為測試版加入特殊的字串,例如 beta release 會習慣在 versionName 後頭加上特殊的字串用來辨識。還需要在每次發布新版時,更新 versionCode裡頭的數字。

本篇來分享幾個我曾經使用過的做法,讓編譯的過程之中自動產生 versionCode。總過嘗試過兩種方式,一種是直接產生 timestamp,另一個則稍微把玩了 gradle —— 由 gradle 的變數編輯 versionName ,解決 timeStamp 法遇到的問題。

先從產生 timeStamp 的方式來介紹:

這個方法為:每次 Build 時產生一組新的 versionCode ,由編譯的時間點生成 TimeStamp。這種做法的優點是快速、方便,設定一次後就不用再理 versionCode調整,可以說是設定好後就可以不理它。

不過缺點也是滿明顯的,不能透過 versionCode 反推出是哪一個版本。 versionName 還是需要手動輸入,但真正的問題是 Google Play 商店對 versionCode限制。Google Play 商店對 versionCode 有最大值 2100000000 ,超過這個數字便沒有辦法繼續更新架上的 App。
這表示 TimeStamp 的 Pattern 不能設定為 yyMMddHHmm ,也就是年月日時分。因為只要這個 app 到 2021 年就沒辦法繼續更新下去。(還是這已經是太奢侈的煩惱? 😫)

後來才注意到這點的我,只能在 timeStamp 前加上 19 這個 placeholder,防止 2021 年 versionCode進展到無法更新的地步。
如此一來就犧牲了 timeStamp 後面的 mm (分鐘),即同一個小時內編譯的 apk 都會是同一組 versionCode ,需要特別注意。

本範例將 groovy 程式碼寫在其他的 gradle 檔案,可以在 build.gradle 引入外部的 gradle 檔案來使用裡頭的方法。

去掉了 mm 的版本

我將這個 script 取名為 build-script.gradle ,放在根目錄的 gradle 資料夾裡頭。在 build.gradle 加入引入外部 gradle 來使用外部 gradle 寫好的方法。

apply from: "${rootDir}/gradle/build-script.gradle"

這樣只要在 defaultConfig 的 versionCode設定使用 getVersionCodeTimeStamps() 這個方法即可。

先撇除一個小時內 versionCode 都會一樣的問題, versionName 還是需要自己輸入。不同的測試階段還需要不忘加上 beta 等測試版前綴詞,我的工作夥伴 TIng-Han Suender503)想到另一個直覺的作法,解決 timeStamp 法遇到的狀況:

  • 自動產生 versionName
  • 反推程式版本 (這個算解決一半)
  • 避免 2021 年更新末日

不過這個作法會需要一些手動,沒有 timeStamp 產生大法這麼的快狠爽。

新的作法如返璞歸真,簡單好理解。
僅藉由將 versionName之中的 <major>.<minor>.<patch> 數值分別獨立出來變成變數,編譯 apk 時透過 gradle 程式碼去代組 versionCodeversionName

首先,在專案根目錄的 build.gradle 之中增加幾個變數:

同樣,新增兩個方法分別處理 versionCodeversionName 的自動生成。這裡用的方法借鑒至 Jake Wharton 的 Decomposed Version Name and Code 作法,依據我們的情境做了一些調整。

放入 defaultConfig 的 versionCodeversionName 的設定之中:

對比 Jake Wharton 的版本

def versionMajor = 2
def versionMinor = 8
def versionPatch = 0
// bump for dogfood builds, public betas, etc.
def versionBuild = 0

android {
defaultConfig {
versionCode versionMajor * 10000 + versionMinor * 1000 + versionPatch * 100 + versionBuild
versionName "${versionMajor}.${versionMinor}.${versionPatch}"
}
}

Jake Wharton 的版本在編譯 Pre-production 版的 app 時,會將 versionBuild 加到 versionCode 裡頭。
假設程式版本 2.8.0 進入到測試階段,編譯 2.8.0 beta 1 , versionCode 將會是 28001。經過了顛簸與坡折,最後的 beta 版本是 beta 7,也就是 versionCode 為 28007。最後,釋出 2.8.0 正式版, versionCode 會是 28000 顯然會造成 Android 安裝與 Google Play 檢查都沒辦法通過的窘境。(因為 versionCode 較小不被視為一個新版)

透過 TIng-Han Suender503)的方法就不會有這個問題。
同樣以 2.8.0 版本的開發為例,2.8.0 beta 1 versionCode 將會是 2079901,最終的 2.8.0 beta 7 versionCode 會是 2079907。測試階段完成,發布 2.8.0 正式版, versionCode 為 2080000,可以正常安裝升級!

但是我們的用法在 minor、patch 和 beta 數字超過 100 時會溢位,所以 minor、patch 和 beta 最大只能設定為 99。(Jake Wharton 的則是 10,他的理由如下所示。)

If you have more than 10 patch or minor versions you have other problems.
— Jake Wharton

你可以在這個 Kotlin Playground 自由操作驗算。 😆

因為這幾個版本號碼都化為變數,在產生 versionName 時就可以直接透過 beta_number 這個變數大於 0 來判斷是否進入 beta pre-production 階段。在編譯 beta 版時,就能直接將我們預先設定好的 postfix_name 加到 versionName 之中。

但缺點也很明顯:無法從 versionCode 來反推 pre-production 的版本號。只能反推 production 版本的 versionCode。例如上述的 2.8.0 versionCode 為 2080000。推測接下來的幾個正式版 release, versionCode 可能為:

2.8.0, versionCode is 2080000
2.8.1, versionCode is 2080100
2.8.2, versionCode is 2080200
2.9.0, versionCode is 2090000
2.9.10, versionCode is 2091000

如上所示,可以反推回去 versionName
所以如果你的情境與我們類似,只關注 production app 的 versionCode ,也許這個作法才會比較適合。

而且,因為這個作法只有 7 碼的數字,不會有 TimeStamp 直出會遇到的2100000000 極限。

這樣設定後,推薦於每次 bump version commit 時調整這幾個變數,就完成 versionCodeversionName 更新。

--

--