🎄

Gradle Pluginの作成のすゝめ

2023/12/06に公開

はじめに

この記事は、Money Forward Engineering Advent Calendar 2023 6日目の投稿です。

こんにちは。株式会社マネーフォワードでモバイルエンジニア(時々バックエンドエンジニア)のbigbackboomです。今日はGradleのPluginについて話したいと思います。

Androidでは長らくGradleというビルドツールが使われています。

2015年より以前はGradleの設定記述はGroovyと言われる言語のみが使われており、その独自性から「なんとなく」で書いてしまうことが多かったと思います。

しかし、現在はAndroidの正式採用言語のKotlinのKTSが使えるようになり、慣れ親しんだKotlinの構文を使いつつGradleの設定を記述することができるようになりました。

問題点

KTSが利用できるようになってから、Gradleの記述はある程度分かりやすくなりましたが、それでも解決できていない問題があります。それが、ビルドスクリプトの冗長性です。

KTSになってもGradleにはDSL特有の特殊な記述が多く分かりにくいのと、Androidのマルチモジュール化により共通の設定を各モジュールに設定したいなどのニーズから、同様のビルドスクリプトをモジュールごとに何度も記述してしまう場合があります。

これはDRYDo not Repeat Yourself)の原則の観点からも、スマートとはいえません。

本記事では、繰り返し記述を避けるための方法として、GradleのPluginを作成の仕方を解説したいと思います

なぜGradle Pluginなのか?

リファクタの手段として、共通の処理を記述する方法はいくつかあります。最もポピュラーなのは、buildSrcというディレクトリを作成し共通処理をそこに定義する方法です。そこで作成したコードはGradleが自動で検知して、ビルドを行ってくれるので、それをKTSから利用できます。

対して、Gradle Pluginでは以下のようなメリット・デメリットがあります。

メリット

  • ライブラリとして、外部公開や内部公開する際に利用者側がプロジェクトに導入しやすい。
  • Plugin毎に機能をまとめやすくモジュール化しやすい
  • 社内の設定ルールなどを統一化・強制化しやすい。

デメリット

  • Pluginの作成に知識が必要
  • buildSrcに共通処理を記述するよりも、準備や設定が必要。

buildSrcに共通処理の記述か、Gradle Pluginかどちらを使うかはプロジェクトの運用方針にもよりますが、マイクロサービス化でアプリも細かくサービスを分割している場合は、Gradle Pluginを使った方が内部的な共通化がしやすいと思います。

準備

Plugin用プロジェクトの設置

別プロジェクトの設置について

今回はnowinandroidでも利用されている、build-logicという別プロジェクトを設置する方法を利用します。

こちらの方法は、「別プロジェクト」という言葉を使った通り、Androidのプロジェクトに完全独立したbuild-logicというプロジェクトを設置する方法となっています。

この方法は以下のようなメリット・デメリットが存在します

メリット

  • コードに変更を加えても、buildSrcなどと違い、ビルドキャッシュが使われる。
  • ビルドスクリプトと、アプリのコードを分離できる。

デメリット

  • build-logic内部で定義したメソッドのコードを、Androidプロジェクト側では使えない。
  • クリーンビルドが遅くなる

※ ビルド速度についてはこちらの記事をご一読ください。

プロジェクトの作成

それでは実際に実装をしていきます。まずは、適当なAndroidプロジェクトを作成して、プロジェクト直下に以下のようなディレクトリを設置してください

project/
  ├ build-logic/
  │   ├ convention/
  │   ├ gradle.properties
  │   └ settings.gradle.kts
  ├ gradle/
  │   ├ wrapper/
  │   └ libs.versions.toml // なければ作成
  └ settings.gradle.kts

project/build-logic/settings.gradle.kts

dependencyResolutionManagement {
  versionCatalogs {
    create("libs") {
      from(files("../gradle/libs.versions.toml"))
    }
  }
}

rootProject.name = "build-logic"
include(":convention")

