Today we will figure out how to manage your build scripts more efficiently, avoid boilerplate and code duplication. We will cover two major topics: dependencies management and common configuration control.
This article was inspired by the Now in Android app. You will notice during reading this article that many parts of our examples will appear very similar. We will follow the main idea and add some additional comments and explanations. I hope you enjoy the article, so let’s begin.

Create a new project
We will start by creating a new project and analyzing what we have in our build files. The picture can be slightly different, but you have probably already seen something like the following code snippet in your application build file. The plugins
section, which contains information about using plugins, is nothing special, just a compact part:
plugins { | |
id 'com.android.application' | |
id 'org.jetbrains.kotlin.android' | |
} |
/composite-build-example/app/build.gradle
The android
section contains noticeably more information than the plugins
section:
android { | |
namespace 'com.touchlane.compositebuild.example' | |
compileSdk 33 | |
defaultConfig { | |
applicationId "com.touchlane.compositebuild.example" | |
minSdk 24 | |
targetSdk 33 | |
versionCode 1 | |
versionName "1.0" | |
testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner" | |
vectorDrawables { | |
useSupportLibrary true | |
} | |
} | |
buildTypes { | |
release { | |
minifyEnabled false | |
proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro' | |
} | |
} | |
compileOptions { | |
sourceCompatibility JavaVersion.VERSION_1_8 | |
targetCompatibility JavaVersion.VERSION_1_8 | |
} | |
kotlinOptions { | |
jvmTarget = '1.8' | |
} | |
buildFeatures { | |
compose true | |
} | |
composeOptions { | |
kotlinCompilerExtensionVersion '1.2.0' | |
} | |
packagingOptions { | |
resources { | |
excludes += '/META-INF/{AL2.0,LGPL2.1}' | |
} | |
} | |
} |
The dependencies
section is the most changeable. In the starter project, it is somewhere between the plugins
and android
parts:
dependencies { | |
implementation 'androidx.core:core-ktx:1.9.0' | |
implementation 'androidx.lifecycle:lifecycle-runtime-ktx:2.5.1' | |
implementation 'androidx.activity:activity-compose:1.6.1' | |
implementation "androidx.compose.ui:ui:$compose_version" | |
implementation "androidx.compose.ui:ui-tooling-preview:$compose_version" | |
implementation 'androidx.compose.material3:material3:1.0.0' | |
testImplementation 'junit:junit:4.13.2' | |
androidTestImplementation 'androidx.test.ext:junit:1.1.3' | |
androidTestImplementation 'androidx.test.espresso:espresso-core:3.4.0' | |
androidTestImplementation "androidx.compose.ui:ui-test-junit4:$compose_version" | |
debugImplementation "androidx.compose.ui:ui-tooling:$compose_version" | |
debugImplementation "androidx.compose.ui:ui-test-manifest:$compose_version" | |
} |
/composite-build-example/app/build.gradle
In general, there aren’t any problems with the build scripts above unless you decide to add a new module. What problems will we face when we add a new module? We should have a very close android
section, as well as a similar dependencies
section. Sure, dependencies wouldn’t be the same, but usually, we will have a pretty extensive intersection of dependencies in our project modules. But size in the dependencies
section isn’t a problem, whereas the problem is managing library versions across all modules.
In cases when you have more than one module, it is highly recommended to use a version catalog.
Version catalog
A version catalog is a list of dependencies, represented as dependency coordinates, that a user can pick from when declaring dependencies in a build script.
Using of version catalog gives us a few very important advantages:
- Type-safe accessors so that we can easily add dependencies with autocompletion in the IDE.
- It is a central place to declare a version of a dependency and to make sure that a change to that version applies to every subproject.
- Catalogs can declare dependency bundles, which are “groups of dependencies” that are commonly used together.
- Catalogs can separate the group and name of a dependency from its actual version and use version references instead, making it possible to share a version declaration between multiple dependencies.
We will not go into detail in this section because there are a lot of very detailed articles, such as this one. Instead, we will briefly cover the most relevant steps.
So, let’s add a version catalog to our project. First of all, we need to create a TOML file under the gradle
directory and fill it with the required dependencies:

If a
libs.versions.toml
file is found in thegradle
subdirectory of the root build, then a catalog will be automatically declared with the contents of this file.
If you want to place it in another place or name it differently, follow the official instructions.
The structure of TOML file is very simple and consists of 4 major sections:
- the
[versions]
section is used to declare versions which can be referenced by dependencies - the
[libraries]
section is used to declare the aliases to coordinates - the
[bundles]
section is used to declare dependency bundles - the
[plugins]
section is used to declare plugins
[versions] | |
androidGradlePlugin = "7.3.1" | |
androidxActivity = "1.6.1" | |
androidxComposeBom = "2022.10.00" | |
kotlin = "1.7.20" | |
[libraries] | |
androidx-activity-compose = { group = "androidx.activity", name = "activity-compose", version.ref = "androidxActivity" } | |
androidx-compose-bom = { group = "androidx.compose", name = "compose-bom", version.ref = "androidxComposeBom" } | |
androidx-compose-material3 = { group = "androidx.compose.material3", name = "material3" } | |
androidx-compose-ui = { group = "androidx.compose.ui", name = "ui" } | |
[bundles] | |
compose = ["androidx-activity-compose", "androidx-compose-material3", "androidx-compose-ui"] | |
[plugins] | |
android-application = { id = "com.android.application", version.ref = "androidGradlePlugin" } | |
android-library = { id = "com.android.library", version.ref = "androidGradlePlugin" } | |
kotlin-android = { id = "org.jetbrains.kotlin.android", version.ref = "kotlin" } |
/composite-build-example/gradle/libs.versions.toml
In addition, to take advantage of all the features of autocompletion, we will migrate all our build script files to Gradle Kotlin DSL.
After changing our app module dependencies
section will look as follows:
dependencies { | |
val composeBom = platform(libs.androidx.compose.bom) | |
implementation(composeBom) | |
implementation(libs.androidx.activity.compose) | |
implementation(libs.androidx.compose.material3) | |
implementation(libs.androidx.compose.ui) | |
implementation(libs.androidx.compose.ui.tooling.preview) | |
implementation(libs.androidx.core.ktx) | |
testImplementation(libs.junit4) | |
androidTestImplementation(composeBom) | |
androidTestImplementation(libs.androidx.test.ext) | |
androidTestImplementation(libs.androidx.test.espresso.core) | |
androidTestImplementation(libs.androidx.compose.ui.test) | |
debugImplementation(libs.androidx.compose.ui.tooling) | |
debugImplementation(libs.androidx.compose.ui.testManifest) | |
} |
/composite-build-example/app/build.gradle.kts
Now the dependencies section looks much better. If we add a new module, for instance :core:ui
, we wouldn’t need to control library versions separately. All that we need to do is update the versions in our libs.versions.toml
file.
When we will start using TOML files, Android Studio stopped showing us expired dependencies. The solution here is to start using one of the tools to control dependencies updates such as Renovate or Version catalog update plugin.
Composite build
Let’s go back to our android
section and analyze what we have in it.
android { | |
namespace 'com.touchlane.compositebuild.example' | |
compileSdk 33 | |
defaultConfig { | |
applicationId "com.touchlane.compositebuild.example" | |
minSdk 24 | |
targetSdk 33 | |
versionCode 1 | |
versionName "1.0" | |
testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner" | |
vectorDrawables { | |
useSupportLibrary true | |
} | |
} | |
buildTypes { | |
release { | |
minifyEnabled false | |
proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro' | |
} | |
} | |
compileOptions { | |
sourceCompatibility JavaVersion.VERSION_1_8 | |
targetCompatibility JavaVersion.VERSION_1_8 | |
} | |
kotlinOptions { | |
jvmTarget = '1.8' | |
} | |
buildFeatures { | |
compose true | |
} | |
composeOptions { | |
kotlinCompilerExtensionVersion '1.2.0' | |
} | |
packagingOptions { | |
resources { | |
excludes += '/META-INF/{AL2.0,LGPL2.1}' | |
} | |
} | |
} |
/composite-build-example/app/build.gradle.kts
n general, we can group all sections by two purposes: android-specific sections and 3-rd party sections. The android-specific sections can be divided into two categories — library and application. For instance, we can refer to 3-rd party specific sections Kotlin related content. The majority of the sections from the app-level android
section will be copied into your newly created modules. The same as with the dependencies
section you have to control all similar sections across the entire project.
One of the most effective solutions is to start using a composite build approach.
A composite build is simply a build that includes other builds. In many ways a composite build is similar to a Gradle multi-project build, except that instead of including single projects, complete builds are included.
Why not a buildSrc
? The answer is simple and based on official documentation:
A change in
buildSrc
causes the whole project to become out-of-date.
The Gradle team works on this problem and has the plan to make buildSrc an included build, but now it isn’t.
First of all, we need to create a new project inside our main project. Let’s name it build-logic
:

The core structure should contain settings.gragle.kts
file:
dependencyResolutionManagement { | |
repositories { | |
google() | |
mavenCentral() | |
} | |
versionCatalogs { | |
create("libs") { | |
from(files("../gradle/libs.versions.toml")) | |
} | |
} | |
} | |
rootProject.name = "build-logic" | |
include(":convention") |
/composite-build-example/build-logic/settings.gradle.kts
In our project, we will use the same TOML file as in our main project. To avoid duplication we can specify a direct path to the TOML file and use it. Additionally, we need a module to put all our logic. Let’s call it a convention.
plugins { | |
`kotlin-dsl` | |
} | |
group = "com.touchlane.compositebuild.example.buildlogic" | |
java { | |
sourceCompatibility = JavaVersion.VERSION_1_8 | |
targetCompatibility = JavaVersion.VERSION_1_8 | |
} | |
dependencies { | |
compileOnly(libs.android.gradlePlugin) | |
compileOnly(libs.kotlin.gradlePlugin) | |
} | |
gradlePlugin { | |
plugins { | |
//Here is the place to register plugins | |
} | |
} |
/composite-build-example/build-logic/convention/build.gradle.kts
The build.gradle.kts
looks pretty simple with minimal required parameters. We are interested in the plugins
section under the root gradlePlugin
section. It’s the place where we register our custom plugins. The main idea is to move common parts from application build.gradle.kts
to a specific plugin. This approach helps us achieve a single source of truth for our modules.
Following the single responsibility principle, we will split logic into very small atomic groups that will cover specific cases.
Job Offers
Android application plugin
The first plugin will help us reduce code related to android application logic e.g. minSdk
, compileSdk
, etc. Firstly, we need to create a plugin class:
class AndroidApplicationConventionPlugin : Plugin<Project> { | |
override fun apply(target: Project) { | |
with(target) { | |
with(pluginManager) { | |
apply("com.android.application") | |
} | |
extensions.configure<ApplicationExtension> { | |
configureAndroidCommon(this) | |
configureAndroidApplication(this) | |
} | |
} | |
} | |
} |
/composite-build-example/build-logic/convention/src/main/kotlin/…
So, let’s take a look at what we have. We have the plugin that applied to the project. In the apply section, we have 2 subsections. The first one is:
with(pluginManager) { | |
apply("com.android.application") | |
} |
/composite-build-example/build-logic/convention/src/main/kotlin/…
The section above applies to the Android application plugin. Remember this trick because later, in our app’s build.gradle.kts
file we’ll replace the Android application plugin with our customized AndroidApplicationConventionPlugin
. The second section is:
extensions.configure<ApplicationExtension> { | |
configureAndroidCommon(this) | |
configureAndroidApplication(this) | |
} |
/composite-build-example/build-logic/convention/src/main/kotlin/…
Notice that here we have a deal with ApplicationExtension
. This means that this plugin will work only in an Android application module. In other words, we have to use this plugin only in combination with the com.android.application
plugin. You might notice that we have 2 very similar methods: configureAndroidCommon
and configureAndroidApplication
. The reason is that ApplicationExtension
is compatible only with com.android.application
, but we also need to manage the com.android.library
plugin. In cases where we deal with com.android.library
we will make a deal with LibraryExtension
. The fact is that ApplicationExtension
and LibraryExtension
have slightly different interfaces but they both have a common parent — CommonExtension
that contains a lot of fields but not all that we need.
For that reason, we split the codebase into 2 methods (actually 3 if we consider working with the com.android.library
plugin), configureAndroidCommon
:
internal fun Project.configureAndroidCommon( | |
extension: CommonExtension<*, *, *, *>, | |
) { | |
extension.apply { | |
compileSdk = 33 | |
defaultConfig { | |
minSdk = 24 | |
vectorDrawables { | |
useSupportLibrary = true | |
} | |
} | |
buildFeatures { | |
compose = true | |
aidl = false | |
buildConfig = false | |
renderScript = false | |
shaders = false | |
} | |
packagingOptions { | |
resources { | |
excludes += "/META-INF/{AL2.0,LGPL2.1}" | |
} | |
} | |
} | |
} |
/composite-build-example/build-logic/convention/src/main/kotlin/…
As you can see we moved a lot of stuff here. That means that you don’t need to have this code in your newly built and existing modules. Only targetSdk
is not available to configure our app, and the solution is the configureAndroidApplication
method:
internal fun Project.configureAndroidApplication( | |
extension: ApplicationExtension, | |
) { | |
extension.apply { | |
defaultConfig { | |
targetSdk = 33 | |
} | |
} | |
} |
The next step that we need to do is register our plugin in the build-logic
plugin. To do that we need to do the following:
gradlePlugin { | |
plugins { | |
register("androidApplication") { | |
id = "compositebuild.android.application" | |
implementationClass = "AndroidApplicationConventionPlugin" | |
} | |
} | |
} |
/composite-build-example/build-logic/convention/build.gradle.kts
Finally, we need to include our build-logic
project in the app build pipeline. To do that we need to add the following code to the application-level settings.gradle.kts
file:
pluginManagement { | |
includeBuild("build-logic") | |
repositories { | |
google() | |
mavenCentral() | |
gradlePluginPortal() | |
} | |
} | |
dependencyResolutionManagement { | |
repositoriesMode.set(RepositoriesMode.FAIL_ON_PROJECT_REPOS) | |
repositories { | |
google() | |
mavenCentral() | |
} | |
} | |
rootProject.name = "Composite Build Example" | |
include(":app") | |
include(":core") | |
include(":core:ui") |
/composite-build-example/settings.gradle.kts
Here will be the most interesting part — we’ll replace blocks in our application build.gradle.kts
file with our plugin and check how many lines of code we reduce.
//Before | |
plugins { | |
alias(libs.plugins.android.application) | |
alias(libs.plugins.kotlin.android) | |
} | |
//After | |
plugins { | |
id("compositebuild.android.application") | |
alias(libs.plugins.kotlin.android) | |
} |
/composite-build-example/app/build.gradle.kts
And the impressive part, we can reduce the android
section to these lines:
android { | |
namespace = "com.touchlane.compositebuild.example" | |
defaultConfig { | |
applicationId = "com.touchlane.compositebuild.example" | |
versionCode = 1 | |
versionName = "1.0" | |
testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner" | |
} | |
buildTypes { | |
getByName("release") { | |
isMinifyEnabled = false | |
proguardFiles( | |
getDefaultProguardFile("proguard-android-optimize.txt"), | |
"proguard-rules.pro" | |
) | |
} | |
} | |
compileOptions { | |
sourceCompatibility = JavaVersion.VERSION_1_8 | |
targetCompatibility = JavaVersion.VERSION_1_8 | |
} | |
kotlinOptions { | |
jvmTarget = "1.8" | |
} | |
composeOptions { | |
kotlinCompilerExtensionVersion = libs.versions.androidxComposeCompiler.get() | |
} | |
} |
/composite-build-example/app/build.gradle.kts
Kotlin plugin
Let’s define another plugin and move the Kotlin setup into it. We’ll start with creating a plugin class:
class AndroidKotlinConventionPlugin : Plugin<Project> { | |
override fun apply(target: Project) { | |
with(target) { | |
with(pluginManager) { | |
apply("org.jetbrains.kotlin.android") | |
} | |
extensions.findByType(ApplicationExtension::class.java)?.apply { | |
configureKotlin(this) | |
} | |
extensions.findByType(LibraryExtension::class.java)?.apply { | |
configureKotlin(this) | |
} | |
} | |
} | |
} |
Here you can see that it looks slightly different from our AndroidApplicationConventionPlugin
plugin. As we noticed before, the extension type depends on the type of plugin you use. For android applications we use the com.android.application
plugin and we have access to ApplicationExtension
. On the other hand in our modules, we use com.android.library
and in this case, we have access to LibraryExtension
. For that reason, we need to handle each case because highly likely we will use Kotlin in the whole project including modules.
Move all related logic into the configureKotlin
function:
internal fun Project.configureKotlin( | |
commonExtension: CommonExtension<*, *, *, *>, | |
) { | |
commonExtension.apply { | |
compileOptions { | |
sourceCompatibility = JavaVersion.VERSION_1_8 | |
targetCompatibility = JavaVersion.VERSION_1_8 | |
} | |
kotlinOptions { | |
jvmTarget = JavaVersion.VERSION_1_8.toString() | |
} | |
} | |
} | |
fun CommonExtension<*, *, *, *>.kotlinOptions(block: KotlinJvmOptions.() -> Unit) { | |
(this as ExtensionAware).extensions.configure("kotlinOptions", block) | |
} |
Register our plugin in our build-logic
project:
gradlePlugin { | |
plugins { | |
register("androidApplication") { | |
id = "compositebuild.android.application" | |
implementationClass = "AndroidApplicationConventionPlugin" | |
} | |
register("androidKotlin") { | |
id = "compositebuild.jetbrains.kotlin.android" | |
implementationClass = "AndroidKotlinConventionPlugin" | |
} | |
} | |
} |
And finally, replace all related code in our application build.gradle.kts
file:
//Before | |
plugins { | |
id("compositebuild.android.application") | |
alias(libs.plugins.kotlin.android) | |
} | |
//After | |
plugins { | |
id("compositebuild.android.application") | |
id("compositebuild.jetbrains.kotlin.android") | |
} |
The android
section will look like this:
android { | |
namespace = "com.touchlane.compositebuild.example" | |
defaultConfig { | |
applicationId = "com.touchlane.compositebuild.example" | |
versionCode = 1 | |
versionName = "1.0" | |
testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner" | |
} | |
buildTypes { | |
getByName("release") { | |
isMinifyEnabled = false | |
proguardFiles( | |
getDefaultProguardFile("proguard-android-optimize.txt"), | |
"proguard-rules.pro" | |
) | |
} | |
} | |
composeOptions { | |
kotlinCompilerExtensionVersion = libs.versions.androidxComposeCompiler.get() | |
} | |
} |
/composite-build-example/app/build.gradle.kts
Just compare it with our initial version. We reduced from around 47 lines of code in our android section to around 25! In addition, as a bonus, we create a single source of truth for our project.
Android library plugin
Now let’s create a module and compare the initial and final versions of the build.gradle.kts
file. Here is the initial build.gradle.kts
without dependencies:
plugins { | |
alias(libs.plugins.android.library) | |
alias(libs.plugins.kotlin.android) | |
} | |
android { | |
namespace = "com.touchlane.compositebuild.core.ui" | |
compileSdk = 33 | |
defaultConfig { | |
minSdk = 24 | |
targetSdk = 33 | |
testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner" | |
consumerProguardFiles("consumer-rules.pro") | |
} | |
buildTypes { | |
getByName("release") { | |
isMinifyEnabled = false | |
proguardFiles( | |
getDefaultProguardFile("proguard-android-optimize.txt"), | |
"proguard-rules.pro" | |
) | |
} | |
} | |
compileOptions { | |
sourceCompatibility = JavaVersion.VERSION_1_8 | |
targetCompatibility = JavaVersion.VERSION_1_8 | |
} | |
kotlinOptions { | |
jvmTarget = "1.8" | |
} | |
buildFeatures { | |
compose = true | |
aidl = false | |
buildConfig = false | |
renderScript = false | |
shaders = false | |
} | |
composeOptions { | |
kotlinCompilerExtensionVersion = libs.versions.androidxComposeCompiler.get() | |
} | |
} |
/composite-build-example/core/ui/build.gradle.kts
You can notice that we don’t have com.android.library
plugin implementation yet, so let’s fix that. First of all, we create a plugin class:
class AndroidLibraryConventionPlugin : Plugin<Project> { | |
override fun apply(target: Project) { | |
with(target) { | |
with(pluginManager) { | |
apply("com.android.library") | |
} | |
extensions.configure<LibraryExtension> { | |
configureAndroidCommon(this) | |
configureAndroidLibrary(this) | |
} | |
} | |
} | |
} |
Here we use the configureAndroidCommon
method that was described above and additionally we use the configureAndroidLibrary
method:
internal fun Project.configureAndroidLibrary( | |
extension: LibraryExtension, | |
) { | |
extension.apply { | |
defaultConfig { | |
targetSdk = 33 | |
} | |
} | |
} |
/composite-build-example/build-logic/convention/src/main/kotlin/…
You can say that the code above is the same as in the configureAndroidApplication
function and you’ll be right. Both the ApplicationExtension
and LibraryExtension
APIs have the targetSdk
field in defaultConfig
. However, defaultConfig
is represented by different classes: ApplicationDefaultConfig
for ApplicationExtension
and LibraryDefaultConfig
for LibraryExtension
and they do not have targetSdk
property in their common parent DefaultConfig
, so we have to separate our logic between the library and application extensions.
Don’t forget to add our plugin to the build-logic
project:
gradlePlugin { | |
plugins { | |
register("androidApplication") { | |
id = "compositebuild.android.application" | |
implementationClass = "AndroidApplicationConventionPlugin" | |
} | |
register("androidKotlin") { | |
id = "compositebuild.jetbrains.kotlin.android" | |
implementationClass = "AndroidKotlinConventionPlugin" | |
} | |
register("androidLibrary") { | |
id = "compositebuild.android.library" | |
implementationClass = "AndroidLibraryConventionPlugin" | |
} | |
} | |
} |
And let’s replace the content in our module build.gradle.kts
:
plugins { | |
id("compositebuild.android.library") | |
id("compositebuild.jetbrains.kotlin.android") | |
} | |
android { | |
namespace = "com.touchlane.compositebuild.core.ui" | |
defaultConfig { | |
testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner" | |
consumerProguardFiles("consumer-rules.pro") | |
} | |
buildTypes { | |
getByName("release") { | |
isMinifyEnabled = false | |
proguardFiles( | |
getDefaultProguardFile("proguard-android-optimize.txt"), | |
"proguard-rules.pro" | |
) | |
} | |
} | |
composeOptions { | |
kotlinCompilerExtensionVersion = libs.versions.androidxComposeCompiler.get() | |
} | |
} |
As you can see we have reduced the code in this part a lot.
Jetpack compose plugin
As we mentioned at the very beginning of this article you can move anything to your plugin if it helps you reduce boilerplate and improve maintenance. For instance, we can also move parts related to compose:
class AndroidComposeConventionPlugin : Plugin<Project> { | |
override fun apply(target: Project) { | |
with(target) { | |
extensions.findByType(ApplicationExtension::class.java)?.apply { | |
configureAndroidCompose(this) | |
} | |
extensions.findByType(LibraryExtension::class.java)?.apply { | |
configureAndroidCompose(this) | |
} | |
} | |
} | |
} |
/composite-build-example/build-logic/convention/src/main/kotlin/…
Again, configureAndroidCompose
is applied to 2 types of extensions: ApplicationExtension
and LibraryExtension
. It is also possible to create 2 separate plugins for ApplicationExtension
and LibraryExtension
e.g. AndroidApplicationComposeConventionPlugin
and AndroidLibraryComposeConventionPlugin
but in our example, we use the approach described above. And here is our configureAndroidCompose
method:
internal fun Project.configureAndroidCompose( | |
commonExtension: CommonExtension<*, *, *, *>, | |
) { | |
val libs = extensions.getByType<VersionCatalogsExtension>().named("libs") | |
commonExtension.apply { | |
buildFeatures { | |
compose = true | |
} | |
composeOptions { | |
kotlinCompilerExtensionVersion = | |
libs.findVersion("androidxComposeCompiler").get().toString() | |
} | |
dependencies { | |
val bom = libs.findLibrary("androidx-compose-bom").get() | |
add("implementation", platform(bom)) | |
add("androidTestImplementation", platform(bom)) | |
} | |
} | |
} |
/composite-build-example/build-logic/convention/src/main/kotlin/…
Pay attention that we can also add some dependencies. As we move compose logic to a plugin, it makes sense to add dependencies to the BOM of compose to reduce dependencies in the application’s library section. Register our plugin in the build-logic
project:
gradlePlugin { | |
plugins { | |
register("androidApplication") { | |
id = "compositebuild.android.application" | |
implementationClass = "AndroidApplicationConventionPlugin" | |
} | |
register("androidKotlin") { | |
id = "compositebuild.jetbrains.kotlin.android" | |
implementationClass = "AndroidKotlinConventionPlugin" | |
} | |
register("androidLibrary") { | |
id = "compositebuild.android.library" | |
implementationClass = "AndroidLibraryConventionPlugin" | |
} | |
register("androidCompose") { | |
id = "compositebuild.android.compose" | |
implementationClass = "AndroidComposeConventionPlugin" | |
} | |
} | |
} |
And replace the compose-related part in our application:
plugins { | |
id("compositebuild.android.application") | |
id("compositebuild.jetbrains.kotlin.android") | |
id("compositebuild.android.compose") | |
} | |
android { | |
namespace = "com.touchlane.compositebuild.example" | |
defaultConfig { | |
applicationId = "com.touchlane.compositebuild.example" | |
versionCode = 1 | |
versionName = "1.0" | |
testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner" | |
} | |
buildTypes { | |
getByName("release") { | |
isMinifyEnabled = false | |
proguardFiles( | |
getDefaultProguardFile("proguard-android-optimize.txt"), | |
"proguard-rules.pro" | |
) | |
} | |
} | |
} |
/composite-build-example/app/build.gradle.kts
Additionally, we are reducing our dependencies part:
dependencies { | |
//No more need | |
//val composeBom = platform(libs.androidx.compose.bom) | |
//implementation(composeBom) | |
implementation(libs.androidx.activity.compose) | |
implementation(libs.androidx.compose.material3) | |
implementation(libs.androidx.compose.ui) | |
implementation(libs.androidx.compose.ui.tooling.preview) | |
implementation(libs.androidx.core.ktx) | |
testImplementation(libs.junit4) | |
//No more need | |
//androidTestImplementation(composeBom) | |
androidTestImplementation(libs.androidx.test.ext) | |
androidTestImplementation(libs.androidx.test.espresso.core) | |
androidTestImplementation(libs.androidx.compose.ui.test) | |
debugImplementation(libs.androidx.compose.ui.tooling) | |
debugImplementation(libs.androidx.compose.ui.testManifest) | |
} |
/composite-build-example/app/build.gradle.kts
Congratulations, now you know how to make your work with build files more productive and maintainable. You can always add your custom logic to plugins to reduce the repeatable work that you do when creating a new module. As a developer, you and only you decide what part of the code goes to the plugin, and here are a few really useful examples for you to explore.
- Full code sample (you can checkout to commits to see how the project transforms by each major step)
- Version catalog documentation
- Composite build documentation
- Now in Android GitHub repository
- Renovate GitHub repository
- Version catalog update plugin GitHub repository
This article was originally published on proandroiddev.com