Chapters

Hide chapters

Android Fundamentals by Tutorials

First Edition · Android 14 · Kotlin 1.9 · Android Studio Hedgehog (2023.1.1)

4. Gradle Basics: A Look Behind the Curtain
Written by Ricardo Costeira

Heads up... You're reading this book for free, with parts of this chapter shown beyond this point as scrambled text.

Gradle is the open-source build automation system that Android developers use to build their apps. More specifically, Android uses the Android Gradle Plugin (AGP), which uses Gradle, to compile your code and resources into a single file. You can install your app directly if you create an .apk file. Otherwise, you can create an .aab file, which contains all the information Google Play needs to build an optimized .apk file for the specific device that requests to download the app.

Using Gradle, you can manipulate the build process and its logic, creating multiple versions of your app, optimizing it, removing unused code or resources, or doing something more complex, like modifying intermediate build objects during the build process. Any Android developer must have at least a basic understanding of how to use Gradle. In this chapter, you’ll learn:

  • What the default Gradle scripts of an Android project are, and what they’re for.
  • How Gradle defines which Android versions your app supports.
  • How to customize basic aspects of your build process.
  • What build types are and how to customize them.
  • What a Bill of Materials (BOM) is.
  • How to manage app dependencies.
  • How to sign your app.

Exploring Your Gradle Configuration

In Android Studio, open this chapter’s starter project and, if you’re not doing so already, change the project overview mode to Project so it’s easier to see where the Gradle configuration files fit. Do this by clicking the current mode at the top of the Project tab.

After this, look at the project’s file structure.

The circled files are the Gradle configuration files that exist by default in modern Android projects. Notice how two have the same name, build.gradle.kts. The one at the project’s root is the top-level build.gradle.kts file, and there’s one of these for each Android project. The one under the app folder is a module-level build.gradle.kts file, and there’s one for each module.

A module is a container for code, resources and configuration files, such as the manifest and the module-level build.gradle.kts file. By default, Android projects start with one module: the app module. However, it’s normal for apps to have multiple modules, which means multiple module-level build.gradle.kts files. Each build file configures its corresponding module, possibly while sharing common code.

For many years, these files were called build.gradle instead and were written in Groovy. You can still use build configuration files with that name, but Google now recommends writing Gradle scripts in Kotlin. That’s where the .kts extension comes in: build.gradle.kts files are written in Kotlin; build.gradle files are written in Groovy.

It’s still common to see build.gradle files in the wild and, because they were the original way of writing Gradle scripts, builds that use Kotlin tend to be slower. Despite that, migration is surely happening because Kotlin is easier to read and write and has better IDE support than Groovy.

Understanding the Top-Level Build Gradle File

From the project structure view, open the top-level build.gradle.kts file. You’ll find only a few lines of code:

// 1
plugins {
  // 2
  id("com.android.application") version "8.1.4" apply false
  // 3
  id("org.jetbrains.kotlin.android") version "1.9.10" apply false
}

Inspecting the Settings Gradle File

Before proceeding to the module-level Gradle file, the top level has one more important file: settings.gradle.kts. In the project structure view, find and double-click the file. You’ll find this piece of code inside:

// 1
pluginManagement {
  // 2
  repositories {
    google()
    mavenCentral()
    gradlePluginPortal()
  }
}
// 3
dependencyResolutionManagement {
  repositoriesMode.set(RepositoriesMode.FAIL_ON_PROJECT_REPOS)
  repositories {
    google()
    mavenCentral()
  }
}

// 4
rootProject.name = "Kodeco Chat"
include(":app")

Going Through the Module-Level Build Gradle File

As an Android developer, this is the Gradle script where you’ll focus most of your time. This script has a lot of code in it, so you’ll go through it in parts. In the app module, double-click its module-level build.gradle.kts.

Declaring Plugins

Right at the top, you can see another plugins { } block:

plugins {
  id("com.android.application")
  id("org.jetbrains.kotlin.android")
}

Exploring the Android { } Block

Right below the plugins, there’s the android { } block. This block is where you define all the Android-specific build options.

