ビルドバリアントによる複数バージョンのAPKのビルド

f:id:vasilyjp:20171012144649p:plain

アプリエンジニアの堀江(@Horie1024)です。

先日、1つのコードベースからアプリ名やアプリアイコン、アプリの挙動を変更した複数のバージョンのAPKをビルドする必要があり、その際どのように対応したかをご紹介しようと思います。

サンプルコード

本記事内で使用するコードは以下になります。

github.com

Androidアプリのビルド

Androidビルドシステムは、アプリのリソースとソースコードをコンパイルしAPKにパッケージ化します。Android Studioを使用したAndroidアプリ開発では、GradleAndroid Plugin for Gradle(以下Android Plugin)が連携することでAndroidアプリのビルドが行われます。また、GradleとAndroid Pluginは、Android Studioから独立していますのでコマンドラインから容易にビルド可能です。

f:id:vasilyjp:20171012144448p:plain

カスタムビルド

GradleとAndroid Pluginで構成されるAndroidのビルドシステムは柔軟で、アプリの主要なソースファイルを変更せずにカスタムビルドを設定可能です。カスタムビルドを設定することで、1つのコードベースに対してビルド設定、コード、およびリソースといったソースセットを置き換えるることができます。それによって、アプリ名やアプリアイコン、アプリの挙動を変更したAPKをビルドすることが可能になります。この際重要になる概念がビルドバリアント(Build Variant)です。

ビルドバリアント

ビルドバリアントは以下の2つ要素の組み合わせから構成されます。

  • ビルドタイプ (Build Type)
  • プロダクトフレーバー (Product Flavor)

例えば、ビルドタイプがdebugrelease、プロダクトフレーバーがapp1app2である場合以下のような組み合わせになり、ビルドバリアントはapp1Debugapp2Debugapp1Releaseapp2Releaseの4種類となります。

f:id:vasilyjp:20171012144547p:plain

各ビルドバリアントは、ビルド可能なバージョンのアプリを表しており、特定の組み合わせのビルドを簡単に実行できます。例えばapp2Debugの組み合わせでビルドしたい場合、以下のコマンドでビルド可能です。

$ ./gradlew assembleApp2Debug

ビルド設定ファイル

先程例として上げたビルドバリアントを構成するビルドタイプとプロダクトフレーバーを作成するには、ビルド設定ファイル(build.gradle)に変更を加え、カスタムビルド作成する必要があります。ビルド設定ファイルにはトップレベルとモジュールレベルがあり、Android Studioでプロジェクトを新規作成すると自動で作成されます。プロジェクトルートにあるbuild.gradleがトップレベルのビルド設定ファイル、app/以下にあるのがモジュールレベルのビルド設定ファイルです。

f:id:vasilyjp:20171013154030p:plain

ビルドタイプ

ビルドタイプはモジュールレベルのbuild.gradleファイルのandroid {}ブロック内で作成します。Android Studioでプロジェクトを新規作成するとデバッグおよびリリースビルドタイプが自動的に追加されます。android {}ブロック内に明示的に指定されているのはリリースビルドタイプのみですが、デバッグビルドタイプも有効になっています。

android {
    defaultConfig {}
    buildTypes {
        release {}
    }
}

プロダクトフレーバー

プロダクトフレーバーの設定はビルドタイプと同様で、productFlavors {}ブロックにプロダクトフレーバーを追加して設定します。プロダクトフレーバーでは、defaultConfigと同様のプロパティがサポートされ、applicationIdversionCodeversionNameといった要素も各プロダクトフレーバーで定義できます。

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がビルド時に使用するソースセットが判別されます。

  1. ビルドバリアントのソースセット
  2. ビルドタイプのソースセット
  3. プロダクトフレーバーのソースセット
  4. メインソースセット

したがって、ビルドバリアント、ビルドタイプ、プロダクトフレーバー毎にソースセットを用意することで複数のバージョンのAPKをビルドすることが可能です。

ビルド設定、コード、リソース、マニフェストについてビルドバリアントを使用した際にどのようにビルドされるのかを見ていきます。

ビルド設定

プロダクトフレーバーではdefaultConfigと同様のDSLを使用できますので、app1app2applicationIdを定義することでapplicationIdを変更可能です。

android {
    defaultConfig {}
    buildTypes {
        debug {}
        release {}
    }

    flavorDimensions "tier"

    productFlavors {
        app1 {
            dimension "tier"
            applicationId "com.horie1024.app1"
        }
        app2 {
            dimension "tier"
            applicationId "com.horie1024.app2"
        }
    }
}

