Does it feel like Android Studio packages and builds your apps, with very little input from you?

Behind the scenes, Android Studio uses the Gradle automated build toolkit, and although it’s possible to run Gradle with very little (if any) manual configuration, Gradle has much more to offer than what’s available out-of-the-box!

In this article, I’ll show you how to modify Android’s build process, by making changes to your Gradle build files, including how to automatically build alternative versions of your app – perfect if you want to release a free and a paid version. Once we’ve covered these build variants and product flavors, I’ll also share how to save yourself a tonne of time, by using Gradle tasks and the Gradle wrapper to automate additional parts of the Android build process.

By the end of this article, you’ll have a deeper understanding of what Gradle is, how it works, and how you can use it to customize Android’s build process, to better suit your specific app.

So, what exactly is Gradle?

Whenever you write code, there’s almost always a series of commands you’ll need to run, in order to convert that raw code into a usable format. When it’s time to create an executable file, you could run each of these commands manually – or you could let a build automation tool do the hard work for you!

Build automation tools can save you a significant amount of time and effort by performing all the tasks associated with building a binary, including fetching your project’s dependencies, running automated tests, and packaging your code.

Since 2013, Google have promoted Gradle as the preferred build automation tool for Android developers. This open source build automation system and dependency manager can perform all the work required to convert your code into an executable file, so you don’t have to manually run the same series of commands every single time you want to build your Android app.

How does Gradle work?

Gradle manages the Android build process via several build files, which are generated automatically every time you create a new Android Studio project.

Android studio - gradle tasks

Instead of Java, XML or Kotlin, these Gradle build files use the Groovy-based domain-specific language (DSL). If you’re not familiar with Groovy, then we’ll be taking a line-by-line look at each of these Gradle build files, so by the end of this article you’ll be comfortable with reading and writing simple Groovy code.

Gradle aims to make your life easier, by providing a set of default settings that you can often use with minimum manual configuration – when you’re ready to build your project, simply press Android Studio’s “Run” button and Gradle will start the build process for you.

gradle tasks in android studio

Despite Gradle’s “convention over configuration” approach, if its default settings don’t quite meet your needs, then you can customize, configure and extend the build process, and even tweak the Gradle settings to perform very specific tasks.

Since the Gradle scripts are contained in their own files, you can modify your application’s build process at any time, without having to touch your application’s source code. In this tutorial, we’ll be modifying the build process using flavors, build variants and a custom Gradle task – all without ever touching our application code.

Exploring the Gradle build files

Every time you create a project, Android Studio will generate the same collection of Gradle build files. Even if you import an existing project into Android Studio, it’ll still create these exact same Gradle files, and add them to your project.

To start to get a better understanding of Gradle and the Groovy syntax, let’s take a line-by-line look at each of Android’s Gradle build files.

1. settings.gradle

The settings.gradle file is where you’ll define all of your application’s modules by name, using the “include” keyword. For example, if you had a project consisting of an “app” and a “secondModule,” then your settings.gradle file would look something like this:

include ':app', ':secondmodule'
rootProject.name='MyProject'

Depending on the size of your project, this file may be considerably longer.

During the build process, Gradle will examine the contents of your project’s settings.gradle file and identify all the modules that it needs to include in the build process.

2. build.gradle (project level)

The project-level build.gradle file is located in your project’s root directory and contains settings that will be applied to all your modules (also referred to as “projects” by Gradle).

You should use this file to define any plugins, repositories, dependencies, and configuration options that apply to every module throughout your Android project. Note that if you define any Gradle tasks within the project-level build.gradle file, then it’s still possible to override or extend these tasks for individual modules, by editing their corresponding module-level build.gradle file.

A typical project-level build.gradle file will look something like this:

buildscript {
   repositories {
       google()
       jcenter()

   }
   dependencies {
       classpath 'com.android.tools.build:gradle:3.5.0-alpha06'

// NOTE: Do not place your application dependencies here; they belong
// in the individual module build.gradle files
   }
}

allprojects {
   repositories {
       google()
       jcenter()

   }
}

task clean(type: Delete) {
   delete rootProject.buildDir
}

