Android
AndroidStudio

Android Studio で Build Variants

More than 1 year has passed since last update.

Eclipse の場合

Eclipse + ADT で開発していると /gen の下に R.java の他に BuildConfig.java というのが自動生成される。そこにはこんな事が書かれている:

public final class BuildConfig {
    // Eclipse からデバッグ実行した場合 true が入り APK を明示的に生成した場合は false が入る
    public final static boolean DEBUG = true;
}

なので Java ソース側でこの値を見てデバッグ時と本番時の処理を切り替えることができるように見える。だが当然 XML やその他リソースの方でこの値を参照することができない。あと、業務で実装していて以下の様な要件があったとする:

今度 OEM 販売することになったからガワとアプリ名だけ替えて配布してよ。

この要件だけ聞いて愚直にやろうとすると、「/res の値を適宜書き換えて、あと ADT のリファクタリングでアプリのパッケージ名を替えて配布」などという事になったり、それぞれのアプリごとに /res 下のリソースを用意して適宜差し替える、のような対応になると思う。後これが要件によって Java ソースの方で処理を出し分けなければならなくなった場合や、Google Maps API Key や ContentProvider が絡んできたりすると頭を抱えることになる。OEM 毎の切り替えに enum 定数を用意して切り替えるような事になると思うが、やはり XML 側は手動で書き換えることになり、しかも証明書がどちらを使うかに気を使わなければならなくなるからだ。

しかも、それがアプリのバージョンアップの度に全アプリのバージョンアップ用 APK を作成しないといけなくなったら?……考えるだけで胃が痛くなる。 とかく人間がこの手の作業をすると必ずミスが発生する。自動化する事には作業量以上の効果がある。

Build Variants とは

例えば「アルファ版」「ベータ版」「リリース版」を「alpha」「beta」「release」で定義するのが Build Type であり、「アプリ hoge」「OEM アプリ fuga」で定義するのが Product Flavor だとすると、この Build Type と Product Flavor を組み合わせたものを Build Variant という。

簡単にいえば上記のような要件が来た時に ソースコードやリソースを一切コピペや書き換えることなくそれぞれの APK を作成する事が可能な仕組み である。 Gradle の仕組みなのでコードを書けばいろいろ出来るはずなのだが、とりあえず以下の様な事が簡単にできる:

  • Build Variant によって署名する証明書を自動で分ける
  • Build Variant によって自動で参照する /res を変えるか差分のみマージする

Eclipse + ADT の XML 側が定数で切り替えられない問題を解消し、しかも切り替えは Android Studio のリストボックスから選択するだけである。

定義方法

モジュールの build.gradle は初期状態で以下のようになっている。Groovy が分からなくても見ると何となくどういう事なのか分かると思う:

apply plugin: 'android'

android {
    compileSdkVersion 19
    buildToolsVersion '19.0.3'

    defaultConfig {
        minSdkVersion 19
        targetSdkVersion 19
        versionCode 1
        versionName "1.0"
    }

    compileOptions {
        sourceCompatibility JavaVersion.VERSION_1_7
        targetCompatibility JavaVersion.VERSION_1_7
    }

    buildTypes {
        release {
            runProguard false
            proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.txt'
        }
    }
}

dependencies {
    compile fileTree(include: ['*.jar'], dir: 'libs')
}

これを例えば上記の例のように「アルファ版(デバッグ実行)」「ベータ版」「リリース版」、そして「アプリ hoge」「アプリ fuga」を用意したいのであれば以下のようにする:

apply plugin: 'android'

