いくつかやり方があり、それぞれメリデメがあるようなのでのでまとめておこうと思います。
nowinandroidがComposite Buildを導入したことで、話題になりました。
ちょっと調べてみたんですが、これが最強というのは見つけることができませんでした。かなりプロジェクト規模によって異なってきそうです。メリデメ見ながら採用していく必要がありそうです。
ここではある程度大規模だが、Androidアプリでビルドエンジニアを雇うほどではないという想定をしています。
以下で色々試行錯誤していました。
どこにビルドのコードを置くのか?
buildSrc
プロジェクト/buildSrcにコードを書く方法。
メリット
色々魔法をGradleが使ってくれるので簡単
小さなプロジェクトではこれでも大丈夫。
プラグインとextension以外が公開できない。Kotlinのextension functionを置いて利用側で使ったりできる。
デメリット
ビルドのホットパス = 非常に実行されるビルド手順になるので無視できないコストが発生する。
様々な魔法によるデメリットが発生する。例えば、ビルドのさまざまなものが集められるので、些細な変更に見えてもビルド全体のクラスパスの変更をもたらすことで全体のモジュールを無効にして良くない開発者体験をもたらすことがある。1
buildSrc/にテストを置くとassembleDebugなどをしたときに勝手にテストが実行されてしまう。1
Build Logic
settings.gradleでincludeBuild()を書くことで、プロジェクト内などに置いたGradleモジュールをbuild-logicなどの名前でビルド用のモジュールとして読み込む方法。
https://docs.gradle.org/current/userguide/composite_builds.html#settings_defined_composite
メリット
IDEでそのモジュールだけを開くことができるのでビルドエンジニアの生産性の向上。
buildSrcによる様々な魔法を使わない。
now in androidが採用したので未来があるかもしれない。
デメリット
buildSrcに比べやや複雑。
Gradleの早い段階で依存関係の解決を行う必要があり、それはシングルスレッドになっているのでビルドが遅くなる。大きなモジュールで33%遅くなったという報告がある。1
プラグインとそのExtension以外のコード実装はそのプラグインをユーザーに公開できない。つまり、Kotlinのextension functionを置いて、利用側(build.gradleで)で使ったりできない。 (できたら教えて下さい。)
個別でpluginのモジュールとしてmaven publishする方法
メリット
AGPなどと同じ仕組みで、コンパイルされているためビルド速度が早くなる。
デメリット
includeBuildで統合してビルドするのかを切り替えできる仕組みを作る必要がある。そうでない場合はpluginの変更した内容を変更をリリース後にしか反映することができない。バージョン管理コストが大変。
どうやってまとめるコードを書くか?
Precompiled script plugin
これはbuildSrcでも、Composite Buildでも使える。
java-library-convention.gradle.ktsなどでいつもと同じような形でプラグインのコードを書いておくと、
plugins {
`java-library-convention`
}
などで簡単にプラグインとして適応できる。
メリット
ボイラープレートを生成してくれるので楽。
Gradleのコードをいつもと同じように書ける。
デメリット
ビルド速度に影響が出る。2
カスタマイズした設定をするには Project.afterEvaluate{}かandroidComponents.finalizeDsl{}を使う必要がある。 3
普通にプラグインに書く
メリット
Precompiled script pluginに比べ高速2
デメリット
少しPrecompiled script pluginに比べると1プラグイン毎に複数ファイルをいじる必要があるので、大変。
書き方が普段のGradleとは違うので、慣れる必要がある。
カスタマイズした設定をするには Project.afterEvaluate{}かandroidComponents.finalizeDsl{}を使う必要がある。そのため、DSLを使っている場合、そのDSLからデータを一度ビルドして、そこからデータを利用して行く必要がある。androidComponents.finalizeDsl{}の場合Kotlin MPPではどうするのかという課題がある。Project.afterEvaluate{}ではどこが先にセットアップしたのか、などで問題が発生する可能性がある。 3
プラグインで設定したGradleのExtensionでAGPのapplyなどをしちゃうパターン
Composite Build、build-logicでGradle Pluginを使う場合はPlugin自体とExtension以外が使う側のbuild.gradleに公開されていないようなんですが、一応使う側のbuild.gradleにそのExtensionは公開されているので、Extensionに色々コードを書くことで、そこでAGPのセットアップなどをしちゃうという方法です。
メリット
extensionを使う側のプロジェクトで使ったタイミングで、初期化できるので、Project.afterEvaluate{}かandroidComponents.finalizeDsl{}を使う必要がないのである意味クリーンです。
デメリット
この方法は単に自分が思いついただけで、かなりハックです。
extensionの中でAndroid Gradle Pluginなどを適応した場合、implementation
などがUnresolved referenceになります。そのためPluginのapply()のタイミングで他のプラグインはapplyする必要がありそうです。つまり結局、Gradle Plugin毎にプラグインを分ける必要が出てきて、extensionの意味がなくなります。
どれを採用するの?
個人的な理想
本当の理想のプラグインを使う側のコードは以下のような形でDSLでセットアップできるというものでした。
この方法は順序を気にする必要がなく、タイプセーフで、セットアップが楽でした。
id("com.example")
example {
android {
hilt()
compose()
}
}
がこれは以下のような課題があります。
カスタマイズした設定をするには Project.afterEvaluate{}かandroidComponents.finalizeDsl{}を使う必要がある。そのため、DSLを使っている場合、そのDSLからデータを一度ビルドして、そこからデータを利用して行く必要がある。androidComponents.finalizeDsl{}の場合Kotlin MPPではどうするのかという課題がある。Project.afterEvaluate{}ではどこが先にセットアップしたのか、などで問題が発生する可能性がある。
このアプローチは実際に以下のプラグインで行われているアプローチになり実績はありますが大変そうではあります。
現実的に何を選ぶか
どこにコードを置くのかという観点でいうと、他のやり方に問題があるため、Composite Buildによる方法に一番未来を感じています。
その中でいくつかやり方があるので、それを紹介します。
1. Composite buildで複数pluginを使ってうまく構成する
このやり方はnow in androidのやり方にちょっと工夫を加えるものになります。
このやり方はプラグインを複数定義して、うまく実装してくのが基本になっていくと思われますが、プラグイン同士の依存関係が分かりにくいというデメリットを感じていました。例えば nowinandroid.android.feature
を適応したら、nowinandroid.android.library
のpluginの適応はいらないのか。。。?など
必要なプラグインを上位のグループとしてドットでつないで書くという方法を行うことで、依存忘れを防ぐという方法が今のところは良いかなと思っています。 (目視確認が必要になりますが。)
plugins {
id("com.example.primitive.android") // com.android.libraryを適応
// 以下を適応するにはcom.example.primitive.androidが必要!
id("com.example.primitive.android.compose")
id("com.example.primitive.android.hilt")
id("com.example.primitive.mpp")
}
で、以下のようにいつも使うやつはまとめてかけたりとかもできるみたいな雰囲気です。
plugins {
id("com.example.convension.feature")
}
2. Composite buildで用途ごとに一つ一つpluginを作って中でKotlin DSLを使う
Gradle Pluginを使う側でDSLを使うと複雑化する(afterEvaluate{}などを使う必要が出てくる)ので、中でだけKotlinのDSLを使うパターンです。DSLのメリットが享受できる代わりに、core-uiみたいなモジュールを作るときにプラグインを作成し、それを登録するという、ビルドファイルを2ファイル変更する必要が出てきます。
@Suppress("unused")
class CoreUiPlugin : Plugin<Project> {
override fun apply(project: Project) {
example {
android {
hilt()
compose()
}
}
}
}
register("coreUi") {
id = "example.dsl"
implementationClass = "com.example.project.template.dsl.CoreUiPlugin"
}
plugins {
id("com.example.core.ui")
}
まとめ
最後に書いた2つの方法が今の所は現実的かなと思いましたが、なんかめんどくさく考えすぎじゃない?何かこうすればいいじゃんとか、ここ間違ってるからできるよ!とかあればぜひ教えて下さい。