Blog Infos
Author
Published
Topics
, , , ,
Published

Inthis article I’d like to describe how you can get rid of boilerplate code in your build.gradle files in multimodule project with the help of the Convention plugins.

TL;DR — with the help of convention plugins you can refactor your Gradle file from this state (71 lines):

plugins {
alias(libs.plugins.kotlinMultiplatform)
alias(libs.plugins.androidLibrary)
id("maven-publish")
}
group = "com.mikhailovskii.kmp"
version = System.getenv("LIBRARY_VERSION") ?: libs.versions.pluginVersion.get()
kotlin {
androidTarget {
compilations.all { publishLibraryVariants("release", "debug") }
}
listOf(
iosX64(),
iosArm64(),
iosSimulatorArm64()
).forEach {
it.binaries.framework {
baseName = "in-app-review-kmp-rustore"
isStatic = true
}
}
jvmToolchain(17)
sourceSets {
androidMain.dependencies {
implementation(libs.rustore.review)
}
commonMain.dependencies {
api(projects.inAppReviewKmp)
}
}
}
android {
namespace = "com.mikhailovskii.inappreview"
compileSdk = 34
defaultConfig {
minSdk = 26
}
}
publishing {
publications {
matching {
return@matching it.name in listOf("iosArm64", "iosX64", "kotlinMultiplatform")
}.all {
tasks.withType<AbstractPublishToMaven>()
.matching { it.publication == this@all }
.configureEach { onlyIf { findProperty("isMainHost") == "true" } }
}
}
repositories {
maven {
url = uri("https://maven.pkg.github.com/SergeiMikhailovskii/kmp-app-review")
credentials {
username = System.getenv("GITHUB_USER")
password = System.getenv("GITHUB_API_KEY")
}
}
}
}
tasks.register("buildAndPublish", DefaultTask::class) {
dependsOn("build")
dependsOn("publish")
tasks.findByPath("publish")?.mustRunAfter("build")
}

To this (15 lines):

plugins {
id("com.mikhailovskii.kmp.module")
id("maven-publish")
}
kotlin {
sourceSets {
androidMain.dependencies {
implementation(libs.rustore.review)
}
commonMain.dependencies {
api(projects.inAppReviewKmp)
}
}
}

Interested? Before diving into the refactoring let me discuss the initial setup I have. I have a KMP library that can launch the in-app review in various Android and iOS stores. Since some stores implementations require additional SDKs I decided to make them as separate modules. So, the library contains multiple modules that have the same structure:

  1. Artifact group + version for building and publishing the module
  2. Targets setup
  3. Java setup
  4. Dependencies setup
  5. Android setup
  6. Publishing setup
  7. Wrapper task for building and publishing

Everything except the 4th point is the same across the modules, so we can extract it into the Convention Plugin. Probably, it’s a default Gradle Plugin that contains some “conventional setup” for some part of the logic in the .gradle file.

To start the implementation we need to create the module for the plugin. Gradle documentation advises us to implement composite-build for this case, so let’s do it. Declare a folder and name it in any way you want (it’s a common practice to name it build-logic). To make this project a subproject we need to add settings.gradle.kts file where the subproject’s structure will be described. Here’s the way how it looks like:

dependencyResolutionManagement {
@Suppress("UnstableApiUsage")
repositories {
google()
mavenCentral()
}
versionCatalogs {
create("libs") {
from(files("../gradle/libs.versions.toml"))
}
}
}
rootProject.name = "build-logic"
include(":convention")

There are repositories declared in this file that contain the dependencies used inside the subproject, specified path to the version catalog and included the module (convention) where the plugin is located.

Don’t forget to add the subproject into the main project — put the line includeBuild(“build-logic”) inside the pluginManagement block inside the root’s settings.gradle.kts.

Now let’s move to the module with the plugin. Here’s it’s build.gradle file:

plugins {
`kotlin-dsl`
}
gradlePlugin {
plugins {
register("com.mikhailovskii.kmp.KMPModuleConventionPlugin") {
id = "com.mikhailovskii.kmp.module"
implementationClass = "KMPModuleConventionPlugin"
}
}
}
dependencies {
compileOnly(libs.android.gradle)
compileOnly(libs.kotlin.gradle.plugin)
}

Plugin kotlin-dsl is applied to this module as it contains an extension to register the convention plugin we are going to implement. Then we register the convention plugin (specify it’s name, id, implementation class) and apply dependencies we need for the development of plugin. Since the plugin is used only during the compile time we declare the dependencies as compileOnly.

