Navigating the Mysteries of Gradle Build Scripts: Questions You Might Hesitate to Ask
Beyond StackOverflow: A Safe Haven for Understanding Gradle in Android Development

Gradle stands as a pivotal tool in Android development, orchestrating a broad spectrum of tasks from code compilation and dependency management to test execution and binary packaging. It’s a system that’s as powerful as it is complex, often leaving even seasoned developers with lingering questions.
In the world of Android app development, our journey with Gradle typically begins with the default setup provided by Android Studio. As we integrate new libraries or features, we follow documentation instructions, add dependencies as needed, and then move on, often leaving a deeper understanding of the build system by the wayside.
However, questions about Gradle build scripts can linger in the minds of many developers. Questions that, in the fast-paced world of forums like StackOverflow, might be met with more downvotes than helpful answers. Recognising this gap, this article aims to demystify some of the lesser-discussed aspects of Gradle in Android development. Let’s embark on a journey to untangle the complexities of Gradle build scripts, creating a space where no question is too fundamental, and every query is a step towards more profound understanding.
The key Gradle files in an Android App project
Let’s talk about gradle.properties, local.properties, settings.gradle(.kts) and build.gradle(.kts).
1. gradle.properties
It is a key configuration file in a Gradle project, to configure options that affect the Gradle build process.
- Common uses include setting JVM options, enabling or disabling features, and configuring performance-related settings like Gradle daemon usage.
💡 There is no difference in syntax between Groovy and Kotlin DSL for
gradle.properties,as this file uses simple key-value pairs.
# JVM options to optimize Gradle performance
org.gradle.jvmargs=-Xmx2048m -XX:MaxMetaspaceSize=512m
# Enables new dependency management behavior
android.useAndroidX=true
# Enables parallel execution to improve build speed
org.gradle.parallel=true
# Configures the Gradle daemon for faster builds
org.gradle.daemon=true2. local.properties
It is optionally used for settings specific to a local machine or development environment. These settings are not intended to be checked into version control (like Git), as they can differ for each developer or environment.
- Same as
gradle.properties, there is no difference in syntax between Groovy and Kotlin DSL, as this file uses simple key-value pairs. - One of the most common uses in Android development is to specify the location of the Android SDK on the local machine. This is particularly useful when developers have the SDK installed in different locations, or for CI/CD systems where the SDK path might vary.
- It can also store sensitive information or configuration data that should not be shared, such as API keys (If possible, avoid storing sensitive data like API keys in
local.properties, especially if there's a risk of it being committed to version control. Consider more secure approaches for managing sensitive configuration data).
sdk.dir=/path/to/android/sdk💡 It’s a common practice to include
local.propertiesin.gitignoreto prevent it from being committed to version control.
3. settings.gradle(.kts)
settings.gradle (Groovy) or settings.gradle.kts (Kotlin) is used to configure the Gradle build at a high level, primarily for multi-project builds.
- It defines which modules (subprojects) are included in the build.
- It can also include other high-level configurations, such as dependency resolution strategies and plugin management.
🧑💻 Sample Code — Groovy DSL (settings.gradle):
rootProject.name = 'MyApp'
// Include modules
include ':app'
include ':mylibrary'
// Define module paths
project(':mylibrary').projectDir = new File('libs/mylibrary')🧑💻 Sample Code — Kotlin DSL (settings.gradle.kts):
rootProject.name = "MyApp"
pluginManagement {
repositories {
gradlePluginPortal()
google()
mavenCentral()
}
}
dependencyResolutionManagement {
repositoriesMode.set(RepositoriesMode.FAIL_ON_PROJECT_REPOS)
repositories {
google()
mavenCentral()
maven("https://jitpack.io")
}
}
// Include modules
include(":app")
include(":mylibrary")
// Define module paths
project(":mylibrary").projectDir = file("libs/mylibrary")4. build.gradle(.kts)
build.gradle (Groovy) or build.gradle.kts (Kotlin) is the core build script in each Gradle project or module.
- It is used for more specific configurations related to the project or module, such as declaring dependencies, setting up plugins, defining build variants, and customising the build process.
- In multi-module projects, each module will have its own
build.gradle(.kts)file, while the root project will have one that may include common configurations or subprojects' specifics.
🧑💻 Sample Code — Project-Level Groovy DSL (build.gradle):
// Example of root project build.gradlein Groovy DSL
buildscript {
ext.kotlin_version = '1.4.10'
repositories {
gradlePluginPortal()
google()
mavenCentral()
}
dependencies {
classpath 'com.android.tools.build:gradle:4.1.0'
classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version"
}
}
plugins {
id 'io.fabric' version '1.31.2'
}
allprojects {
repositories {
gradlePluginPortal()
google()
mavenCentral()
}
dependencies {
// Other shared dependencies
}
}
subprojects {
dependencies {
// Other shared dependencies
}
}🧑💻 Sample Code — Project-Level Kotlin DSL (build.gradle.kts):
// Example of root project build.gradle.kts in Kotlin DSL
buildscript {
val kotlinVersion = "1.4.10"
dependencies {
classpath("com.android.tools.build:gradle:4.1.0")
classpath("org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlinVersion")
}
}
plugins {
id("io.fabric") version "1.31.2"
}
allprojects {
repositories {
gradlePluginPortal()
google()
mavenCentral()
}
dependencies {
// Other shared dependencies
}
}
subprojects {
dependencies {
// Other shared dependencies
}
}
tasks.register("clean", Delete::class) {
delete(rootProject.buildDir)
}💡 The
allprojectsblock applies configurations to every project in the build. This includes the root project itself and all of its subprojects.
The
subprojectsblock applies configurations only to the subprojects of the root project, not to the root project itself.
Where should we define repositories{}?
Looking carefully at the sample code above, we see repositories{} appeared repeatedly. This is one common problem I have seen; as we upgrade the Gradle version, migrate our Groovy build scripts and add new dependencies to our projects, we may introduce redundant declarations.
In modern Gradle setups, especially with the introduction of centralised dependency management in Gradle 6.8 and later, it’s possible to consolidate repository declarations that were traditionally placed in the buildscript, allprojects, and subprojects blocks of build.gradle(.kts) files into settings.gradle.kts. This approach streamlines the management of repositories and ensures consistency across the entire project.
💡 Groovy does not have direct equivalents to
pluginManagementanddependencyResolutionManagement.
For Kotlin DSL , in short, if we see
pluginManagementanddependencyResolutionManagementinsettings.gradle(.kts), it is almost good to try removing the repository declarations inbuild.gradle(.kts).
Centralising Repository Declarations in settings.gradle(.kts)
This approach is especially beneficial in multi-module projects to maintain consistency and reduce redundancy.
For Plugin Repositories (pluginManagement):
- In
settings.gradle.kts, use thepluginManagementblock to define repositories for resolving Gradle plugins. - This replaces the need for repository declarations in the
buildscriptblock of ourbuild.gradle(.kts)files.
For Project Dependencies (dependencyResolutionManagement):
- The
dependencyResolutionManagementblock insettings.gradle.ktsallows us to specify repositories for all project dependencies. - This can replace the
repositorydeclarations inallprojectsandsubprojectsblocks.
Understanding Project-Level vs. Module-Level build.gradle(.kts) Build Scripts
In a typical Android project using Gradle, we will encounter two main types of build.gradle files: one at the project level and one or more at the module level. Each serves a distinct purpose in the build configuration.
Project-Level build.gradle(.kts)
It defines configuration settings that apply to the entire project. This includes the versions of Gradle and the Android Gradle Plugin, repositories for fetching dependencies, and dependencies for the build script itself.
Common Configurations:
- Plugin version management.
- Definition of repositories (like Google’s Maven repository or jCenter).
- Classpaths for buildscript dependencies.
💡 If a project is not implementing the centralising repository configuration mentioned above, the
buildscriptblock in the project-level build.gradle(.kts) is primarily used for declaring classpaths for plugins and other build script dependencies, whereas thepluginsblock is used for applying plugins directly. This distinction is crucial.
Module-Level build.gradle(.kts)
It is focused on the configuration specific to a module within the project. In Android development, a module can be an app, a library, or a test module. This script defines configurations like application ID, minimum and target SDK versions, dependencies specific to the module, and build variants.
Common Configurations:
- Android SDK versions.
- Module-specific dependencies.
- Build types (debug, release) and their specific configurations.
- Android-specific configurations like signing configurations, resource configurations, and more.
🧑💻 Sample Code — Groovy DSL (app/build.gradle):
// Example of module-level build.gradle in Groovy DSL for an app module
apply plugin: 'com.android.application'
android {
compileSdkVersion 30
defaultConfig {
applicationId "com.example.myapp"
minSdkVersion 21
targetSdkVersion 30
versionCode 1
versionName "1.0"
}
buildTypes {
release {
minifyEnabled false
proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro'
}
}
}
dependencies {
implementation fileTree(dir: 'libs', include: ['*.jar'])
implementation "org.jetbrains.kotlin:kotlin-stdlib:$kotlin_version"
// Other module-specific dependencies...
}🧑💻 Sample Code — Kotlin DSL (app/build.gradle.kts):
// Example of module-level build.gradle.kts in Kotlin DSL for an app module
plugins {
id("com.android.application")
kotlin("android")
}
android {
compileSdkVersion(30)
defaultConfig {
applicationId = "com.example.myapp"
minSdkVersion(21)
targetSdkVersion(30)
versionCode = 1
versionName = "1.0"
}
buildTypes {
getByName("release") {
isMinifyEnabled = false
proguardFiles(getDefaultProguardFile("proguard-android-optimize.txt"), "proguard-rules.pro")
}
}
}
dependencies {
implementation(fileTree(mapOf("dir" to "libs", "include" to listOf("*.jar"))))
implementation("org.jetbrains.kotlin:kotlin-stdlib")
// Other module-specific dependencies...
}Key Differences Between a Plugin and a Dependency
- Functionality: Plugins modify or extend the build process, adding tasks or configuring the project, while dependencies are libraries, or modules the project’s code uses.
- Impact Scope: The impact of a plugin is on the build process itself, potentially affecting all project modules, whereas dependencies impact the code’s runtime behaviour, usually within a specific module.
Common Plugins
- The
com.android.applicationplugin is used for Android app projects. - The
kotlin-androidplugin is used when incorporating Kotlin into an Android project.
Is it possible to define plugins at the module level only?
Yes, it is entirely possible, and often quite common, to define plugins at the module level only, without defining them at the project level in Gradle. This approach allows for more granularity and specificity in how plugins are applied to different project parts, especially in multi-module projects.
apply plugin: vs plugins{} and Version Specifications
Groovy DSL
apply plugin:: Traditional method for applying plugins.
💡 The plugin version is typically specified in the
buildscriptblock's classpath of the root project’sbuild.gradlefile, which is whyapply plugin:doesn't require a version.
apply plugin: 'com.android.application'plugins{}Block: More declarative, allows specifying the version within the block.
plugins {
id 'java' version '1.8'
// Other plugins
}Kotlin DSL
plugins{}Block: Standard method for applying plugins, with inline version specification.
plugins {
id("com.android.application") version "4.1.0"
// Other plugins
}💡
apply plugin:is not commonly used in Kotlin DSL. Thepluginsblock is preferred for its clarity and simplicity.
Kotlin DSL: 'apply’ Keyword in plugins{} Block
In Kotlin DSL, we can control whether a plugin is immediately applied to the project with the apply keyword.
💡 The
pluginsblock in Groovy doesn’t support theapply falsesyntax.
apply = true: The plugin is applied immediately (default behaviour, ifapplyis not specified). This is used when we want the plugin’s features to be active in that particular Gradle script.
plugins {
id("kotlin-android-extensions") version "1.4.10" apply true
}apply = false: This indicates that the plugin should be made available but not applied. It’s a way to declare a plugin for potential use without activating it immediately.
plugins {
id("kotlin-android-extensions") version "1.4.10" apply false
}💡 The use of
apply falseat the project level in Kotlin DSL is a strategic choice for making plugins available to all modules without immediately applying them. It streamlines the build configuration process, especially in large or complex multi-module projects. Each submodule then explicitly applies the plugins it requires. This approach is specific to Kotlin DSL and reflects its more advanced and declarative capabilities than Groovy DSL.
Closing note
Navigating the intricacies of Gradle build scripts in Android development can be a daunting task, even for the experienced. This exploration aims to shed light on some of the more elusive aspects of Gradle, offering clarity on topics that are often overlooked or considered too basic to question. Remember, mastering a tool like Gradle is continuous, and every question, no matter how simple it may seem, is a vital step in that journey. By demystifying these fundamental concepts, we enhance our understanding and contribute to a more informed and supportive Android development community.