This project-level build.gradle file is divided into the following blocks:

  • Buildscript. This contains settings that are required to perform the build.
  • Repositories. Gradle is responsible for locating your project’s dependencies and making them available in your build. However, not all dependencies come from the same repository, so you’ll need to define all the repositories that Gradle should search, in order to retrieve your project’s dependencies.
  • Dependencies. This section contains your plugin dependencies, which are downloaded and stored in your local cache. You should not define any module dependencies within this block.
  • Allprojects. This is where you’ll define the repositories that should be available to all of your project’s modules.

3. build.gradle (module level)

This is the module-level build.gradle file, which is present in every module throughout your project. If your Android project consists of multiple modules, then it’ll also consist of multiple module-level build.gradle files.

Each module-level build.gradle file contains your project’s package name, version name and version code, plus the minimum and target SDK for this particular module.

A module-level build.gradle file can also have its own unique set of build instructions and dependencies. For example, if you’re creating an application with a Wear OS component, then your Android Studio project will consist of a separate smartphone/tablet module and a Wear module – since they’re targeting entirely different devices, these modules with have drastically different dependencies!

A basic module-level build.gradle file will typically look something like this:

apply plugin: 'com.android.application'

android {
   compileSdkVersion 28
   defaultConfig {
       applicationId "com.jessicathornsby.speechtotext"
       minSdkVersion 23
       targetSdkVersion 28
       versionCode 1
       versionName "1.0"
       testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
   }
   buildTypes {
       release {
           minifyEnabled false
           proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro'
       }
   }
}

dependencies {
   implementation fileTree(dir: 'libs', include: ['*.jar'])
   implementation 'androidx.appcompat:appcompat:1.0.2'
   implementation 'androidx.constraintlayout:constraintlayout:1.1.3'
   testImplementation 'junit:junit:4.12'
   androidTestImplementation 'androidx.test.ext:junit:1.1.0'
   androidTestImplementation 'androidx.test.espresso:espresso-core:3.1.1'
}

Let’s take a closer look at each of these sections:

  • apply plugin. This is a list of the plugins required to build this module. The com.android.application plugin is necessary to setup the Android-specific build process, so this is added automatically.
  • android. This is where you should place all of the module’s platform-specific options.
  • compileSdkVersion. This is the API level that this module is compiled with. You cannot use features from an API higher than this value.
  • buildToolsVersion. This indicates the version of the compiler. In Gradle 3.0.0 and higher, buildToolsVersion is optional; if you don’t specify a buildToolsVersion value then Android Studio will default to the most recent version of Build Tools.
  • defaultConfig. This contains options that will be applied to all build versions of your app, such as your debug and release builds.
  • applicationId. This is your application’s unique identifier.
  • minSdkVersion. This parameter defines the lowest API level that this module supports.
  • targetSdkVersion. This is the maximum API level that your application has been tested against. Ideally, you should test your application using the latest API, which means the targetSdkVersion value will always be equal to the compileSdkVersion value.
  • versionCode. This is a numeric value for your application version.
  • versionName. This is a user-friendly string, which represents your application version.
  • buildTypes. By default, Android supports two build types: debug and release. You can use the “debug” and “release” blocks to specify your application’s type-specific settings.
  • dependencies. This is where you’ll define any libraries that this module depends on.

Declaring your project’s dependencies: Local libraries

You can make additional functionality available to your Android projects, by adding one or more project dependencies. These dependencies can be local, or they can be stored in a remote repository.

To declare a dependency on a local JAR file, you’ll need to add that JAR to your project’s “libs” directory.

Android studio - local libraries in gradle tasks

You can then modify the module-level build.gradle file to declare a dependency on this file. For example, here we’re declaring a dependency on a “mylibrary” JAR.

implementation files('libs/mylibrary.jar')

Alternatively, if your “libs” folder contained several JARs, then it might be easier to simply state that your project depends on all the files located within the “libs” folder, for example:

implementation fileTree(dir: 'libs', include: ['*.jar'])

Adding a build dependency: Remote repositories

If a library is located in a remote repository, then you’ll need to complete the following steps:

  • Define the repository where this dependency is located.
  • Declare the individual dependency.

Connecting to a remote repository

The first step, is telling Gradle which repository (or repositories) it needs to check, in order to retrieve all of your project’s dependencies. For example:

   repositories {
       google()
       jcenter()

   }
}

Here, the “jcenter()” line ensures that Gradle will check the JCenter repository, which is a free, public repository hosted at bintray.