The implementation of the plugin is the following:

Job Offers

Job Offers

There are currently no vacancies.

OUR VIDEO RECOMMENDATION

,

Crash Course in building your First Gradle Plugin

A Gradle plugin is something that we use every day, but have you ever considered how they’re created? What’s behind the magic of the Kotlin DSLs provided by the plugins we use daily?
Watch Video

Crash Course in building your First Gradle Plugin

Iury Souza
Mostly Android things
Klarna

Crash Course in building your First Gradle Plugin

Iury Souza
Mostly Android thing ...
Klarna

Crash Course in building your First Gradle Plugin

Iury Souza
Mostly Android things
Klarna

Jobs

class KMPModuleConventionPlugin : Plugin<Project> {
override fun apply(target: Project) {
with(target) {
with(pluginManager) {
apply(extensions.getPluginId("androidLibrary"))
apply(extensions.getPluginId("kotlinMultiplatform"))
apply("maven-publish")
}
group = "com.mikhailovskii.kmp"
version = System.getenv("LIBRARY_VERSION") ?: extensions.getVersion("pluginVersion")
extensions.configure<KotlinMultiplatformExtension> {
androidTarget { publishLibraryVariants("release", "debug") }
iosX64()
iosArm64()
iosSimulatorArm64()
applyDefaultHierarchyTemplate()
jvmToolchain(17)
}
extensions.configure<LibraryExtension> {
namespace = "com.mikhailovskii.inappreview"
compileSdk = 34
defaultConfig {
minSdk = 21
}
compileOptions {
sourceCompatibility = JavaVersion.VERSION_17
targetCompatibility = JavaVersion.VERSION_17
}
}
extensions.configure<PublishingExtension> {
publications {
matching {
return@matching it.name in listOf(
"iosArm64",
"iosX64",
"kotlinMultiplatform"
)
}.all {
tasks.withType<AbstractPublishToMaven>()
.matching { it.publication == this@all }
.configureEach { onlyIf { findProperty("isMainHost") == "true" } }
}
}
repositories {
maven {
url = uri("https://maven.pkg.github.com/SergeiMikhailovskii/kmp-app-review")
credentials {
username = System.getenv("GITHUB_USER")
password = System.getenv("GITHUB_API_KEY")
}
}
}
}
tasks.register("buildAndPublish", DefaultTask::class) {
dependsOn("build")
dependsOn("publish")
tasks.findByPath("publish")?.mustRunAfter("build")
}
}
}
}

The new plugin should be a subclass of the Project. And inside the body of the apply method we add all the logic we need. So, as you can see here we apply default plugins, setup targets, Java and Android, publishing and even create a new task.

I want to highlight that it’s more convenient to create extensions to get the pluginId/version you need from the version catalog. This is the way how these extensions look like:

internal fun ExtensionContainer.getPluginId(alias: String): String =
getByType<VersionCatalogsExtension>().named("libs").findPlugin(alias).get().get().pluginId
internal fun ExtensionContainer.getVersion(alias: String): String =
getByType<VersionCatalogsExtension>().named("libs").findVersion(alias).get().toString()
view raw Extensions.kt hosted with ❤ by GitHub

After everything is done we just add the plugin into the modules we need and cleanup the duplicated code from the build.gradle file since this logic is encapsulated inside the plugin:

plugins {
id("com.mikhailovskii.kmp.module")
id("maven-publish")
}
kotlin {
sourceSets {
androidMain.dependencies {
implementation(libs.rustore.review)
}
commonMain.dependencies {
api(projects.inAppReviewKmp)
}
}
}

The full implementation can be found inside the project.

Thank you for reading! Feel free to ask questions and leave the feedback in comments or Linkedin.

This article is previously published on proandroiddev.com

YOU MAY BE INTERESTED IN

YOU MAY BE INTERESTED IN

blog
Managing dependencies in a single module project is pretty simple, but when you start…
READ MORE
blog
The LookaheadScope (replaced by the previous LookaheadLayout) is a new experimental API in Jetpack…
READ MORE
blog
With Compose Multiplatform 1.6, Jetbrains finally provides an official solution to declare string resources…
READ MORE
blog

Running Instrumented Tests in a Gradle task

During the latest Google I/O, a lot of great new technologies were shown. The…
READ MORE

Leave a Reply

Your email address will not be published. Required fields are marked *

Fill out this field
Fill out this field
Please enter a valid email address.

Menu