この場合、プロダクトフレーバーapp1app2を含むビルドバリアントでビルドすると、それぞれで別アプリとしてビルドされます。メインソースセットよりプロダクトフレーバーのソースセットの優先度が高いためです。Gradleはビルド時により優先度の高いソースセットを利用します。また、プロダクトフレーバー毎に別々の署名設定を追加することも可能です。

f:id:vasilyjp:20171013154214p:plain

コード

ソースコードについても各ビルドタイプとプロダクトフレーバー毎に用意することで、ビルドするビルドバリアントに応じて挙動を変化させることができます。

以下のコードは、メインソースセットで定義している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クラスの実装をプロダクトフレーバーapp1app2のソースセットのディレクトリsrc/app1/kotlin/com.horie1024.buildvariantssample/src/app2/kotlin/com.horie1024.buildvariantssample/以下に置きます。

f:id:vasilyjp:20171013154250p:plain

実装は以下の通りです。greetメソッドを実行した結果が異なります。

class Greeting {
    fun greet() = "World!"
}
class Greeting {
    fun greet() = "Universe!"
}

ビルドバリアントapp1Debugapp2Debugでビルドした結果は以下の通りです。表示されるテキストの内容が変化しています。

f:id:vasilyjp:20171013154316p:plain

リソース

メインソースセットのresディレクトリ以下のリソースについても、各ソースセットディレクトリ以下にresディレクトリを作成することでビルドバリアントに応じて利用するリソースを切り替えることができます。

strings.xmlで定義したapp_nameリソースを例にどうビルドされるか見てみましょう。ここではapp1app2のソースセットディレクトリに加えて、ビルドバリアントapp2Debugに対応するディレクトリを作成します。

f:id:vasilyjp:20171013154507p:plain

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ソースセットに加えて、app1app2プロダクトフレーバー、ビルドバリアントapp2Debugの各ソースセットでも同名で定義されています。ビルド時にGradleは優先度に応じてこれらの中からどのソースセットを使うかを判断します。

ビルドバリアントapp1Debugでビルドした場合プロダクトフレーバーapp1src/app1/以下に置かれたリソースが使われ、ビルドバリアントapp2Debugでビルドした場合src/app2Debug/以下に置かれたリソースが使われます。以下のようにアプリ名としてそれぞれのリソースで定義したapp_nameの値が使われています。

f:id:vasilyjp:20171013154525p:plain

マニフェスト

マニフェストについても同様です。マニフェストの場合、優先順位が低いものから高いものへマニフェストがマージされ最終的に1つの統合済みのマニフェストとなりAPKにパッケージ化されます。

以下の画像は複数のマニフェスト ファイルの統合から引用したものになります。

f:id:vasilyjp:20171013154551p:plain

ビルドバリアントでのマニフェストの優先度は以下のようになっており、ソースセットの優先度と同一です。

  1. ビルドバリアントマニフェスト(src/app1Debug/ など)
  2. ビルドタイプマニフェスト(src/debug/ など)
  3. プロダクトフレーバーマニフェスト(src/app1/ など)

フレーバーディメンションを設定している場合、flavorDimensionsでの定義順に優先度が付きます。

マニフェストのマージは統合のポリシーはこちらにしたがって行われますが、複数のマニフェストで要素の競合が発生した場合、明示的に統合ルールマーカーを指定して解決します。統合ルールマーカーの詳細はこちらをご覧ください。

ビルドバリアントによるマニフェストの統合を確認するためapp1app2プロダクトフレーバーに対応したソースセットディレクトリに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>

ビルドバリアントapp1Debugapp2Debugでそれぞれビルドした結果は以下の通りです。ビルドバリアントによってAndroidManifest.xmlで適応されるstyleが異なっています。

f:id:vasilyjp:20171013154612p:plain

コード内でのビルドタイプ、プロダクトフレーバーの参照

以下のように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が用意されています。特定のビルドバリアントに切り替えて挙動を確認したい場合簡単に切り替えられます。

f:id:vasilyjp:20171013154634p:plain

「app」の「Build Variant」をクリックするとドロップダウンでビルドバリアントを選択できます。

f:id:vasilyjp:20171013154643p:plain

まとめ

Androidのビルドシステムは、Gradleを採用したことで非常に柔軟にカスタムビルドを作成できるようになっています。ビルド設定ファイルを編集し、ビルドバリアントを利用可能にすれば、主要なコードベースを変更せずともアプリのデザイン・挙動を変化させることができます。

実際に業務でapplicationIdversionCodeversionName、アプリアイコン、アプリ名の異なる4種類のアプリをビルドする必要があったのですが、ビルドバリアントを利用することでコードを変更することなく対応できました。

さいごに

VASILYではアプリエンジニアを大募集しています。是非Wantedlyからご応募ください。

カテゴリー