sumioの技術メモ

Androidについての記事が多くなると思います。

ビルドタイプ・プロダクトフレーバーのカスタマイズ可能項目一覧

はじめに

前回の記事で、ビルドタイプもプロダクトフレーバーも、リソースやソースコードの一部を差し替えることができる点は同じであると書きました。

ビルドタイプでもプロダクトフレーバーでも

src/<ビルドタイプ名 or プロダクトフレーバー名>/

配下にソースコードやリソースの差分を置くことができます。その点では、ビルドタイプとプロダクトフレーバーに機能差分はありません。

ところが、build.gradleでカスタマイズすることのできる項目は、ビルドタイプとプロダクトフレーバーに違いがあります。本家のユーザーガイドの末尾には、

BuildType and Product Flavor property reference

という見出しがあるのですが、現状では「coming soon」としか書かれていません。

そこで、ビルドタイプとプロダクトフレーバーに、それぞれどのような設定項目があるのか調べてみました。

調べ方

ビルドタイプも、プロダクトフレーバーも単なるGroovyクラス(のインスタンス)なので、それぞれのメソッド一覧をリストアップしてみました。リストアップした結果から、以下を取り除いたものを、設定可能項目と推定しています。
実際に設定が反映されるかまでの調査はしていませんので、その点はご注意ください。

  • equalstoStringなどの明らかに無関係なメソッド
  • getからはじまるもの

また、setDebuggabledebuggableのように、プロパティと同名のメソッドが定義されている場合は、後者のみを列挙しています。

調査したプラグインのバージョンは0.5.1です。

参考までに、調査したbuild.gradleファイルの一部を掲載しておきます。

// buildscript { ... } とapplyは省略

android {
    compileSdkVersion 17
    buildToolsVersion "17.0"

    // 適当にプロダクトフレーバーを宣言する
    productFlavors {
        f1 {}
        f2 {}
    }
}

// 「debug」ビルドタイプのインスタンスから、メソッドの一覧を出力する
println "buildTypes = ["
android.buildTypes.debug.metaClass.methods*.name.sort().unique().each {
    println "    ${it}"
}
println "]"

// 「f1」フレーバーのインスタンスから、メソッドの一覧を出力する
println "flavors = ["
android.productFlavors.f1.metaClass.methods*.name.sort().unique().each {
    println "    ${it}"
}
println "]"

カスタマイズ可能項目一覧

上記スクリプトの出力結果を、

  • ビルドタイプ・プロダクトフレーバー共通で利用できるもの
  • ビルドタイプでのみ利用できるもの
  • プロダクトフレーバーでのみ利用できるもの

の順に並べて表にしたものを以下に示します。

メソッド名 Build Type Product Flavor 備考
buildConfig  
proguardFile  
proguardFiles  
signingConfig  
debuggable ×  
init × 通常では利用しない?
initWith × 新規ビルドタイプ生成時に利用
jniDebugBuild ×  
packageNameSuffix × cf. packageName
renderscriptDebugBuild ×  
renderscriptOptimLevel ×  
runProguard ×  
versionNameSuffix × cf. versionName
zipAlign ×  
flavorGroup × 所属するフレーバーグループの宣言に使う
minSdkVersion ×  
packageName × cf. packageNameSuffix
renderscriptTargetApi ×  
targetSdkVersion ×  
testInstrumentationRunner ×  
testPackageName ×  
versionCode ×  
versionName × cf. versionNameSuffix

以上を見てみると、やはり、ビルドタイプは「デバッグ時とリリース時で設定が異なるべきものが設定できる」という位置付けのようです。一方、プロダクトフレーバーでは、minSdkVersionのようにサポートする機種ごとにapkを分けるための仕組みや、フレーバーごとにテスト関連の設定を変更できるような仕組みが用意されていることがわかります。

また、以下の点にも興味を引きました。

  • ProGuard関連や署名関連は、ビルドタイプだけでなく、プロダクトフレーバーでも設定可能。たとえば、有料アプリだけProGuardを有効にしたり、有料アプリと無料アプリで異なる署名をつけることができる。
  • マニフェストに設定するpackageNameversionNameは、ビルドタイプでは接尾辞のみが設定でき、プロダクトフレーバーでは文字列全体のみが設定できる。

まとめ

build.gradle上でカスタマイズ可能な項目について、ビルドタイプとプロダクトフレーバーの違いを列挙してみました。

  • ビルドタイプはデバッグ用(テスト用)とリリース用の違いを設定するために使う
  • その他の違いはプロダクトフレーバーでカスタマイズする

という用途で使い分ければ、カスタマイズしたくなるような項目は大体カバーされているのではないかと思います。

本家ドキュメントの補足として参考にしていただけると嬉しいです。

Android Gradleプラグインにおけるビルドタイプ・フレーバー・フレーバーグループ

Android Gradleプラグインには、「同じような機能を持つけれども少しだけ異なるアプリ」を、便利に開発するための機能が備わっています。
そのためには以下の3つの概念を理解しておく必要があるのですが、すぐに分からなくなってしまうので、ここにメモしておこうと思います。

  • ビルドタイプ
  • フレーバー
  • フレーバーグループ