project/settings.gradle.kts

  • includeBuildを追加して、build-logicをプロジェクトに含めます。
pluginManagement {
  includeBuild("build-logic")
}

Pluginの実装

  • 以下のような構成で、ファイルを作成する。
project/
  └ build-logic/
      ├ convention/
      │   └ src/
      │     ├ main/
      │     │   kotlin/
      │     │    ├ com/bigbackboom/test/build/logic 
      │     │    │  └ AndroidSetting.kt                  // 本項で解説
      │     │    ├ AndroidApplicationConventionPlugin.kt // 本項で解説
      │     │    └ AndroidLibraryConventionPlugin.kt     // 本項で解説
      │     └ build.gradle.kts                         // 本項で解説
      ├ gradle.properties
      └ settings.gradle.kts

build.gradle.ktsの作成

build.gradle.ktsまず用意することで、src配下にKotlinのソースが存在することが認識され始めます。

記述内容は、Androidでの開発同様Javaのバージョンや、Plugin内部で使いたいDependenciesなどを記述することになります。

また後ほど、こちらのファイルに作成したPluginを定義します。

plugins {
  `kotlin-dsl`
}

group = "com.testplugin.buildlogic"

java {
  sourceCompatibility = JavaVersion.VERSION_17
  targetCompatibility = JavaVersion.VERSION_17
}

tasks.withType<org.jetbrains.kotlin.gradle.tasks.KotlinCompile>().configureEach {
  kotlinOptions {
    jvmTarget = JavaVersion.VERSION_17.toString()
  }
}

allprojects {
  repositories {
    google()
    mavenCentral()
    gradlePluginPortal()
  }
}

// libs.versions.tomlに追記
dependencies {
  compileOnly(libs.android.gradle)
  compileOnly(libs.kotlin.gradle)
}

libs.versions.toml


[versions]
android-gradle-plugin = "8.1.4"

# Kotlin
kotlin = "1.9.10"

[libraries]

android-gradle = { group = "com.android.tools.build", name = "gradle", version.ref = "android-gradle-plugin" }
kotlin-gradle = { group = "org.jetbrains.kotlin", name = "kotlin-gradle-plugin", version.ref = "kotlin" }

[plugins]

この時点でGradle Syncをすると以下のような表示になると思います
directoryimage1.png

Pluginを作成していく

ようやくプロジェクトに必要な構成を作成することができました。

ここから実際のPlPlugin成を行なっていきます。まずは、Androidの基礎設定をしていくメソッド作成を行います。今回は、全部記述すると長くなってしまうので、設定を絞って記述します。。

AndroidSetting.kt

internal fun Project.configureAndroid(
  commonExtension: CommonExtension<*, *, *, *, *>
) {
  commonExtension.apply {
    compileSdk = 34

    defaultConfig {
      minSdk = 21
    }

    compileOptions {
      // Up to Java 11 APIs are available through desugaring
      // https://developer.android.com/studio/write/java11-minimal-support-table
      sourceCompatibility = JavaVersion.VERSION_17
      targetCompatibility = JavaVersion.VERSION_17
    }
  }
}

次にPluginの入り口部分となる、クラスを2種類作成します。一つは、AndroidのApplicationモジュール設定用、もう一つはAndroライブラリモジュール設定用となります。

二つに分ける理由として、:appモジュールとライブラリモジュールでは設定できる内容が若干違うため、入り口を分けることで共通化できる部分はメソッドで共通化し、そのモジュール特有の設定はそれぞれで行うような形にしていきます。

AndroidApplicationConventionPlugin.kt

class AndroidApplicationConventionPlugin : Plugin<Project> {
  override fun apply(target: Project) {
    extensions.getByType(ApplicationExtension::class.java).apply {
      defaultConfig.targetSdk = 34
      configureAndroid(this)
    }
  }
}

AndroidLibraryConventionPlugin.kt