android {
  // 1
  namespace = "com.kodeco.chat"
  // 2
  compileSdk = 34

  // 3
  defaultConfig {
    // 4
    applicationId = "com.kodeco.chat"
    // 5
    minSdk = 30
    // 6
    targetSdk = 34
    // 7
    versionCode = 1
    // 8
    versionName = "1.0"
    // 9
    testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner"
    // 10
    vectorDrawables {
      useSupportLibrary = true
    }
  }

  // ...
}
android {
  // ...

  // 1
  buildTypes {
    release {
      isMinifyEnabled = false
      proguardFiles(
        getDefaultProguardFile("proguard-android-optimize.txt"),
        "proguard-rules.pro"
      )
    }
  }
  // 2
  compileOptions {
    sourceCompatibility = JavaVersion.VERSION_1_8
    targetCompatibility = JavaVersion.VERSION_1_8
  }
  // 3
  kotlinOptions {
    jvmTarget = "1.8"
  }
  // 4
  buildFeatures {
    compose = true
  }
  // 5
  composeOptions {
    kotlinCompilerExtensionVersion = "1.5.3"
  }
  // 6
  packaging {
    resources {
      excludes += "/META-INF/{AL2.0,LGPL2.1}"
    }
  }
}
android {
  namespace = "com.kodeco.chat"
  compileSdk = 34

  defaultConfig {
    applicationId = "com.kodeco.chat"
    minSdk = 30
    targetSdk = 34
    versionCode = 1
    versionName = "1.0"

    testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner"
  }
  buildTypes {
    release {
      isMinifyEnabled = false
      proguardFiles(
        getDefaultProguardFile("proguard-android-optimize.txt"),
        "proguard-rules.pro"
      )
    }
  }
  kotlinOptions {
    jvmTarget = "1.8"
  }
  buildFeatures {
    compose = true
  }
  composeOptions {
    kotlinCompilerExtensionVersion = "1.5.3"
  }
}

Managing Build Types

Returning to the buildTypes { } block, you can see it has one build type: release. With that in mind, click the Build Variants icon in Android Studio’s left sidebar.

buildTypes {
  release {
    // 1
    isMinifyEnabled = false
    // 2
    proguardFiles(
      getDefaultProguardFile("proguard-android-optimize.txt"),
      "proguard-rules.pro"
    )
  }
}
debug {
  applicationIdSuffix = ".debug"
}

Syncing the Project With Gradle

Due to your changes on build.gradle.kts, this little banner now appears at the top of the editor.

Including Dependencies

This file’s last code block is the dependencies { } block. This is where you declare all the libraries and modules your module needs to work. You can define exactly the configuration you want your dependencies to be a part of.

implementation(group = "androidx.core", name = "core-ktx", version = "1.12.0")

Using the Bill of Materials

You’ll notice some Compose dependencies, like implementation("androidx.compose.ui:ui"), have no version. This is due to the following line of code:

implementation(platform("androidx.compose:compose-bom:2023.10.01"))

Managing Dependencies With Version Catalogs

For your non-Compose dependencies, you have to maintain the dependencies and periodically check on new versions so you don’t miss out on new features or bug fixes. For instance, if you have an app that uses the CameraX Jetpack Library, you might have this set of dependencies:

implementation("androidx.camera:camera-camera2:1.3.0")
implementation("androidx.camera:camera-lifecycle:1.3.0")
implementation("androidx.camera:camera-video:1.3.0")
implementation("androidx.camera:camera-view:1.3.0")

# 1
[versions]
# 2
# Plugin versions
android-gradle-plugin = "8.2.0"
kotlin = "1.9.10"

# Dependency versions
activity-compose = "1.8.0"
androidx-compose-bom = "2023.10.01"
core-ktx = "1.12.0"
lifecycle-runtime-ktx = "2.6.2"

# Instrumented tests versions
androidx-junit = "1.1.5"
espresso-core = "3.5.1"

# Unit tests versions
junit = "4.13.2"
# 1
[libraries]
# 2
androidx-activity-compose = { group = "androidx.activity", name = "activity-compose", version.ref = "activity-compose" }
androidx-core-ktx = { group = "androidx.core", name = "core-ktx", version.ref = "core-ktx" }
androidx-lifecycle-runtime-ktx = { group = "androidx.lifecycle", name = "lifecycle-runtime-ktx", version.ref = "lifecycle-runtime-ktx" }
# 3
androidx-compose-bom = { group = "androidx.compose", name = "compose-bom", version.ref = "androidx-compose-bom" }
# 4
androidx-compose-material3 = { group = "androidx.compose.material3", name = "material3" }
androidx-compose-ui-ui-graphics = { group = "androidx.compose.ui", name = "ui-graphics" }
androidx-compose-ui-ui = { group = "androidx.compose.ui", name = "ui" }
androidx-compose-ui-ui-tooling-preview = { group = "androidx.compose.ui", name = "ui-tooling-preview" }

androidx-compose-ui-ui-tooling = { group = "androidx.compose.ui", name = "ui-tooling" }
androidx-compose-ui-ui-test-manifest = { group = "androidx.compose.ui", name = "ui-test-manifest" }

androidx-compose-ui-ui-test-junit4 = { group = "androidx.compose.ui", name = "ui-test-junit4" }
androidx-espresso-core = { group = "androidx.test.espresso", name = "espresso-core", version.ref = "espresso-core" }
androidx-junit = { group = "androidx.test.ext", name = "junit", version.ref = "androidx-junit" }