android {
    compileSdkVersion 19
    buildToolsVersion '19.0.1'

    defaultConfig {
        minSdkVersion 15
        targetSdkVersion 19
    }

    compileOptions {
        sourceCompatibility JavaVersion.VERSION_1_7
        targetCompatibility JavaVersion.VERSION_1_7
    }

    // 証明書の定義. ここで定義したもので自動で署名が行われる
    signingConfigs {
        hoge {
            keyAlias 'hoge'
            keyPassword 'hoge'
            storeFile file('../etc/keystore/hoge.keystore')  // 対象の位置に証明書を用意すること
            storePassword 'hoge'
        }
        fuga {
            keyAlias 'fuga'
            keyPassword 'fuga'
            storeFile file('../etc/keystore/fuga.keystore')
            storePassword 'fuga'
        }
    }

    // ビルドタイプ. アルファ版はデフォルトからある debug を使ってみる
    buildTypes {
        debug {
            debuggable true  // debug の時デフォルト値として true になっているので本来書く必要は無いのだが説明の為
            applicationIdSuffix = '.alpha'  // パッケージ名の末尾に .alpha をつける
            versionNameSuffix = 'a'
        }
        beta {
            applicationIdSuffix = '.beta'
            versionNameSuffix = 'b'
        }
        release {
            runProguard false
            proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.txt'
        }
    }

    // プロダクトフレーバー. それぞれのアプリのパッケージ名と使用する証明書を定義
    productFlavors {
        hoge {
            applicationId = 'com.kojion.hoge'
            signingConfig signingConfigs.hoge
        }
        fuga {
            applicationId = 'com.kojion.fuga'
            signingConfig signingConfigs.fuga
        }
    }
}

dependencies {
    compile fileTree(dir: 'libs', include: ['*.jar'])
}

12.png

これで Gradle の Sync Project をするとスクリーンショットのように Build Variants が 6 種類に増える。このリストボックスを切り替えて Android Studio から Run (実行) すると対象の Build Variants で定義された証明書、パッケージ名等の情報でビルドした APK を生成し端末にインストールしてくれる。

上記 build.gradle の例だと beta と release ビルドの時は本番用証明書が使われ、debug の時はデバッグ用の証明書が使用される。なので debug の時は IDE から実行しないとインストールできないはず。

これを使うと本番用 APK を作る際に Generate Signed APK を選択して作らなくても良くなる。Run した結果作成された APK は /build/outputs/apk 直下に (モジュール名)-(Product Flavor)-(Build Type).apk のような名前で作成されるので、それを配布なり何なり利用すれば良い。

これで 2 種類のアプリを簡単に共存させることができるようになった。次はリソースの切り替えだ。

Build Variant に依存するリソースを配備

13.png

Android Studio で新規プロジェクトを作ると Eclipse に比べて階層が深いのが気になると思う。Eclipse だと /src 下に Java パッケージ名を示すフォルダを掘った上で Java ソースファイルを置くのだが、Android Studio だと /src の下に見慣れない main というのが増えている。この main というのは どの Build Variant でも定義されていない Java ソース及びリソースを置く場所 と考えられる。

さて、ではここで「アプリ Hoge の場合はアプリ名を Hoge とし、Fuga の場合はアプリ名を Fuga にする」というのを行ってみる。 アプリを起動した時に表示する Hello World はどちらのアプリでも共通としよう。

まず左のスクリーンショットのように main と同じ階層に hoge と fuga というフォルダを作り、同じように /res/strings.xml を配置する。 strings.xml だけでなく drawable や layout や AndroidManifest.xml など全てのリソースを配置して、 そちら向けのビルドの場合はそのリソースで自動で置き換えてくれる。

対象が XML の場合は自動でマージされるのが便利だ。この要件の場合は以下のようにそれぞれのリソースを定義すれば良い。

main の strings.xml

<?xml version="1.0" encoding="utf-8"?>
<resources>
    <!-- アプリ Hoge でも Fuga でも使用するリソースを定義 -->
    <string name="hello_world">Hello world!</string>
</resources>

hoge の strings.xml

<?xml version="1.0" encoding="utf-8"?>
<resources>
    <!-- アプリ Hoge で使用するリソースを定義 -->
    <string name="app_name">Hoge</string>