class AndroidLibraryConventionPlugin : Plugin<Project> {
  override fun apply(target: Project) {
    with(target) {
      extensions.getByType(LibraryExtension::class.java).apply {
        configureAndroid(this)
      }
    }
  }
}

以上で、Pluginの作成が完了しました。あとはこれを利用できるようにするための、設定を追記していきます。

Pluginを定義する

前述もしたとおり、Pluginが存在することをbuild.gradle.ktsに書き込みます。そうすることで、外部から該当のPluginを呼び出し、利用することができるようになります。

build-logic/convention/build.gradle.kts

// 以下を追記する

gradlePlugin {
  plugins {

    register("AndroidApplication") {
      id = "test.android.app" // こちらはなんでもいい
      implementationClass = "AndroidApplicationConventionPlugin" // AndroidApplicationConventionPlugin.ktのクラス名
    }   

    register("AndroidLibrary") {
      id = "test.android.library" // こちらもなんでもいい
      implementationClass = "AndroidLibraryComposeConventionPlugin" // AndroidLibraryConventionPlugin.ktのクラス名
    }
  }
}

これで二つのPlugin、test.android.apptest.android.libraryがあることが定義されました。

あとは利用したいモジュールのGradle設定側で記述を追加するだけです!

Pluginを利用する

まずは、以前作ったlibs.versions.tomlにPluginを定義してあげましょう。こちらは記述をしなくてもPlugin自体の利用は可能ですが、libs.versions.tomlにまとめておいた方が便利なのでこちらに記述します。

libs.versions.toml

[plugins]
test-plugin-android-application = { id = "test.android.app", version = "unspecified" } // 「Pluginを定義する」項で定義したPluginのIDを記述
test-plugin-android-library = { id = "test.android.library", version = "unspecified" } // 「Pluginを定義する」項で定義したPluginのIDを記述

次にプロジェクトレベルのbuild.gradle.kts(プロジェクト名)に以下を定義します。

こちらの定義は、Pluginの利用を宣言しつつも、apply false を設定することで、プロジェクト全体でのPluginバージョンを固定する設定となっています。この設定を忘れると、モジュールレベルでPluginの利用の連言をしたときにエラーになってしまいます。

build.gradle.kts(プロジェクト名)


plugins {
  alias(libs.plugins.test.plugin.android.application) apply false // libs.versions.tomlのキー(test-plugin-android-application)から自動生成
  alias(libs.plugins.test.plugin.android.library) apply false // libs.versions.tomlのキー(test-plugin-android-library)から自動生成
}

最後に、モジュールレベルでPluginの利用を宣言します。今回は:appモジュールを例とします。

plugins {
  alias(libs.plugins.test.plugin.android.application)
}

android { 
  ...
  
  // targetSdk = 34 こちらはPluginで設定済みのためコメントアウト

  defaultConfig {
    ...

    // minSdk = 26 こちらはPluginで設定済みのコメントアウト 
  }

  compileOptions {
    // sourceCompatibility = JavaVersion.VERSION_17 こちらはPluginで設定済みのコメントアウト
    // targetCompatibility = JavaVersion.VERSION_17 こちらはPluginで設定済みのコメントアウト
  }
}

以上で準備完了です。本来そのまま設定に書いていたAndroidにまつわる設定を、Plugin側で設定してくれるようになりました。

上記の例では:appモジュールに限定していますが、ライブラリーモジュールで利用すれば、モジュール追加ごとに面倒な設定をせずににPluginの利用を宣言するだけで、設定が完了となります。

まとめ

AndroidのGradleはKotlinが記述できるようになり書きやすくなりましたが、まだまだKTSに移行しただけではリファクタリングできる項目が多々あります。

その中で、今回紹介したPlugin作成はコードの冗長性を解決するための手段として非常に有効で、繰り返し記述が避けられるためミスなく新規のモジュール追加などがしやすくなります。

もし、Gradleの記述が散らかっていると感じる方は是非一度試してください。

GitHubで編集を提案

Discussion