4.
Gradle Basics: A Look Behind the Curtain
Written by Ricardo Costeira
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
}
Going step by step:
- This defines which plugins Gradle should use project-wide. Plugins are essential for the build system to work. This
plugins { }block applies plugins through their ID, a unique identifier each plugin has. - The Android build system requires apps to have one application module and allow for multiple (optional) library modules. The
com.android.applicationplugin lets you define a module as the application module (by default, the app module). If the project had any library modules, you’d have to import thecom.android.libraryplugin here. Both of these plugins are part of AGP, so the version code you use to import them belongs, in fact, to AGP. - The second plugin is the Kotlin Gradle plugin, which lets you write Gradle files in Kotlin.
Note: Although each
com.android.applicationmodule defines one app, a single Android project can have multiple application modules and produce multiple apps depending on the configuration. For instance, you might want to produce different apps with different features but many shared components. In that case, managing every app under the same project might be easier.
You want to access these plugins in your modules, but you don’t want to apply them at the root level — there’s no need, and it would be extra work. Nonetheless, you still want to define them in one place to achieve a single source of truth. That’s why you declare them in this top-level file but have that apply false at the end.
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")
Here’s what’s going on:
- This code defines a
pluginManagement { }block, where you can declare the repositories for plugins. Gradle uses these to resolve the top-level build.gradle.kts plugins. You can also use this block to define plugins with specific versions, add aresolutionStrategy { }block to detail how to resolve plugin conflicts, etc. - This
repositories { }block is where you add repositories (by order of preference) where Gradle should search for the plugins you use. - In the
dependencyResolutionManagement { }block, you declare the repositories for project dependencies. Gradle uses these to resolve library dependencies you include in the project. Another important aspect here isRepositoriesMode. By default, any repositories you declare in a project’s build.gradle.kts override the ones you declare in settings.gradle.kts. By settingrepositoriesModetoFAIL_ON_PROJECT_REPOS, you force the build to fail if any repositories are declared outside this file. - This sets the name of the root project, which, in Gradle language, corresponds to the project that the top-level build.gradle.kts manages — Gradle views module-level build.gradle.kts as sub-projects. That said, this file’s last line includes the app module as a sub-project. You need to include all the project’s modules like this, or Gradle won’t see them.
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")
}
Here, you declare which plugins the app module should use. The module just happens to use all the plugins the top-level build.gradle.kts made available.
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
}
}
// ...
}
Focus on the top part first so it’s easier to follow. Going step by step:
-
Defining a
namespaceis necessary for resource access. This property lived in the AndroidManifest.xml file under thepackageproperty for many years but has now migrated. -
The
compileSdkoption indicates the API level you’ll use to compile your app. This means you can’t use features from an API higher than this value. Here, you’ve set the value to use APIs from Android 14. -
The
defaultConfig { }block contains options you want to apply to all of your app’s build versions (e.g., debug, release, etc.) by default. -
The
applicationIdis the identifier of your app. It should be unique, allowing you to publish or update your app on the Google Play Store. If you leave it undefined, the build system will use thenamespaceasapplicationId. -
With
minSdk, you set the lowest API level your app supports. Your app won’t be available in the Play Store for devices running on lower API levels. -
On the other hand,
targetSdkdefines the maximum API level on which your app has been tested. That is to say, you’re sure your app works properly on the devices with this SDK version and doesn’t require any backward-compatibility behavior. The best approach is to thoroughly test an app using the latest API, keeping yourtargetSdkvalue equal tocompileSdk. -
versionCodeis a numeric value for the app version. -
versionNameis a user-friendly string for the app version. -
With Android, you can run tests on the Java Virtual Machine (JVM) or directly on the Android device. The latter are called instrumented tests; to run them, you must define the
testInstrumentationRunner. You can have different values here depending on your needs, butandroidx.test.runner.AndroidJUnitRunneris the default. -
By enabling the support library for vector drawables, you allow proper vector drawable usage below API 24.
Vector drawables are images you define through XML. They’re ideal for Android because they scale in size without losing quality, so you don’t need to have different image files for different screen sizes. The support library uses the support methods below API 24 and delegates to the framework’s VectorDrawable API when the app is above API 24. In this case, you have minSdk = 30, so there’s no need for this bit of code (Android Studio adds it automatically when creating a project). You’ll remove it in a bit.
Ok, that was a lot! Take a deep breath: Here’s the rest of the android { } block:
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}"
}
}
}
And going through it all:
- By defining your
buildTypes { }, you can build your app according to different properties. You’ll learn more about them in the next section. - The
compileOptions { }block is where you define every Java compilation option. You don’t have any Java code in the project, so you’ll remove this shortly. - Conversely,
kotlinOptions { }is where you define Kotlin compilation options. In this case, you’re setting thejvmTargetto Java 8. Why 8? Because the compiler will try to compile Kotlin to Java 8 compatible code by default. If you don’t change this, it’ll try to use whatever version of Java you have installed (probably one a lot more recent than 8). So setting this to1.8avoids version conflicts. - The
buildFeatures { }block lets you enable certain features, like View binding or Compose. In this case, it’s doing the latter. - With the
composeOptions { }block, you set specific options for Compose. WithkotlinCompilerExtensionVersion, you set the Compose compiler’s version. - The
packaging { }block defines options that tell the build system how to package files when creating the final output file. Adding this pattern to theresources.excludesproperty (adding, and not setting, because you’re using+=instead of=) makes it so any files matching the pattern will not be present in the final output.
The packaging { } block is useful in certain situations, like when your project has two libraries that have files with the same name and matching directories. If that happens, you’ll get a build conflict, and the packaging { } block is the way to fix it. Be that as it may, you don’t need it in the project for now, so you might as well remove it to keep the file as simple as possible. While you’re at it, remove the vectorDrawables { } and compileOptions { } blocks also. In the end, your android { } should look like:
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.
Note: Build variants are all the possible versions of your app you can build. They result from the combination of all build types and build flavors, which are out of the scope of this chapter. But because you only have build types in this project, your build variants will simply be equivalent to your build types.
If you have trouble finding the icon, click the More tool windows button to show all the hidden buttons.
By clicking the Build Variants icon, you open the Build Variants window below the Project window. The Build Variants window lists the currently selected build variant for each module. Weirdly enough, the active build variant isn’t release, but debug instead!
If you click the active build variant, you get a dropdown menu with all the possibilities.
As it turns out, the build system always has two default build types:
-
debugwith theisDebuggableproperty set totrue. -
releasewith theisDebuggableproperty set tofalse.
The reason to declare them explicitly in build.gradle.kts is to customize properties. Any properties you don’t change remain the same. When you create a project, Android Studio configures the release build type in the file, which is the case for this project:
buildTypes {
release {
// 1
isMinifyEnabled = false
// 2
proguardFiles(
getDefaultProguardFile("proguard-android-optimize.txt"),
"proguard-rules.pro"
)
}
}
Android Studio does it so:
- It can set
isMinifyEnabledtofalseand leave the decision to turn it back on to developers. This property tells the build system to obfuscate, optimize and shrink your code, which you want in a release build. But if you don’t have your Proguard rules properly defined, it can lead to runtime crashes. - You can tell the build system where to find the Proguard rules. Usually, you’ll put them in that proguard-rules.pro file, and the build system will handle the rest. This is out of the scope of the chapter, but most rules boil down to you telling the system explicitly what classes not to obfuscate/remove.
You’ll now configure the debug build type as well. You’ll add a suffix to the application ID. Right under the release { } block, add:
debug {
applicationIdSuffix = ".debug"
}
This is extremely useful to ensure you don’t send the wrong file to the Play Store, for instance. When you upload an app to Google Play, it shows you its app ID — if it has .debug at the end, you’ll know you messed up. :]
Note: Any app you run on an Android Studio emulator is considered debuggable no matter how
isDebuggableis set in the running build type. That means you can build, run and debug an app in release mode while havingisDebuggable = falsein the build type definition, provided you’re doing so in an emulator.
Syncing the Project With Gradle
Due to your changes on build.gradle.kts, this little banner now appears at the top of the editor.
When you change a Gradle script, it asks you to sync it with the project. This ensures the script has everything it needs to build your project. For instance, if you add any dependencies, Gradle downloads them during the sync process. If you don’t sync Gradle (or ignore the changes) and try to build the app, Gradle uses the previously valid configuration, so be sure to sync the project every time!
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.
In this project, you have the following configurations:
- implementation: The most common configuration. Use it to include the dependencies you need for development.
-
testImplementation: Use it to add the dependencies you need for JVM tests. It also inherits everything from
implementation. - androidTestImplementation: Same as above, but for instrumented tests.
- debugImplementation: A special configuration that adds dependencies only to the debug build type.
You can do many other things with dependency configurations. But, more often than not, your work with dependencies will revolve around the implementation and both test configurations.
Another important thing to note is that although you only have dependencies on external libraries here — or, in Gradle language, remote binary dependencies — you can also have dependencies on local binaries and local modules. To depend on local binaries, you’d typically have a dependency line like implementation(fileTree(mapOf("dir" to "libs", "include" to listOf("*.jar")))) that includes every .jar file in the module’s libs directory. To depend on another module, you’d have something like implementation(project(:moduleName)).
When you have dependencies on remote binaries, you have to specify the binary group and name and the version you want to target. For instance, when you write implementation("androidx.core:core-ktx:1.12.0"), you’re targeting version 1.12.0 of a binary named core-ktx that belongs to the group androidx.core. Another way to write it would be:
implementation(group = "androidx.core", name = "core-ktx", version = "1.12.0")
But the tendency is to pick the compact format.
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"))
This line adds Compose’s Bill of Materials (BOM) as a dependency. The BOM is a platform (hence the platform after implementation), a special kind of dependency you use here to get the recommended versions of your Compose dependencies. You still have to include the Compose libraries you want, but you don’t need to specify the version, like in the androidx.compose.ui:ui example earlier. If you do specify a version, it’ll override the one the BOM proposes.
The BOM ensures all your versions play nicely with each other. By updating the BOM to its latest version, you automatically update all your Compose dependencies. That said, using the BOM is the recommended way to manage your Compose dependencies whenever you have a Compose project.
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")
You must update that version number manually at every new library update. As your project grows and you add more dependencies, it can get quite cumbersome to maintain all your dependencies like this; you might forget to update some values or even to delete unneeded dependencies because you miss them among all the others.
A common rule of software development applies here: The messier and more complex code is, the less people want to touch it. As time passes, you’re left with dozens or even hundreds of old or deprecated dependencies, and you set off a cascading dependency update nightmare when you finally decide to update them.
Gradle has ways to make this easier, but the most recent iteration on Gradle dependency management is Version Catalogs. They let you maintain your dependencies and plugins for complex projects in a clean and scalable way. All the modules in your app can refer to this so-called catalog and fetch their dependencies in a type-safe way.
Version catalogs require some initial configuration. Locate the gradle folder on the root project. Right-click it, select New and then File, and create libs.versions.toml.
libs.version.toml is a special name Gradle uses to locate the file in the gradle folder. You’ll put all the information regarding versions, dependencies and plugins here. It also has a specific syntax. To add the versions, open the file and fill it with this code:
# 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"
Here, you:
- Add the
versionssection to the file. There are four possible sections in total. - Add the version values for all dependencies and plugins.
Below this code, add the next section:
# 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" }
In this piece of code:
- You declare the
librariessection, where you list all the dependencies. You can declare them in different ways, but here, you’re being specific about thegroupandnameof each dependency. Each dependency version comes from theversionssection throughversions.ref. - By using kebab case for dependency names (using
-as a separator), you’ll have better code completion when accessing the dependencies. - Declaring the dependency on Compose BOM is like declaring any other dependency.
- Using BOM, you don’t need to declare the Compose library versions.
The next section you need is plugins. Add it below what you have so far:
[plugins]
android-application = { id = "com.android.application", version.ref = "android-gradle-plugin" }
kotlin-android = { id = "org.jetbrains.kotlin.android", version.ref = "kotlin" }
Here, you declare the plugin IDs (like in the top-level build.gradle.kts) and the corresponding versions.
That’s all in this file for now. Sync the project, and the versions catalog is ready. Go to the top-level build.gradle.kts and update the plugins:
plugins {
alias(libs.plugins.android.application) apply false
alias(libs.plugins.kotlin.android) apply false
}
The alias function maps the plugin from the accessors that Gradle automatically creates from the catalog. Also, notice how Gradle picked up the - in android-application you defined in the .toml and made it so you can access it like libs.plugins.android.application.
Next, look at the module-level build.gradle.kts. Notice the dependencies in your dependencies { } block are now highlighted. If you hover your mouse over one, you’ll see this tooltip:
While the tooltip is visible, follow the command suggestion to replace the dependency with its version catalog counterpart immediately. For Mac OS, the command is Option-Shift-Enter (or Alt-Shift-Enter on Windows). Another option would be to click the dependency and then press Option-Enter (or Alt-Enter on Windows) to show this context menu:
If you do so, press Enter again to do the change. Follow these methods for all dependencies, except the Compose ones you didn’t specify a version for. For the BOM dependency, there’s no tooltip available. You’ll have to convert these manually.
To include the BOM using Version Catalogs, you first have to declare the platform as a property in the dependencies { } block:
dependencies {
val composeBom = platform(libs.androidx.compose.bom)
// ...
}
Then, include it in the configurations you need:
dependencies {
val composeBom = platform(libs.androidx.compose.bom)
// ...
implementation(composeBom)
// ...
androidTestImplementation(composeBom)
}
In the end, your dependencies { } block should look like this:
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)
}
Sync the project, and everything should work as before. While adding the dependencies manually, you already enjoyed the code completion Version Catalogs provide. It’s a completely different (and better) experience than in the Dark Ages of Gradle, when there was no code completion at all. What a time to be alive! :]
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.
At the bottom of libs.versions.toml, add the fourth and final section:
[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"]
The bundles section is where you declare sets of dependencies. Here, you define five dependency bundles according to possible groups in which the individual dependencies might be included. Naming these can become quite difficult as you try to create names that mirror the dependencies they bundle.
Back in the module-level build.gradle.kts, update your dependencies { } block to:
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)
}
It’s a lot cleaner, right? Bundles keep your Gradle scripts simpler, but they can make it harder for developers to be sure of what dependencies they’re including. Again, you should always put effort into correctly naming any bundles you create.
Sync the project and run the app. Everything should work as before, but now with a more modern Gradle scripting.
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.
Without a signature, you’ll be unable to publish your app because it’s necessary to verify you as its owner. You don’t need to sign the debug build because Android Studio does it for you. But you must sign the release build before it can be distributed.
Note: To proceed, you must generate the keystore for your release build. Look at this tutorial to find a step-by-step guide. For a deeper dive into app distribution, check out this book.
When your keystore is ready, add the code below in the android { } block and above the buildTypes { } block (the order of declaration matters) of the module-level build.gradle.kts:
signingConfigs {
create("release") {
storeFile = file("path to your keystore file")
storePassword = "your store password"
keyAlias = "your key alias"
keyPassword = "your key password"
}
}
In the signingConfigs { } block, you specify your signature information for the build types. You’ll want to name them according to the build type they sign to avoid confusion.
Pay attention to the keystore file path. It should be specified with respect to the module directory. In other words, if you were to create a keystore file in the module directory and name it “keystore.jks”, the value you should specify would be storeFile = file("keystore.jks").
Update the buildTypes { } block to sign your release build:
release {
signingConfig = signingConfigs.getByName("release")
// ...
}
Keeping Your Secrets Safe
Remember two important considerations regarding your keystore file:
- Once you’ve published your app to the Play Store, subsequent submissions must use the same keystore file and password, so keep them safe.
- DON’T commit your keystore passwords to a version control system such as GitHub. Anyone can say they own the app if they can access the signing information.
You have a few options to avoid pushing your secrets to a repository. A simple but effective way is to have them in a file you don’t commit. Create a new file with the name keys.properties at the project’s root.
Open it and move all your signing information there:
storeFile = "path to your keystore file"
storePassword = "your store password"
keyAlias = "your key alias"
keyPassword = "your key password"
Then, back at the module-level build.gradle.kts, just between the plugins { } and android { } blocks, add:
val keysPropertiesFile: File = rootProject.file("keys.properties")
val keysProperties = Properties()
keysProperties.load(FileInputStream(keysPropertiesFile))
You’ll need to import these:
import java.io.FileInputStream
import java.util.Properties
This code searches for the new file in the project’s root and maps its contents into keyProperties.
With this, update the signingConfigs { } block:
signingConfigs {
create("release") {
keyAlias = keysProperties["keyAlias"] as String
keyPassword = keysProperties["keyPassword"] as String
storeFile = file(keysProperties["storeFile"] as String)
storePassword = keysProperties["storePassword"] as String
}
}
You get the corresponding values using the keys defined in keys.properties.
Now, it’s time to exclude this file from the version control system. At the project’s root, find and open .gitignore.
Add the name of the secrets file at the end:
# ...
# Keystore confidential information
keys.properties
Sync the project, and you’re done! Now, you can build a release version of the app and keep your secrets safe in your machine.
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.