アプリエンジニアの堀江(@Horie1024)です。
先日、1つのコードベースからアプリ名やアプリアイコン、アプリの挙動を変更した複数のバージョンのAPKをビルドする必要があり、その際どのように対応したかをご紹介しようと思います。
サンプルコード
本記事内で使用するコードは以下になります。
Androidアプリのビルド
Androidビルドシステムは、アプリのリソースとソースコードをコンパイルしAPKにパッケージ化します。Android Studioを使用したAndroidアプリ開発では、GradleとAndroid Plugin for Gradle(以下Android Plugin)が連携することでAndroidアプリのビルドが行われます。また、GradleとAndroid Pluginは、Android Studioから独立していますのでコマンドラインから容易にビルド可能です。
カスタムビルド
GradleとAndroid Pluginで構成されるAndroidのビルドシステムは柔軟で、アプリの主要なソースファイルを変更せずにカスタムビルドを設定可能です。カスタムビルドを設定することで、1つのコードベースに対してビルド設定、コード、およびリソースといったソースセットを置き換えるることができます。それによって、アプリ名やアプリアイコン、アプリの挙動を変更したAPKをビルドすることが可能になります。この際重要になる概念がビルドバリアント(Build Variant)です。
ビルドバリアント
ビルドバリアントは以下の2つ要素の組み合わせから構成されます。
- ビルドタイプ (Build Type)
- プロダクトフレーバー (Product Flavor)
例えば、ビルドタイプがdebug
とrelease
、プロダクトフレーバーがapp1
とapp2
である場合以下のような組み合わせになり、ビルドバリアントはapp1Debug
、app2Debug
、app1Release
、app2Release
の4種類となります。
各ビルドバリアントは、ビルド可能なバージョンのアプリを表しており、特定の組み合わせのビルドを簡単に実行できます。例えばapp2Debug
の組み合わせでビルドしたい場合、以下のコマンドでビルド可能です。
$ ./gradlew assembleApp2Debug
ビルド設定ファイル
先程例として上げたビルドバリアントを構成するビルドタイプとプロダクトフレーバーを作成するには、ビルド設定ファイル(build.gradle
)に変更を加え、カスタムビルド作成する必要があります。ビルド設定ファイルにはトップレベルとモジュールレベルがあり、Android Studioでプロジェクトを新規作成すると自動で作成されます。プロジェクトルートにあるbuild.gradle
がトップレベルのビルド設定ファイル、app/
以下にあるのがモジュールレベルのビルド設定ファイルです。
ビルドタイプ
ビルドタイプはモジュールレベルのbuild.gradle
ファイルのandroid {}
ブロック内で作成します。Android Studioでプロジェクトを新規作成するとデバッグおよびリリースビルドタイプが自動的に追加されます。android {}
ブロック内に明示的に指定されているのはリリースビルドタイプのみですが、デバッグビルドタイプも有効になっています。
android { defaultConfig {} buildTypes { release {} } }
プロダクトフレーバー
プロダクトフレーバーの設定はビルドタイプと同様で、productFlavors {}
ブロックにプロダクトフレーバーを追加して設定します。プロダクトフレーバーでは、defaultConfig
と同様のプロパティがサポートされ、applicationId
やversionCode
、versionName
といった要素も各プロダクトフレーバーで定義できます。
Android Plugin for Gradle 3.0.0以降を使用する場合flavorDimensions
の設定が必須になります。flavorDimensions
についてはこちらをご覧ください。以下の例では、tierフレーバーディメンションを作成し各プロダクトフレーバーに設定しています。
android { defaultConfig {} buildTypes { debug {} release {} } flavorDimensions "tier" productFlavors { app1 { dimension "tier" } app2 { dimension "tier" } } }
複数バージョンのAPKのビルド
各ビルドタイプとプロダクトフレーバー毎にビルド設定、コード、およびリソースといったソースセットを持つことができます。特定のビルドバリアントでビルドする場合、以下の優先順位に従ってGradleがビルド時に使用するソースセットが判別されます。
- ビルドバリアントのソースセット
- ビルドタイプのソースセット
- プロダクトフレーバーのソースセット
- メインソースセット
したがって、ビルドバリアント、ビルドタイプ、プロダクトフレーバー毎にソースセットを用意することで複数のバージョンのAPKをビルドすることが可能です。
ビルド設定、コード、リソース、マニフェストについてビルドバリアントを使用した際にどのようにビルドされるのかを見ていきます。
ビルド設定
プロダクトフレーバーではdefaultConfig
と同様のDSLを使用できますので、app1
、app2
でapplicationId
を定義することでapplicationId
を変更可能です。
android { defaultConfig {} buildTypes { debug {} release {} } flavorDimensions "tier" productFlavors { app1 { dimension "tier" applicationId "com.horie1024.app1" } app2 { dimension "tier" applicationId "com.horie1024.app2" } } }
この場合、プロダクトフレーバーapp1
、app2
を含むビルドバリアントでビルドすると、それぞれで別アプリとしてビルドされます。メインソースセットよりプロダクトフレーバーのソースセットの優先度が高いためです。Gradleはビルド時により優先度の高いソースセットを利用します。また、プロダクトフレーバー毎に別々の署名設定を追加することも可能です。
コード
ソースコードについても各ビルドタイプとプロダクトフレーバー毎に用意することで、ビルドするビルドバリアントに応じて挙動を変化させることができます。
以下のコードは、メインソースセットで定義しているMainActivity
で、Greeting
クラスのgreet
メソッドを実行した結果を画面に表示します。
class MainActivity : AppCompatActivity() { override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) setContentView(R.layout.activity_main) val tv: TextView = findViewById(R.id.hello_text) tv.text = "Hello ${Greeting().greet()}" } }
Greeting
クラスの実装をプロダクトフレーバーapp1
、app2
のソースセットのディレクトリsrc/app1/kotlin/com.horie1024.buildvariantssample/
、src/app2/kotlin/com.horie1024.buildvariantssample/
以下に置きます。
実装は以下の通りです。greet
メソッドを実行した結果が異なります。
class Greeting { fun greet() = "World!" }
class Greeting { fun greet() = "Universe!" }
ビルドバリアントapp1Debug
、app2Debug
でビルドした結果は以下の通りです。表示されるテキストの内容が変化しています。
リソース
メインソースセットのres
ディレクトリ以下のリソースについても、各ソースセットディレクトリ以下にres
ディレクトリを作成することでビルドバリアントに応じて利用するリソースを切り替えることができます。
strings.xml
で定義したapp_name
リソースを例にどうビルドされるか見てみましょう。ここではapp1
、app2
のソースセットディレクトリに加えて、ビルドバリアントapp2Debug
に対応するディレクトリを作成します。
各strings.xml
は以下のように定義しています。
- src/main/
<resources> <string name="app_name">BuildVariantsSample</string> </resources>
- src/app1/
<resources> <string name="app_name">app1</string> </resources>
- src/app2/
<resources> <string name="app_name">app2</string> </resources>
- src/app2Debug/
<resources> <string name="app_name">app2Debug</string> </resources>
app_name
リソースはmainソースセットに加えて、app1
、app2
プロダクトフレーバー、ビルドバリアントapp2Debug
の各ソースセットでも同名で定義されています。ビルド時にGradleは優先度に応じてこれらの中からどのソースセットを使うかを判断します。
ビルドバリアントapp1Debug
でビルドした場合プロダクトフレーバーapp1
のsrc/app1/
以下に置かれたリソースが使われ、ビルドバリアントapp2Debug
でビルドした場合src/app2Debug/
以下に置かれたリソースが使われます。以下のようにアプリ名としてそれぞれのリソースで定義したapp_name
の値が使われています。
マニフェスト
マニフェストについても同様です。マニフェストの場合、優先順位が低いものから高いものへマニフェストがマージされ最終的に1つの統合済みのマニフェストとなりAPKにパッケージ化されます。
以下の画像は複数のマニフェスト ファイルの統合から引用したものになります。
ビルドバリアントでのマニフェストの優先度は以下のようになっており、ソースセットの優先度と同一です。
- ビルドバリアントマニフェスト(src/app1Debug/ など)
- ビルドタイプマニフェスト(src/debug/ など)
- プロダクトフレーバーマニフェスト(src/app1/ など)
フレーバーディメンションを設定している場合、flavorDimensions
での定義順に優先度が付きます。
マニフェストのマージは統合のポリシーはこちらにしたがって行われますが、複数のマニフェストで要素の競合が発生した場合、明示的に統合ルールマーカーを指定して解決します。統合ルールマーカーの詳細はこちらをご覧ください。
ビルドバリアントによるマニフェストの統合を確認するためapp1
、app2
プロダクトフレーバーに対応したソースセットディレクトリにandroid:theme
で指定するstyleを変更したAndroidManifest.xml
を用意しました。統合ルールマーカーtools:replace="android:theme"
を指定して競合を解決しています。
- メインソースセットのAndroidManifest.xml
<?xml version="1.0" encoding="utf-8"?> <manifest xmlns:android="http://schemas.android.com/apk/res/android" package="com.horie1024.buildvariantssample"> <application android:allowBackup="true" android:icon="@mipmap/ic_launcher" android:label="@string/app_name" android:roundIcon="@mipmap/ic_launcher_round" android:supportsRtl="true" android:theme="@style/AppTheme"> <activity android:name=".MainActivity"> <intent-filter> <action android:name="android.intent.action.MAIN" /> <category android:name="android.intent.category.LAUNCHER" /> </intent-filter> </activity> </application> </manifest>
- app1プロダクトフレーバーソースセットのAndroidManifest.xml
<?xml version="1.0" encoding="utf-8"?> <manifest xmlns:android="http://schemas.android.com/apk/res/android" xmlns:tools="http://schemas.android.com/tools" package="com.horie1024.buildvariantssample"> <application android:allowBackup="true" android:icon="@mipmap/ic_launcher" android:label="@string/app_name" android:roundIcon="@mipmap/ic_launcher_round" android:supportsRtl="true" android:theme="@style/App1Theme" <!-- AppThemeの代わりにApp1Themeを指定 --> tools:replace="android:theme"> <activity android:name=".MainActivity"> <intent-filter> <action android:name="android.intent.action.MAIN" /> <category android:name="android.intent.category.LAUNCHER" /> </intent-filter> </activity> </application> </manifest>
- app2プロダクトフレーバーソースセットのAndroidManifest.xml
<?xml version="1.0" encoding="utf-8"?> <manifest xmlns:android="http://schemas.android.com/apk/res/android" xmlns:tools="http://schemas.android.com/tools" package="com.horie1024.buildvariantssample"> <application android:icon="@mipmap/ic_launcher" android:label="@string/app_name" android:roundIcon="@mipmap/ic_launcher_round" android:supportsRtl="true" android:theme="@style/App2Theme" <!-- AppThemeの代わりにApp2Themeを指定 --> tools:replace="android:theme"> <activity android:name=".MainActivity"> <intent-filter> <action android:name="android.intent.action.MAIN" /> <category android:name="android.intent.category.LAUNCHER" /> </intent-filter> </activity> </application> </manifest>
ビルドバリアントapp1Debug
、app2Debug
でそれぞれビルドした結果は以下の通りです。ビルドバリアントによってAndroidManifest.xml
で適応されるstyleが異なっています。
コード内でのビルドタイプ、プロダクトフレーバーの参照
以下のようにBuildConfig.java
が自動生成されるのでビルドタイプ、プロダクトフレーバーの種類で処理を分岐可能です。
public final class BuildConfig { public static final boolean DEBUG = Boolean.parseBoolean("true"); public static final String APPLICATION_ID = "com.horie1024.app1"; public static final String BUILD_TYPE = "debug"; public static final String FLAVOR = "app1"; public static final int VERSION_CODE = 2; public static final String VERSION_NAME = "2.0"; }
プロダクトフレーバーがapp1
の場合のみ実行したい処理がある場合以下のように書けます。
if (BuildConfig.FLAVOR == "app1") // do something
Android Studio上でのビルドバリアントの切り替え
Android Studioでは、ビルドバリアントを切り替えるUIが用意されています。特定のビルドバリアントに切り替えて挙動を確認したい場合簡単に切り替えられます。
「app」の「Build Variant」をクリックするとドロップダウンでビルドバリアントを選択できます。
まとめ
Androidのビルドシステムは、Gradleを採用したことで非常に柔軟にカスタムビルドを作成できるようになっています。ビルド設定ファイルを編集し、ビルドバリアントを利用可能にすれば、主要なコードベースを変更せずともアプリのデザイン・挙動を変化させることができます。
実際に業務でapplicationId
、versionCode
、versionName
、アプリアイコン、アプリ名の異なる4種類のアプリをビルドする必要があったのですが、ビルドバリアントを利用することでコードを変更することなく対応できました。
さいごに
VASILYではアプリエンジニアを大募集しています。是非Wantedlyからご応募ください。