ビルドタイプ

ビルドタイプは「デバッグ版」「リリース版」を分けるのに使います。デフォルトでは、デバッグ版を表すdebugと、リリース版を表すreleaseの2つのタイプが定義されています。
デバッグ版にはデバッグ署名が付けられ、リリース版には、build.gradleに書いた設定にしたがって、リリース向けの署名が付けられる、といった具合です。

もちろん、必要であれば第3のビルドタイプ、第4のビルドタイプを定義することもできますし、ビルドタイプごとに、ソースコードを差し替えたり、リソースを差し替えたりすることもできます。

なお、デフォルトでは、テストは「デバッグ版」に対してしか実行されません。試験対象を他のビルドタイプに変更することはできますが、複数のビルドタイプを試験対象にすることはできません*1

フレーバー

フレーバーは、アプリケーションの一部の機能や振舞いをカスタマイズするために使います。たとえば「広告あり」フレーバーと「広告なし」フレーバーを定義する、といった具合です。
フレーバーは、build.gradleに定義することではじめて作られますが、1つもフレーバーが定義されていない場合は、暗黙的に名前のない1つのフレーバーが定義されているとみなされます。

ビルドタイプと同様に、フレーバーについても、それぞれソースコードを差し替えたり、リソースを差し替えたりすることができます。

こちらは、ビルドタイプとは異なり、全てのフレーバーが試験対象となります。そのため、テストコードもフレーバーごとに差し替えることが可能になっています。

フレーバーグループ

フレーバーが複数定義されている場合、gradle assembleコマンドによってビルドされるapkは、ビルドタイプの集合とフレーバーの集合の全組み合わせになります。
たとえば、ビルドタイプとフレーバーが、それぞれ以下のように定義されているとします。

  • ビルドタイプ: debug, release
  • フレーバー: f1, f2, fa, fb

この場合にビルドされるapkは、以下の8種類になります。

  • ビルドタイプがdebugで、かつ、フレーバーがf1のもの
  • ビルドタイプがdebugで、かつ、フレーバーがf2のもの
  • ビルドタイプがdebugで、かつ、フレーバーがfaのもの
  • ビルドタイプがdebugで、かつ、フレーバーがfbのもの
  • ビルドタイプがreleaseで、かつ、フレーバーがf1のもの
  • ビルドタイプがreleaseで、かつ、フレーバーがf2のもの
  • ビルドタイプがreleaseで、かつ、フレーバーがfaのもの
  • ビルドタイプがreleaseで、かつ、フレーバーがfbのもの

フレーバーグループは、上記のようなapk生成時の組み合わせを制御するためのもので、フレーバーを複数のグループに分割するために利用します。たとえば、この4つのフレーバーを、以下のように2つのフレーバーグループに所属させてみます。

  • フレーバーグループ1: f1, f2
  • フレーバーグループ2: fa, fb

このようにグループ分けを行うと、ビルドされるapkは、ビルドタイプの集合と、各フレーバーグループの集合の全組み合わせとなります。この場合だと{build, release} × {f1, f2} × {fa, fb}という組み合わせになりますので、以下の8種類のapkがビルドされることになります。

  • ビルドタイプがdebugで、かつ、フレーバーがf1、かつfaのもの
  • ビルドタイプがdebugで、かつ、フレーバーがf1、かつfbのもの
  • ビルドタイプがdebugで、かつ、フレーバーがf2、かつfaのもの
  • ビルドタイプがdebugで、かつ、フレーバーがf2、かつfbのもの
  • ビルドタイプがreleaseで、かつ、フレーバーがf1、かつfaのもの
  • ビルドタイプがreleaseで、かつ、フレーバーがf1、かつfbのもの
  • ビルドタイプがreleaseで、かつ、フレーバーがf2、かつfaのもの
  • ビルドタイプがreleaseで、かつ、フレーバーがf2、かつfbのもの

このように、フレーバーグループを適切に使うことで、いくつもの差分を積み重ねたapkを一気にビルド・テストすることができるようになります。

優先順位

これらの機能を駆使して、差分を積み重ねたapkがビルドできるようになると、各差分に同一の定義がある場合(たとえば同じ名前のstringリソースが、debugf1faに定義されている場合など)に、何が優先されるのか、ということが問題になります。

それもきちんと決まっていて、以下のリストのうち、上にある方がより優先度が高いことになっています。言い換えると、リストの下から順に上書きされていきます。

  • ビルドタイプ
  • フレーバーグループのうち、1つめに宣言したもの
  • フレーバーグループのうち、2つめに宣言したもの
  • ...

このように、フレーバーグループの宣言順序によって優先度が決まってしまうので、フレーバーグループの宣言には気を使う必要がありあます。なお、フレーバーグループの宣言は、以下のような感じで行います。

android {
    ...
    // "group1"の方が"group2"より優先度が高い
    flavorGroups "group1", "group2"
    ...
}

まとめ