junit = { group = "junit", name = "junit", version.ref = "junit" }
[plugins]
android-application = { id = "com.android.application", version.ref = "android-gradle-plugin" }
kotlin-android = { id = "org.jetbrains.kotlin.android", version.ref = "kotlin" }
plugins {
  alias(libs.plugins.android.application) apply false
  alias(libs.plugins.kotlin.android) apply false
}

dependencies {
  val composeBom = platform(libs.androidx.compose.bom)
  // ...
}
dependencies {
  val composeBom = platform(libs.androidx.compose.bom)
  // ...

  implementation(composeBom)
  // ...
  androidTestImplementation(composeBom)
}
dependencies {
  val composeBom = platform(libs.androidx.compose.bom)

  implementation(libs.androidx.core.ktx)
  implementation(libs.androidx.lifecycle.runtime.ktx)
  implementation(libs.androidx.activity.compose)
  implementation(composeBom)
  implementation(libs.androidx.compose.ui.ui)
  implementation(libs.androidx.compose.ui.ui.graphics)
  implementation(libs.androidx.compose.ui.ui.tooling.preview)
  implementation(libs.androidx.compose.material3)
  testImplementation(libs.junit)
  androidTestImplementation(libs.androidx.junit)
  androidTestImplementation(libs.androidx.espresso.core)
  androidTestImplementation(composeBom)
  androidTestImplementation(libs.androidx.compose.ui.ui.test.junit4)
  debugImplementation(libs.androidx.compose.ui.ui.tooling)
  debugImplementation(libs.androidx.compose.ui.ui.test.manifest)
}

Grouping Dependencies With Bundles

A lot of these dependencies can be put into groups. With Version Catalogs, you can create bundles of dependencies — using one dependency line, you can include a bunch of dependencies simultaneously.

[bundles]
androidx = ["androidx-activity-compose", "androidx-core-ktx", "androidx-lifecycle-runtime-ktx"]
compose = ["androidx-compose-material3", "androidx-compose-ui-ui-graphics", "androidx-compose-ui-ui", "androidx-compose-ui-ui-tooling-preview"]
compose-debug = ["androidx-compose-ui-ui-tooling", "androidx-compose-ui-ui-test-manifest"]
instrumented-tests = ["androidx-junit", "androidx-espresso-core", "androidx-compose-ui-ui-test-junit4"]
unit-tests = ["junit"]
dependencies {
  val composeBom = platform(libs.androidx.compose.bom)

  implementation(composeBom)
  implementation(libs.bundles.androidx)
  implementation(libs.bundles.compose)

  testImplementation(libs.bundles.unit.tests)
  androidTestImplementation(composeBom)
  androidTestImplementation(libs.bundles.instrumented.tests)

  debugImplementation(libs.bundles.compose.debug)
}

Signing Your App for Release

To install an app on a device, Android requires a certificate to assert the authenticity of the app. You add this certificate to your app through app signing.

signingConfigs {
  create("release") {
    storeFile = file("path to your keystore file")
    storePassword = "your store password"
    keyAlias = "your key alias"
    keyPassword = "your key password"
  }
}
release {
  signingConfig = signingConfigs.getByName("release")
  // ...
}

Keeping Your Secrets Safe

Remember two important considerations regarding your keystore file:

storeFile = "path to your keystore file"
storePassword = "your store password"
keyAlias = "your key alias"
keyPassword = "your key password"
val keysPropertiesFile: File = rootProject.file("keys.properties")
val keysProperties = Properties()

keysProperties.load(FileInputStream(keysPropertiesFile))
import java.io.FileInputStream
import java.util.Properties
signingConfigs {
  create("release") {
    keyAlias = keysProperties["keyAlias"] as String
    keyPassword = keysProperties["keyPassword"] as String
    storeFile = file(keysProperties["storeFile"] as String)
    storePassword = keysProperties["storePassword"] as String
  }
}

# ...
# Keystore confidential information
keys.properties

Key Points

  • Gradle is a powerful and customizable build system.
  • You can define different build types for your app, which let you build it using different properties, according to your needs.
  • You can manage your dependencies in an organized and scalable way using Version Catalogs.
  • Signing your app is essential for you to be able to release it.
  • It’s extremely important to keep your signing information safe.
Have a technical question? Want to report a bug? You can ask questions and report bugs to the book authors in our official book forum here.
© 2024 Kodeco Inc.

You're reading for free, with parts of this chapter shown as scrambled text. Unlock this book, and our entire catalogue of books and videos, with a Kodeco Personal Plan.

Unlock now