Posts match “ gradle ” tag:

與別人合作的 project 因為對方用的是 Eclipse (其實是只有我用 Android Studio),版本的設定會習慣放在 AndroidManifest.xml 裡面,但這麼一來如果 gradle 運行中需要使用到 versionName 或者 versionCode,就得額外設定並且注意是否與 AndroidManifest.xml 同步。這麼做除了讓專案的維護更麻煩以外,還得注意不同步的問題。

最早都是透過複雜的 script 分析 AndroidManifest.xml 把設定值取出,後來發現有個很好用的工具:com.android.builder.core.DefaultManifestParser

在你的 build.gradle 中最前面先

import com.android.builder.core.DefaultManifestParser

接著在 defaultConfig 裡面設定:

defaultConfig {
    def manifestParser = new DefaultManifestParser()

    versionName = manifestParser.getVersionName(android.sourceSets.main.manifest.srcFile)
    versionCode = manifestParser.getVersionCode(android.sourceSets.main.manifest.srcFile)
}

That’s it ! 唯一要注意的是,sourceSets 的設定要放在 defaultConfig 之前,否則 gradle 會跟你抱怨 manifest 找不到。

如果不想在 apk 裡包入某個 jar 檔 (使用 device 上的, 或者某版本沒提供某種功能) 有兩種方式: 一個是加法, 一個是減法.

加法就是透過 configuration 的設定, 在 compile 時才把把該檔案加入 class path 參考: https://gist.github.com/shakalaca/6551508

減法就是在 dex task (compile task 之後) 開始處理 jar 檔前, 把 jar 檔從 library 路徑中移除: https://gist.github.com/shakalaca/6551576

一開始最直覺的方式就是加法, 但後來發現我需要的是在某個 build 不放置 jar 檔, 才有了減法的作法, 但感覺有些醜, 再想想有沒有更好的方式..

延續之前討論的 rename APK 的問題, 那樣的寫法在 CLI 使用 gradle 時一切正常, 但如果把 project 匯入 Android Studio, 在 build 的時候會有奇怪的事情發生:

  • 如果跟我一樣在檔名安插 gitHash, commit 新的 change 之後在 Android Studio compile & run 時會抱怨檔案找不到, 此時會發現 Android Studio 要求的檔名為上一次的 gitHash 值, 但實際上新的檔案已經產生在 build/apk 目錄下.
  • 就算沒有使用動態檔名, 偶爾也會發生產生的檔名會回復成原本的名稱.

目前懷疑 Android Studio 會 cache 輸出的檔案路徑, 其實也算合理, 因為沒事除了切換 variant 檔名的確不會改變.

我自己採用折衷的方法: 把編譯好的 apk 移到另一個目錄並且更名. 一方面對 Android Studio 而言, 一切如常, 如果要調出檔案分享到指定的目錄找尋即可; 另一方面我還是可以享受編譯好檔案即更名的好處.

相關 code 可參考這裡 https://gist.github.com/shakalaca/6422811

之前都是透過 gradle.properties 分開管理 release 用的簽章,好處是不用把敏感的 information 也丟到公開的 repo,但有個問題最近困擾著我:如果存放 keystore 的路徑有中文,gradle 在 sign apk 時無法正確讀取 keystore。試過很多亂七八糟的方法,最後是透過分開的 .gradle 檔案滿足這樣的需求。

Continue Reading →

之前提到 Google Maps API (v2) 在整合至 gradle 時會碰到一些問題, 這邊提供一種可能的解法.

首先來看是怎樣的狀況, Google Maps API 在使用時得請求一組 API key, 而這組 key 得對應到一個 package name. 我自己會申請兩組, 一組給 debug 版用, 另一組給 release 版用, 這是因為 debug 版使用不同的 package name (透過 packageNameSuffix) 在測試時才能於同一支手機安裝 debug & release 版. 這樣的組合透過簡單的 overlay 應用即可輕鬆達成: 獨立出 debug (or release) 使用的 resource folder, 把 API key 放在 strings.xml 即可.

但如果你的 app 有免費版與付費版呢 ? 套用我的設定就有 4 種 package name 配 4 組 key, 理論上分 4 個 resource overlay 就可以解決. 可以問題來了, gradle 只能讓你設定 flavor 或者 buildtype 的 sourceSets, 例如 free, paid, debug, release, 但並不提供 freedebug, freerelease, paiddebug, paidrelease 的設定方式, 因此就算你把 folder 區分為前述的樣子, 或者異想天開在 sourceSets 分兩層方式設定 flavor.buildtype 的 res.srcDirs, 都無法順利分開 API key 所使用的 strings.xml.

目前看到的解法是透過修改 AndroidManifest.xml 的方式, 在 variant.processManifest.doLast 時複製一份方便修改的版本到編譯中介目錄, 然後置入正確 API key 的設定. 

我稍微研究 Android Tools Project Site 的文件, 試著找出另一種不用修改檔案的方式達到同樣的效果. 其中這一段吸引我的注意:

