はじめに
エクストーンのAndroidエンジニアの石原です。 本記事は前編からの続きです。
Convention Plugin の具体的な使用例
前章では、Convention Pluginの概念とbuild-logicモジュールの作成方法について説明しました。しかし、これだけでは具体的にどのようにConvention Pluginが機能し、どのような効果をもたらすのかイメージしづらいかもしれません。
そこで本章では、実際にAndroidLibraryConventionPluginがどのように実装され、各モジュールに適用されるのか、そして具体的にどのような設定が集約されるのかを、実際のコード例を使って詳しく解説します。これにより、Convention Pluginの実用的な価値をより具体的に理解することができるでしょう。
import com.android.build.api.dsl.AndroidResources import com.android.build.api.dsl.BuildFeatures import com.android.build.api.dsl.BuildType import com.android.build.api.dsl.CommonExtension import com.android.build.api.dsl.DefaultConfig import com.android.build.api.dsl.Installation import com.android.build.api.dsl.LibraryExtension import com.android.build.api.dsl.ProductFlavor import org.gradle.api.Plugin import org.gradle.api.Project import org.gradle.kotlin.dsl.configure class AndroidLibraryConventionPlugin : Plugin<Project> { override fun apply(target: Project) { with(target) { with(pluginManager) { apply("com.android.library") apply("org.jetbrains.kotlin.android") apply("org.jetbrains.kotlin.kapt") } extensions.configure<LibraryExtension> { @Suppress("UNCHECKED_CAST") val commonExtension = this as CommonExtension<BuildFeatures, BuildType, DefaultConfig, ProductFlavor, AndroidResources, Installation> configureAndroid(commonExtension) configureBuildTypes(commonExtension) configureTestOptions(commonExtension) configurePackagingOptions(commonExtension) configureKotlinAndroid(commonExtension) configureKapt(commonExtension) configureFlavors(commonExtension) sourceSets.getByName("main") { java.srcDirs("src/main/kotlin") } } } } }
import com.android.build.api.dsl.AndroidResources import com.android.build.api.dsl.BuildFeatures import com.android.build.api.dsl.BuildType import com.android.build.api.dsl.CommonExtension import com.android.build.api.dsl.DefaultConfig import com.android.build.api.dsl.Installation import com.android.build.api.dsl.ProductFlavor import org.gradle.api.JavaVersion import org.gradle.api.plugins.ExtensionAware import org.jetbrains.kotlin.gradle.plugin.KaptExtension /** * Android Library の共通設定を行う */ internal fun configureAndroid(commonExtension: CommonExtension<BuildFeatures, BuildType, DefaultConfig, ProductFlavor, AndroidResources, Installation>) { commonExtension.apply { compileSdk = 34 defaultConfig { minSdk = 24 testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner" } compileOptions { sourceCompatibility = JavaVersion.VERSION_17 targetCompatibility = JavaVersion.VERSION_17 } buildFeatures { aidl = true } } } /** * BuildTypes の共通設定を行う */ internal fun configureBuildTypes(commonExtension: CommonExtension<BuildFeatures, BuildType, DefaultConfig, ProductFlavor, AndroidResources, Installation>) { commonExtension.apply { buildTypes { getByName("release") { isMinifyEnabled = false } create("minified") create("ciRelease") } } } /** * TestOptions の共通設定を行う */ internal fun configureTestOptions(commonExtension: CommonExtension<BuildFeatures, BuildType, DefaultConfig, ProductFlavor, AndroidResources, Installation>) { commonExtension.apply { testOptions { unitTests.isReturnDefaultValues = true unitTests.isIncludeAndroidResources = true } } } /** * PackagingOptions の共通設定を行う */ internal fun configurePackagingOptions(commonExtension: CommonExtension<BuildFeatures, BuildType, DefaultConfig, ProductFlavor, AndroidResources, Installation>) { commonExtension.apply { packaging { resources { excludes += "/META-INF/{AL2.0,LGPL2.1}" excludes += "META-INF/*" } } } } /** * Kapt の共通設定を行う */ internal fun configureKapt(commonExtension: CommonExtension<BuildFeatures, BuildType, DefaultConfig, ProductFlavor, AndroidResources, Installation>) { (commonExtension as ExtensionAware).extensions.findByType(KaptExtension::class.java)?.apply { arguments { correctErrorTypes = true } } }
AndroidLibraryConventionPluginの責務と利用方法
AndroidLibraryConventionPlugin
はGradleプラグインとして、Androidライブラリモジュールに共通の設定を適用する役割を持っています。
AndroidLibrary.ktとの関係
AndroidLibrary.kt
で定義されている関数は、モジュール間で再利用可能な設定ロジックを提供しています。AndroidLibraryConventionPlugin
クラスのapply
メソッド内でこれらの関数が以下のように呼び出されています
configureAndroid
コンパイルSDK、最小SDK、Javaバージョンなどの基本設定configureBuildTypes
release/minified/ciReleaseなどのビルドタイプ設定configureTestOptions
ユニットテスト設定configurePackagingOptions
リソース除外などのパッケージング設定configureKapt
Kotlin注釈処理ツールの設定
プラグインの処理フロー
- まず、必要な基本プラグインを適用
com.android.library
org.jetbrains.kotlin.android
org.jetbrains.kotlin.kapt
LibraryExtension
をキャストしてCommonExtension
に変換AndroidLibrary.kt
で定義された各設定関数を順次呼び出し- ソースセットの設定(Kotlinソースディレクトリの指定)
このプラグインを使用することで、各ライブラリモジュールごとに同じ設定を繰り返し記述する必要がなくなり、設定の統一性と保守性が向上します。
プラグインの登録
先ほど作成したプラグインの登録方法は以下の通りです。
libs.versions.toml
// 必要な箇所のみ抜粋 # Plugins defined by this project // 以下は同様のやり方で独自で定義したプラグイン sample-android-application = { id = "sample.android.android.application" } sample-android-application-compose = { id = "sample.android.application.compose" } sample-android-application-flavors = { id = "sample.android.application.flavors" } // 上で定義したAndroidLibraryConventionPlugin sample-android-library = { id = "sample.android.library" } sample-android-library-compose = { id = "sample.android.library.compose" } sample-hilt = { id = "sample.hilt" } sample-ui = { id = "sample.ui" }
build-logic/build.gradle
gradlePlugin { plugins { // 先ほど定義したAndroidLibraryConventionPlugin register("androidLibrary") { // libs.version.tomlで定義したid id = libs.plugins.sample.android.library.asProvider().get().pluginId // conventionPluginクラスのパス implementationClass = "com.sample.android.example.AndroidLibraryConventionPlugin" } } }
プラグインのモジュールへの適用
domain モジュールへの AndroidLibraryConventionPlugin の適用例を通して説明します。
domain/build.gradle.kts の記述
:domain モジュールは、アプリケーションのビジネスロジックを担うライブラリモジュールです。 このモジュールに、 AndroidLibraryConventionPlugin を適用して、共通のビルド設定を適用してみましょう。 以下は、domain/build.gradle.kts の内容です。
plugins { // 今回説明しているAndroidLibraryConventionPluginの利用設定 alias(libs.plugins.sample.android.library) // 以下同様に作成した別のプラグイン alias(libs.plugins.sample.android.library.compose) alias(libs.plugins.sample.hilt) alias(libs.plugins.kotlinx.serialization) alias(libs.plugins.kotlin.parcelize) } android { namespace = "com.example.android.sample.domain" } dependencies { // domainモジュールに必要な依存関係・ここでは省略 }
AndroidLibraryConventionPlugin の効果
:domain モジュールの build.gradle.kts では、plugins { alias(libs.plugins.sample.android.library) } の一行で AndroidLibraryConventionPlugin を適用しています。これにより、次の効果が得られます。
Android プラグインと Kotlin プラグインの適用
AndroidLibraryConventionPlugin の内部では、com.android.library と org.jetbrains.kotlin.android プラグインが適用されます。これにより、Android ライブラリモジュールとしてビルドできるようになり、 Kotlin のコードを扱うことができるようになります。 またorg.jetbrains.kotlin.kaptプラグインも適用されます。
共通の Android 設定
AndroidLibrary.kt で定義された configureAndroid() 関数が呼び出され、以下の設定が適用されます。
compileSdk = 34 コンパイル SDK バージョンが 34 に設定されます。
minSdk = 24 最小 SDK バージョンが 24 に設定されます。
testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner" テスト環境が設定されます。
compileOptions ソースコードの互換性としてJava 17を設定し、コンパイル時にJava17でコンパイルされます。
buildFeatures aidlを有効にします。
共通の Build Types 設定
AndroidLibrary.kt で定義された configureBuildTypes() 関数が呼び出され、以下の設定が適用されます。
release: isMinifyEnabled = false が設定され、リリースビルドでのコード難読化が無効になります。
minified: 難読化が有効なBuildTypesを作成します。
ciRelease: CI用のrelease用BuildTypesを作成します。
共通の Test Options 設定
AndroidLibrary.kt で定義された configureTestOptions() 関数が呼び出され、以下の設定が適用されます。
unitTests.isReturnDefaultValues = true: ユニットテストでデフォルト値を返すように設定されます。
unitTests.isIncludeAndroidResources = true: android resourcesをテストに含めます。
共通の Packaging Options 設定
AndroidLibrary.kt で定義された configurePackagingOptions() 関数が呼び出され、以下の設定が適用されます。
excludes: パッケージングから除外されるファイルが設定されます。
共通のKapt設定
AndroidLibrary.kt で定義されたconfigureKapt()関数が呼び出され、Kaptのエラータイプが補正されます。
sourceSets設定
ソースコードのディレクトリを上書き設定しています。 このように、AndroidLibraryConventionPlugin を適用するだけで、AndroidLibrary.kt に記述した複数の設定を、:domain モジュールに一括して適用できました。
他のライブラリモジュールへの展開
ここで注目したいのは、:domain モジュールだけでなく、他のライブラリモジュール( 例えば :data, :ui など)も同様に AndroidLibraryConventionPlugin を適用することで、同じ設定を一貫して適用できる点です。
// data/build.gradle.kts plugins { alias(libs.plugins.sample.android.library) } // ui/build.gradle.kts plugins { alias(libs.plugins.sample.android.library) }
このように、各モジュールで数行のコードを追記するだけで、 AndroidLibrary.kt で定義された共通設定を適用できます。
Convention Plugin によるメリット
AndroidLibraryConventionPlugin を使用することで、以下のメリットが得られます。
ビルド設定の重複の排除
各モジュールで個別に設定を記述する必要がなくなり、 ビルドスクリプトの重複を大幅に削減できます。 設定の一貫性の確保: AndroidLibrary.kt を修正するだけで、すべてのライブラリモジュールに一括して設定を適用できるため、 プロジェクト全体の設定の一貫性を確保できます。
保守性の向上
設定が一箇所に集約されるため、変更や修正が容易になり、 ビルドスクリプトの保守性が向上します。
可読性の向上
各モジュールでの設定記述が減るため、build.gradle.kts ファイルが簡潔になり、可読性が向上します。
新規モジュール作成のコスト削減
新規モジュールを追加する際も、AndroidLibraryConventionPlugin を適用するだけで、一貫した設定を適用できるため、 設定にかかるコストを削減できます。
ボイラープレート削減効果
前章では、AndroidLibraryConventionPluginの実装とその使用方法について詳細に解説しました。これまでの説明で、Convention Pluginの仕組みと適用方法について理解を深めていただけたと思います。
しかし、多くの開発者が最も気になるのは「実際にどれだけの効果があるのか?」という点ではないでしょうか。次の章では、Convention Plugin導入前後のビルドスクリプトを比較し、具体的にどのようなボイラープレートコードが削減されたのか、そしてそれによってどのようなメリットが得られたのかを検証します。これにより、Convention Plugin導入の具体的な効果を数値やコード例とともに理解することができます。
以下は、Convention Plugin を適用する前の domains/build.gradle (Groovy) の内容です。
plugins { alias(libs.plugins.android.gradle.library) alias(libs.plugins.kotlin.android) alias(libs.plugins.kotlin.kapt) alias(libs.plugins.dagger.hilt) alias(libs.plugins.kotlin.parcelize) alias(libs.plugins.kotlinx.serialization) alias(libs.plugins.compose.compiler) } android { namespace "com.sample.domain" compileSdk libs.versions.compile.sdk.get().toInteger() defaultConfig { minSdk libs.versions.min.sdk.get().toInteger() targetSdk libs.versions.target.sdk.get().toInteger() testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner" } with flavorConfig with sourceConfig buildTypes { minified ciRelease } compileOptions { coreLibraryDesugaringEnabled true sourceCompatibility JavaVersion.VERSION_17 targetCompatibility JavaVersion.VERSION_17 } kotlinOptions { jvmTarget = JavaVersion.VERSION_17 } testOptions { unitTests { includeAndroidResources = true } } packagingOptions { resources.excludes.add("META-INF/*") } } kapt { correctErrorTypes = true } dependencies { implementation fileTree(include: ['*.jar'], dir: 'libs') implementation project(":analytics") compileOnly fileTree(dir: '../libs', include: ['*.aar', '*.jar'], exclude: []) implementation(libs.androidx.core.ktx) testImplementation(libs.bundles.test) lintChecks(libs.ktlint.compose) androidTestImplementation(libs.bundles.androidx.test.ext) androidTestImplementation(libs.bundles.android.test) implementation(libs.kotlinx.serialization) // Dagger Hilt implementation(libs.dagger.hilt.android) kapt(libs.dagger.hilt.compiler) implementation(libs.timber) implementation(libs.androidx.paging.runtime) { exclude group: 'androidx.lifecycle', module: 'lifecycle-viewmodel-ktx' } implementation platform(libs.firebase.bom) implementation(libs.firebase.messaging.ktx) implementation(libs.firebase.crashlytics.ktx) // Desugar coreLibraryDesugaring(libs.androidx.desugar) implementation(libs.bundles.compose) implementation platform(libs.compose.bom) }
以下は、コンベンションプラグイン適用後のdomain/build.gradle.kts の内容です。
plugins { alias(libs.plugins.sample.android.library) alias(libs.plugins.sample.android.library.compose) alias(libs.plugins.sample.hilt) alias(libs.plugins.kotlinx.serialization) alias(libs.plugins.kotlin.parcelize) } android { namespace = "com.example.android.sample.domain" } dependencies { implementation(project(":analytics")) implementation(fileTree(mapOf("include" to listOf("*.jar"), "dir" to "libs"))) compileOnly(fileTree(mapOf("dir" to "../libs", "include" to listOf("*.aar", "*.jar"), "exclude" to listOf<String>()))) implementation(libs.androidx.core.ktx) implementation(libs.timber) implementation(libs.androidx.paging.runtime) { exclude(group = "androidx.lifecycle", module = "lifecycle-viewmodel-ktx") } implementation(platform(libs.firebase.bom)) implementation(libs.firebase.messaging.ktx) implementation(libs.firebase.crashlytics.ktx) testImplementation(libs.bundles.test) androidTestImplementation(libs.bundles.androidx.test.ext) androidTestImplementation(libs.bundles.android.test) coreLibraryDesugaring(libs.androidx.desugar) implementation(libs.kotlinx.serialization) }
AndroidLibraryConventionPlugin 適用によるボイラープレート削減効果
上記の「適用前」と「適用後」の build.gradle を比較することで、Convention Plugin の適用によるボイラープレート削減効果が明確にわかります。
設定の集約とコード量の削減
android ブロック
適用前の Groovy ファイルでは、android ブロック内に多くの設定が直接記述されていました。 具体的には、 compileSdk, minSdk, targetSdk, testInstrumentationRunner、ビルドタイプの定義(buildTypes)、コンパイルオプション(compileOptions)、Kotlin オプション(kotlinOptions)、テストオプション(testOptions)、packagingOptionsなどです。 これらの設定は、AndroidLibrary.kt で定義された関数 (configureAndroid, configureBuildTypes, configureTestOptions, configurePackagingOptions) に集約されました。 適用後の Kotlin DSL ファイルでは、android ブロックに namespace のみ記述され、それ以外は省略されています。 その結果、androidブロック内の設定に関するコード量で約20行の削減が実現できました。
kapt設定
kaptブロックに直接記載していた設定はAndroidLibrary.ktに集約されたため、1行の削減が実現できました。
ソースコードのディレクトリの設定
with sourceConfigで設定していた記述がAndroidLibraryConventionPluginに設定を移行したため約10行の削減が実現できました。 本ブログでは説明しませんが、source directoryに関わる記述がgroovy scriptで記述されていたものをケアしています。
プラグインの適用の一元管理
適用前のGroovy
各モジュールで必要なpluginをそれぞれ記載する必要がありました。 com.android.library, org.jetbrains.kotlin.androidといったAndroidライブラリとkotlinの設定が記載されています。
適用後のKotlin DSL
AndroidLibraryConventionPluginを定義したbuild-logicモジュールに集約されます。 これによって各モジュールにcom.android.library, org.jetbrains.kotlin.androidの記載が不要となります。 その結果、各モジュールあたり2行削減に繋がります。
ビルドタイプ、コンパイルオプション、kotlinオプション、 テストオプション、 パッケージオプションの削除
これらの設定は、すべてAndroidLibraryConventionPluginに集約されているため、各モジュールで重複して記載する必要はなくなリました。 その結果、各モジュールあたり、20行近くの削減に繋がリました。
flavorの設定の削除
with flavorConfigで設定していた記述がAndroidLibraryConventionPluginに設定を移行したため約10行の削減 本ブログでは説明しませんが、flavorに関わる記述がgroovy scriptで記述されていたものをケアしています。 結果、全体で50行以上の削減が、各モジュールで期待されます。
設定の統一と保守性の向上
設定の統一
AndroidLibrary.kt で設定を定義し、AndroidLibraryConventionPlugin を通じて各モジュールに適用することで、プロジェクト全体の設定を一貫して管理できるようになりました。
保守性の向上
設定の変更や追加は、AndroidLibrary.kt のみを修正すれば済むため、保守性が大幅に向上しました。 例えば、 コンパイル SDK のバージョンを変更する場合、以前はすべての build.gradle ファイルを修正する必要がありましたが、Convention Plugin 導入後は AndroidLibrary.kt の一行を修正するだけで済みます。これにより、 変更作業の負荷とヒューマンエラーのリスクを大幅に軽減することができました。
可読性の向上
適用後の domain/build.gradle.kts では、必要な設定のみが簡潔に記述されており、 非常に可読性が高くなりました。 複雑なビルド設定が抽象化され、各モジュールのビルドスクリプトが何をしているのかを把握しやすくなりました。
新規モジュールの追加コスト削減
既存のモジュール同様に、新規モジュールにおいても AndroidLibraryConventionPluginを適用するだけで、一貫した設定を適用できます。 その結果、新規モジュール作成時に発生していた設定コストを大幅に削減することが期待できます。
他のライブラリモジュールへの展開
ここで注目したいのは、:domain モジュールだけでなく、他のライブラリモジュール( 例えば :data, :ui など)も同様に AndroidLibraryConventionPlugin を適用することで、同じ設定を一貫して適用できる点です。
まとめ
今回の実装詳細編では、Groovy DSLからKotlin DSLへの段階的な移行手法と、AIツール(Gemini in Android Studio)の活用による効率化、さらにマルチモジュール環境でのConvention Plugin導入によるビルドロジックの集約について具体的に解説しました。
成果と効果
特に、AIを活用した構文変換やコード自動生成により手作業の負担を大幅に軽減できたこと、また、build-logicモジュールとConvention Pluginの組み合わせによって、ビルド設定の一元管理・重複排除・保守性向上といった大きな効果が得られた点が大きな成果です。各モジュールでの設定記述が最小限となり、可読性・一貫性・新規モジュール追加時のコスト削減も実現できました。
実践する際の注意点
この手法を自身のプロジェクトに適用する際、いくつかの重要なポイントがあります。
段階的な移行計画を立てる
いきなり全モジュールを変換するのではなく、一つのモジュールから始めて徐々に拡大していくアプローチが効果的です。これにより、問題が発生した場合の影響範囲を限定できます。
チームのコンセンサスを得る
Kotlin DSLとConvention Pluginの導入はビルドシステム全体に影響するため、チーム全体の理解と協力が不可欠です。移行の目的とメリットを明確に共有しましょう。
テスト環境の整備
移行後のビルド結果が移行前と同一であることを確認するためのCI/CDパイプラインの整備が重要です。特にビルドバリアントやフレーバーが複雑な場合は注意が必要です。
適切なドキュメント化
特にConvention Pluginの機能と使い方については、チーム内で共有できるドキュメントを作成することが重要です。新メンバーのオンボーディングも容易になります。
どのようなプロジェクトに有効か
この手法は、以下のような特徴を持つプロジェクトで特に効果を発揮します。
マルチモジュール構成
モジュール数が5つ以上あるプロジェクトでは、設定の重複が顕著になり、Convention Pluginによる恩恵が大きくなります。
長期的な保守が必要
継続的な開発・保守が予定されているプロジェクトでは、初期投資以上のメリットが得られます。
チーム開発
複数のエンジニアが関わるプロジェクトでは、統一されたビルド設定によるコラボレーション効率の向上が期待できます。
頻繁なモジュール追加
新機能開発に伴い、モジュールが追加されていく予定のプロジェクトでは、新規モジュール作成の効率化により大きな時間節約になります。
今後も、AIツールとプラグイン設計のベストプラクティスを積極的に取り入れ、プロジェクト全体の効率化と品質向上、そして継続的な改善を推進していきます。
Xtoneでは、エンタメやIoT、モビリティなど多様な領域でBtoC向けサービスのFlutter,iOS,Androidアプリを開発しています。
私たちと共に、新しい技術へ挑戦しながら価値あるプロダクト開発に取り組む仲間を募集しています。興味ある方はぜひ採用ページをご覧ください!