Alternatively, if you or your organization maintain a personal repository, then you should add this repository’s URL to your dependency declaration. If the repository is password-protected, then you’ll also need to provide your login information, for example:

   repositories {
      mavenCentral()
      maven {

//Configure the target URL//

        url "http://repo.mycompany.com/myprivaterepo"
      }
      maven {
        credentials {
          username 'myUsername'
          password 'myPassword'
        }
        url "http://repo.mycompany.com/myprivaterepo"
      }

If a dependency is present within multiple repositories, then Gradle will select the “best” version of this dependency, based on factors such as the age of each repository and the static version.

Declaring a remote dependency

The next step is declaring the dependency in your module-level build.gradle file. You add this information to the “dependencies” block, using any of the following:

  • Implementation. This is a normal dependency you need whenever you build your project. An “implementation” dependency will be present across all your builds.
  • Testimplementation. This is a dependency that’s required to compile your application’s test source and run JVM-based tests. When you mark a dependency as “Testimplementation” Gradle will know that it doesn’t have to run tasks for this dependency during a normal build, which can help reduce build time.
  • Androidtestimplementation. This is a dependency that’s required when running tests on a device, for example the Espresso framework is a common “Androidtestimplementation.”

We define a remote dependency, using one of the above keywords, followed by the dependency’s group, name and version attributes, for example:

dependencies {
   implementation fileTree(dir: 'libs', include: ['*.jar'])
   implementation 'androidx.appcompat:appcompat:1.0.2'
   implementation 'androidx.constraintlayout:constraintlayout:1.1.3'
   testImplementation 'junit:junit:4.12'
   androidTestImplementation 'androidx.test.ext:junit:1.1.0'
   androidTestImplementation 'androidx.test.espresso:espresso-core:3.1.1'
}

Generating multiple APKs: How to create build variants

Sometimes, you may need to create multiple versions of your application. For example, you might want to release a free version and a paid version, which includes some additional features.

This is a build task that Gradle can help you with, so let’s look at how you’d modify the build process to create multiple APKs from a single project:

  • Open your strings.xml file and delete your original application name string.
  • Next, define the names of each product flavor that you want to create; in this instance, I’m using:
<string name="app_name_free">My free app</string>
<string name="app_name_paid">My paid app</string>
  • Open your AndroidManifest.xml file and replace android:label=”@string/app_name” with:
android:label="${appName}"
  • Open your module-level build.gradle file and add the following to the “android” block:
   flavorDimensions "mode"

   productFlavors {
       free {
           dimension "mode"
           applicationIdSuffix ".free"
           manifestPlaceholders = [appName: "@string/app_name_free"]
       }

       paid {
           dimension "mode"
           applicationIdSuffix ".paid"
           manifestPlaceholders = [appName: "@string/app_name_paid"]
       }
   }
}

Let’s break down what’s happening here:

  • flavorDimensions. The Android plugin creates build variants by combining flavors from different dimensions. Here, we’re creating a flavor dimension consisting of “free” and “paid” versions of our app. Based on the code above, Gradle will generate four build variants: paidDebug, paidRelease, freeDebug and freeRelease.
  • productFlavors. This specifies a list of flavors and their settings, which in the above code are “paid” and “free.”
  • Free / paid. These are the names of our two product flavors.
  • Dimension. We need to specify a “dimension” parameter value; in this instance, I’m using “mode.”
  • applicationIdSuffix. Since we want to create multiple versions of our app, we need to give each APK a unique app identifier.
  • manifestPlaceholders. Each project has a single Manifest file containing important information about your project’s configuration. When creating multiple build variants, you’ll typically want to modify some of these Manifest properties at build time. You can use the Gradle build files to specify unique Manifest entries for each build variant, which will then be inserted into your Manifest at build time. In the above code, we’re modifying the “appName” value depending on whether Gradle is building the free or the paid version of our app.

Creating a custom Gradle task

Sometimes you may need to customize the build process, using Gradle tasks.

A task is a named collection of actions that Gradle will execute as it performs a build, for example generating a Javadoc. Gradle supports plenty of tasks by default, but you can also create custom tasks, which can come in handy if you have a very specific set of build instructions in mind.

In this section, we’ll be creating a custom Gradle task that will iterate through all of our project’s build variants (paidDebug, paidRelease, freeDebug and freeRelease), create a date and time stamp, and then append this information to each generated APK.

Open your module-level build.gradle file and add the following:

task addDateAndTime() {

//Iterate through all the output build variants//

   android.applicationVariants.all { variant ->

//Iterate through all the APK files//

       variant.outputs.all { output ->

//Create an instance of the current date and time, in the format specified//

           def dateAndTime = new Date().format("yyyy-MM-dd:HH-mm")

//Append this information to the APK’s filename//

           def fileName = variant.name + "_" + dateAndTime + ".apk"
           output.outputFileName = fileName
       }
   }
}

Next, we need to tell Gradle when it should execute this task. During a build, Gradle identities everything it needs to download and all the tasks it has to execute, and arranges them in a Directed Acyclic Graph (DAG). Gradle will then execute all of these tasks, according to the order defined in its DAG.

For my app, I’m going to use the “whenReady” method, which ensures our task will be called once the DAG has been populated, and Gradle is ready to start executing its tasks.

Add the following to your module-level build.gradle file:

//Execute this task//

gradle.taskGraph.whenReady {
   addDateAndTime
}

Let’s put our custom task and our build variant code to the test, by building this project using a Gradle command.

Building your project with the Gradle wrapper

You issue Gradle commands using the Gradle wrapper (“gradlew”). This script is the preferred way to start a Gradle build, as it makes the execution of the build independent from your version of Gradle. This separation can be useful if you’re collaborating with others who may not necessarily have the same version of Gradle installed.

When issuing your Gradle wrapper commands, you’ll use “gradlew” for Unix-like operating systems, including macOS, and “gradlew.bat” for Windows. I have a Mac, so I’ll be using “gradlew” commands.

You can issue Gradle commands from inside Android Studio:

  • In the Android Studio toolbar, select “View > Tools Windows > Terminal.” This opens a Terminal panel along the bottom of the IDE window.
  • Enter the following command into the Terminal:
./gradlew build

Android Studio should look something like this:android studio - building projects with gradle wrapper

  • Press the “Enter” key on your keyboard. Gradle will now build your project.

Gradle stores all of the generated APKs in your project’s app/build/outputs/apk directory, so navigate to this directory. The “APK” folder should contain several folders and subfolders; make sure that Gradle has generated an APK for each of your build variants, and that the correct date and time information has been added to each file.

gradle wrapper example

What other Gradle tasks are available?

In addition to any custom tasks you might create, Gradle supports a list of predefined tasks out-of-the-box. If you’re curious to see exactly what tasks are available, then:

  • Open Android Studio’s Terminal window, if it isn’t already open (by selecting “View > Tools Windows > Terminal” from the Android Studio toolbar).
  • Type the following into the Terminal:
./gradlew -q tasks
  • Press the “Enter” key on your keyboard.

This “tasks” task will now run, and after a few moments the Terminal will display a list of all the tasks available for this project, complete with a short description of each task.

Getting more out of Gradle: Adding plugins

Gradle ships with a number of plugins pre-installed, but you can further extend Gradle by adding new plugins. These plugins make new tasks available to your Android projects, for example the Java plugin includes tasks that allow you to compile Java source code, run unit tests and create a JAR file, such as “compileJava,” “compileText,” “jar,” “javadoc,” and “clean.”

To apply a plugin, add the “apply plugin” declaration to your module-level build.gradle file, followed by the name of the plugin. For example, here we’re applying the Java plugin:

apply plugin: 'java'

If you’re curious to see what plugins are available, then check out Gradle Plugin search, which provides a comprehensive registry of Gradle plugins.

The Gradle Kotlin DSL

By default, you’ll write your Gradle build scripts using the Groovy DSL, but if you’re one of the many developers who’ve adopted Kotlin for Android development, then you may prefer to write your build scripts in Kotlin instead.

Unlike Groovy, Kotlin is a statically typed programming language, so if you make the switch then your build files will be compatible with Android Studio’s autocompletion and source code navigation features. Plus, moving from Groovy to Kotlin means you’ll be using the same programming language across your project, which can make development more straightforward – particularly if you’re not overly familiar with Groovy!

If you want to start writing your build logic in Kotlin, then you’ll need to setup the Gradle Kotlin DSL and follow the instructions in the migration guide.

Wrapping up

In this article, we explored Android Studio’s build automation and dependency management tool. We examined how Gradle automates the build process out-of-the-box, and how you can modify the build process by editing your project’s Gradle build files, including creating custom Gradle tasks, and generating multiple build variants from a single project.

Have you extended Gradle to automate other parts of the Android build process? Let us know in the comments below!

Comments
Read comments