The following rules are used when dealing with all the sourcesets used to build a single APK:

  • All source code (src/*/java) are used together as multiple folders generating a single output.
  • Manifests are all merged together into a single manifest. This allows Product Flavors to have different components and/or permissions, similarly to Build Types.
  • All resources (Android res and assets) are used using overlay priority where the Build Type overrides the Product Flavor, which overrides the main sourceSet.
  • Each Build Variant generates its own R class (or other generated source code) from the resources. Nothing is shared between variants.

第三點提到 build type 的優先權最高, 如果我們在整合 resource 之前把 build type 的 folder 指定到我想要用的目錄呢 ? android gradle plugin 有提供 variant.mergeResources, 那麼試著在那之前把 build type 的 sourcesets 替換, 應該就行了吧 ? 花了點時間修改, 很幸運的跑起來沒有問題, 這邊提供給各位參考, 有使用 Google Maps API 且跟我有類似需求的朋友可以參考看看 :)

相關 code 可參考這 https://github.com/shakalaca/learning_gradle_android/blob/a7a37a7b94cc79fd5f6cac834935c178f50a4061/07_tricks/app/build.gradle

如果要定義額外的 resource file, 透過 project.ext.flavorname 定義, 其中 debugRes 為 debug 版的 resource, releaseRes 為 release 版的 resource, 沒有定義會自動使用 default.

又到了難纏的狀況, 跟之前 Google Maps 的狀況很類似, 不過這次需要支援的檔案為 AndroidManifest.xml

GCM 需要在 AndroidManifest.xml 中設定權限, 而這個權限又得跟著 package name 跑, 所以不同的 flavour & buildType 就會有不同的設定值. 但問題是 gradle 並不支援 flavourBuildType (比如 amazonFreeDebug) 的目錄設定, 所以即使它可以幫你整合檔案裡的設定值, 但要怎麼擺放 AndroidManifest.xml 卻是個大問題.

這邊一樣提供 hack 的方法, 首先指定 flavour 所要使用的 AndroidManifest.xml 路徑

project.ext.flavor1 = [
    debugManifest: 'src/flavor1Debug/AndroidManifest.xml',
    releaseManifest: 'src/flavor1Release/AndroidManifest.xml'
]

project.ext.flavor2 = [
    debugManifest: 'src/flavor2Debug/AndroidManifest.xml',
    releaseManifest: 'src/flavor2Release/AndroidManifest.xml'
]

接著在 variant.processManifest 時想辦法讓 gradle 吃到這些檔案, 由於這個 case 的 debug & release 都是不同的 package name, 我們可以利用 flavour 本身並不具有資源設定值的特色, 把新的 AndroidManifest.xml 偷偷地塞進 flavour 的目錄裡, 名正言順的讓 gradle 編譯該 flavour. 最後記得刪除這個偷塞的檔案:

android.applicationVariants.all { variant ->
    variant.processManifest.doFirst {
        if (project.ext.has(variant.productFlavors.name)) {
            if (project.ext[variant.productFlavors.name].debugManifest != null &&
                project.ext[variant.productFlavors.name].releaseManifest != null) {
                def manifestDirectory = android.sourceSets[variant.productFlavors.name].manifest.srcFile.parentFile
                if (variant.buildType.name.equals("debug")) {
                    copy {
                        from project.ext[variant.productFlavors.name].debugManifest
                        into manifestDirectory
                    }
                } else if (variant.buildType.name.equals("release")) {
                    copy {
                        from project.ext[variant.productFlavors.name].releaseManifest
                        into manifestDirectory
                    }
                }
            }
        }
    }

    variant.processManifest.doLast {
        if (project.ext.has(variant.productFlavors.name)) {
            project.delete android.sourceSets[variant.productFlavors.name].manifest.srcFile
        }
    }
}

至於 AndroidManifest.xml, 只要留 GCM 相關設定即可, 其他 Activity 啥鬼的都不用放進去, 由 gradle 在編譯過程中自行與 main 整合.

<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
    package="com.android.example.free.debug"
    android:versionCode="1"
    android:versionName="1.0" >

    <permission
        android:name="com.android.example.free.debug.permission.C2D_MESSAGE"
        android:protectionLevel="signature" />

    <uses-permission android:name="android.permission.INTERNET" />
    <uses-permission android:name="com.android.example.free.debug.permission.C2D_MESSAGE" />
    <uses-permission android:name="com.google.android.c2dm.permission.RECEIVE" />

</manifest>

收工, 跟平常一樣 assemble 即可, happy hacking ! 相關的 code 可以參考這 : https://github.com/shakalaca/learning_gradle_android/blob/bef7af864f1d89483df3c611d76a199815f65660/07_tricks/app/build.gradle

基本上使用 gradle wrapper 應該沒有這樣的問題, 不過由於 gradle 1.8 需要 plugin 0.6 以上, 如果 project 與其他人共用, 版本又沒統一的狀況下 (比如怕有地雷..) 可以透過判斷 gradle version 的小技巧, 讓 project 可以在 gradle 1.7 & 1.8 下都能編譯.

參考 https://gist.github.com/shakalaca/6779748 修改 project root 的 build.gradle, 在 dependencies 判斷使用的 plugin 版本即可.

搭配 gradle wrapper 使用效果更好, 如果先前用 1.7 編譯完全沒問題, 而 1.8 無法正常編譯, 修改 task wrapper 中的 gradleVersion 後, 執行一次 ./gradlew wrapper 即可退版, 等到之後 plugin 或者 gradle 有修正再切換過去.

gradle 在 zipAlign 為 true 時, 會產生兩個檔案: 一個為 unaligned 一個為 aligned (好像是廢話)

而在 android.applicationVariants 裡面, variant.packageApplication.outputFile 指向的是原始版本 (unaligned) 而 variant.outputFile 才是 zipAlign 過後的版本.

因此如果在 "保留兩種版本的檔案" 為前提的狀況下針對輸出的 apk 檔案更名, 應該要如連結中這麼做: variant.packageApplication.outputFile 一律更名, 而碰到 zipAlign 為 true 時, 額外更名 variant.outputFile.

相關 code 可參考這 https://gist.github.com/shakalaca/6414702