</resources>

fuga の strings.xml

<?xml version="1.0" encoding="utf-8"?>
<resources>
    <!-- アプリ Fuga で使用するリソースを定義 -->
    <string name="app_name">Fuga</string>
</resources>

これで前述の Build Variants のリストボックスを切り替えると自動でそちらのリソースを見てくれるようになる。 ちなみにこの例の場合 main には app_name の定義は あってもなくても良い。 あった場合は hoge や fuga を選択した場合 main に定義した app_name はその app_name で上書きされる。 例えばこの後「アプリ hage」を追加して hage のリソースを定義しなかった場合、main の app_name が使われることになる。

Build Type 及びその組み合わせでも良い

14.png

さて、Product Flavor である「hoge」「fuga」でリソースを分ける定義を行ってきたが、実はこれは「debug」「release」のような Build Type でも良いしその組み合わせでも良い。 組み合わせの場合は Product Flavor と Build Type を camelCase でつなげた文字列でフォルダを切れば良い。

例えば Google Maps Android API v2 の API Key のようにパッケージ名と証明書の両方に依存するような値はこの Build Variants によるフォルダ分けで対処する事が可能だし、デバッグ用のアプリと本番用のアプリを同一端末に共存させたい場合 ContentProvider の authority のように端末内で一意となっていなければならない値をこのようなフォルダ分けで定義することが可能だ。

さて、今回は左のスクリーンショットのように「main」「hoge」「fuga」に加えて「debug」と「hogeDebug」を定義してみた。追加された strings.xml は以下のように定義されているとする。

debug の strings.xml

<?xml version="1.0" encoding="utf-8"?>
<resources>
    <!-- debug ビルドの時に使用するリソースを定義 -->
    <string name="hello_world">Hello World!! (as Debug)</string>
</resources>

hogeDebug の strings.xml

<?xml version="1.0" encoding="utf-8"?>
<resources>
    <!-- hoge 向け且つ debug ビルドの時に使用するリソースを定義 -->
    <string name="app_name">Hoge (debug)</string>
</resources>

この状態で Android Studio の Build Variants のリストボックスを「hogeDebug」に合わせて実行すると、アプリ名が「Hoge (debug)」となり Hello World が「Hello World!! (as Debug)」となる。

Java ソースの扱いはどうするか

Java ソースは XML と違って マージも置き換えもされない ので注意する。 Java ソース側で Build Variants によって処理を振り分けたい場合、 例えば XML と同じ考え方で main と hoge と fuga にそれぞれ MainActivity.java を置いた場合ビルド時にクラスが重複しているというエラーになる。 main に置いた MainActivity.java を削除して実行すればビルドに成功する。選択した Build Variants で見るソース群の中で一意になっていれば良いようだ。

Activity や Fragment の実装がアプリ毎に全く異なる実装になっていれば良いのだが、大体の要件はロジックをちょっとだけ変更して欲しいというものだろう。 その場合 BuildConfig から Product Flavor 及び Build Type の値が参照できるようになっているのでこの値を見て処理を振り分ければ良い。 例えば hogeDebug でビルドした場合の BuildConfig.java の内容は以下のように自動生成される:

public final class BuildConfig {
  public static final boolean DEBUG = Boolean.parseBoolean("true");
  public static final String PACKAGE_NAME = "com.kojion.hoge.alpha";
  public static final String BUILD_TYPE = "debug";
  public static final String FLAVOR = "hoge";
  public static final int VERSION_CODE = 1;
  public static final String VERSION_NAME = "0.1a";
}

最後に

この Build Variants は Android Studio というより寧ろ Gradle の機能なのだが Android Studio 側でリストボックスで簡単に切り替えてビルド実行できるところに意味があると思う。 これを知る前はビルド先に応じて Google Maps の API Key を手動で書き換えたりしていたが、今になって思うと何やっていたんだろうという感じだ。