特に分かりにくいと思われる点をざっくりまとめてみます。

  • ビルドタイプもフレーバーも、ソースコードやリソースの一部を差し替えることが出来る、という点では同じですが、ビルドタイプは1つしかテスト対象にできない点に注意が必要です。
  • ビルドされるapkは、ビルドタイプとフレーバーの組み合わせになりますが、フレーバーグループを定義することで、組み合わせ方法を制御することができます。
  • フレーバーグループの宣言順序によって、差分適用の優先度が変化することに注意が必要です。

*1:adt-devのやりとりによると、近い将来にこの制限は撤廃されそうな雰囲気です。

Androidのテスト実行も含めてEclipseとGradleを共存させてみる

Androidの新しいビルドシステムで使われているgradleのプラグインは、同じ機能を持つけど少しだけソースが異なるアプリ(フレーバー)を一度にビルド・テストできるなど、Eclipse+ADTやantには無かった素晴しい機能を持っています。

ただ、その機能を享受するためには、従来のAndroid開発とはかけ離れたディレクトリ構成にしなければならず、Eclipse+ADTで開発をしつつ、gradleも使ってみるためには工夫が必要です。

アプリケーションの開発に限って言えば、Eclipse+ADTと共存する方法は、本家のドキュメントや、日経ソフトウェア2013年6月号の「特集4  Androidに新ビルド ・ ツール現る」などに載っているのですが、テストも共存させるための情報は見付けられませんでした。

後者の記事をヒントに、色々と試行錯誤した結果、うまく行く方法を見付けたので、紹介したいと思います。

Eclipseでプロジェクトを作る

まず、Eclipseのウィザードで、アプリケーションプロジェクトと、そのアプリケーションに対するテストプロジェクトを作ります。こんな感じになると思います。
f:id:sumio_tym:20130623220104p:plain

ここでは、アプリケーションプロジェクトの名前を「MyApplication」に、テストプロジェクトの名前を「MyApplicationTest」にしてみました。

もちろん、既に開発中のプロジェクトを利用することもできます。その時は「MyApplication」「MyApplicationTest」を、実際のプロジェクト名に読み替えてください。

作ったら、とりあえずワークスペースから削除してしまいます(ファイルは削除してはいけません)。

ディレクトリ構成をgradle対応にする

適当な場所に

MyApplicationGradle/src

ディレクトリを作成してから、今作成したsrcディレクトリの下に、Eclipseで作成した2つのプロジェクトをコピーします。
この状態で、アプリケーションプロジェクトのディレクトリ名を「main」に、テストプロジェクトのディレクトリ名を「instrumentTest」に変更します。

f:id:sumio_tym:20130623220747p:plain

ここまで出来たら、srcディレクトリ配下をEclipseでインポートしてください。MyApplicationプロジェクトとMyApplicationTestプロジェクトが復活したはずです。これで、Eclipse側ではいつも通り開発することができます。もちろんテスト結果もEcliseのGUIで確認できます。

f:id:sumio_tym:20130623223500p:plain

build.gradleを作る

MyApplicationGradleディレクトリの直下に、以下のようなbuild.graldeファイルを作って保存します。

buildscript {
    repositories {
        mavenCentral()
    }
    dependencies {
        classpath 'com.android.tools.build:gradle:0.4.2'
    }
}
apply plugin: 'android'

android {
    compileSdkVersion 17
    buildToolsVersion "17.0"
}

android.sourceSets.each {
    it.java.srcDirs = [ "src/${it.name}/src" ]
}

これで、gradleでもビルドすることができるようになりました。以下のコマンドで、アプリケーションのビルドとテストを実行できます。複数個の端末が接続されている場合は、全端末でテストが実行されますのでとても便利です。

gradle connectedCheck

なお、テストの実行結果は以下のディレクトリにHTMLとして保存されます。

build/reports/instrumentTests/connected/

まとめと課題

この方法を使えば、少なくとも「main」と「instrumentTest」のソースセットについては、Eclipseで開発とテストを継続することができそうです。

もちろん、フレーバーを追加しはじめると色々と課題がでてきますが、上記構成はAndroid Studioへもそのままインポートできますので、別のフレーバーについて開発する時だけはAndroidStudioを使う、というのも手かもしれません。

しかしながら、Eclipseからはフレーバーを認識しませんので、「main」や「instrumentTest」のソースセットだけでコンパイルエラーが出ないように注意する必要があります。たとえば、フレーバーごとに差し替えたいクラスが有る場合は「片方のフレーバーだけlinked sourceとしてEclipseに登録しておく」という手が使えそうです。

ただ、まだ試していないことも多いので、どこまでこの構成で耐えられるのかは分かりません。たとえば、

  • リソースをフレーバーごとに変えたい場合
  • マニフェストをフレーバーごとに変えたい場合

など、です。これから色々なパターンを試してみたいと思います。

追記

上記build.gradleの末尾に、以下のようなコードを追加することで、プロジェクト名を変更しなくても良いことが分かりました。コメント下さった id:kimukou_26 さん、どうもありがとうございました。

// 'MyApplication' や 'MyApplicationTest' は正しいディレクトリ名にしてください。
[main:'MyApplication', instrumentTest:'MyApplicationTest'].each { key, value ->
    android.sourceSets[key].setRoot("src/${value}")
}