Gradle Tutorial for Android: Getting Started – Part 2
In this Gradle Build Script tutorial, you’ll learn build types, product flavors, build variants, and how to add additional information such as the date to the APK file name. By Ricardo Costeira.
Sign up/Sign in
With a free Kodeco account you can download source code, track your progress, bookmark, personalise your learner profile and more!
Create accountAlready a member of Kodeco? Sign in
Sign up/Sign in
With a free Kodeco account you can download source code, track your progress, bookmark, personalise your learner profile and more!
Create accountAlready a member of Kodeco? Sign in
Sign up/Sign in
With a free Kodeco account you can download source code, track your progress, bookmark, personalise your learner profile and more!
Create accountAlready a member of Kodeco? Sign in
Contents
Gradle Tutorial for Android: Getting Started – Part 2
25 mins
Using the new Variants API
Enough dwelling in the past. You’ll now write a task to add the build date to your APK name, using the new Variants API. For some added complexity, you’ll do it for release builds only.
You already saw how to create a simple task, so you’ll create an enhanced one now. To start, you have to encapsulate the behavior inside a class. You can have this class in the module-level build.gradle.kts file, but it won’t be visible from the outside that way. If you needed it to be visible from the outside, you could place it in the src/main/java directory of the buildSrc, for example.
For now though, the module-level’s build.gradle.kts is enough. Below the android
block, add:
// 1
abstract class AddCurrentDateTask: DefaultTask() {
// 2
@get:Input
abstract val buildFlavor: Property<String>
@get:Input
abstract val buildType: Property<String>
// 3
@TaskAction
fun taskAction() {
// 4
val oldFileName = "app-${buildFlavor.get()}-${buildType.get()}.apk"
val sourcePath = "${project.buildDir}/outputs/apk/${buildFlavor.get()}/${buildType.get()}/"
// 5
val date = SimpleDateFormat("dd-MM-yyyy").format(Date())
// 6
project.copy {
from(sourcePath + oldFileName)
into(sourcePath)
rename { date + "_" + oldFileName }
}
}
}
This code does a lot of different things:
- You define an abstract class that extends
DefaultTask
. All task classes must extend this class. - You define the task’s properties. You can have different kinds of values (not necessarily
Property
), and different kinds of inputs and outputs, marked by different annotations. Here, you have twoProperty
variables that you’ll use as inputs — hence the@Input
annotation. - You define the task’s action. It doesn’t matter what you name the method, as long as you annotate it with
@TaskAction
. - You build the original file name and source path through the input parameters.
- You get the current date.
- You use copy (one of Gradle’s built-in enhanced tasks) to copy the file into the same directory, but prefixing the file name with the date.
If you were using build.gradle, you could write it either in Groovy or Java. Here’s the Java version, to honor Android’s old and dark times: :]
// 1
abstract class AddCurrentDateTask extends DefaultTask {
// 2
@Input
abstract Property<String> getBuildFlavor()
@Input
abstract Property<String> getBuildType()
// 3
@TaskAction
void taskAction() {
// 4
String oldFileName = "app-${buildFlavor.get()}-${buildType.get()}.apk"
String sourcePath = "${project.buildDir}/outputs/apk/${buildFlavor.get()}/${buildType.get()}/"
// 5
String date = new SimpleDateFormat("dd-MM-yyyy").format(new Date())
// 6
project.copy {
from(sourcePath + oldFileName)
into(sourcePath)
rename { date + "_" + oldFileName }
}
}
}
You need to add in both import java.util.Date
and import java.text.SimpleDateFormat
for build.gradle.kts, and import java.text.SimpleDateFormat
for build.gradle.
One thing to keep in mind is that you could have the class variables as simple String
types. However, declaring them as Property
is a good practice. Property
properties are lazy, which means that Gradle will defer calculating their values until they’re needed. This provides a few advantages, like performance improvements during the configuration phase, Gradle being able to automatically infer task dependencies through their Property
inputs and outputs, among others.
You have your enhanced task, but you can’t use it just like this — it’s an abstract class with abstract properties, after all! Same programming rules apply here: you need to instantiate a class in order to set up those abstract properties.
In order to to so, add the following code just below it in build.gradle.kts:
// 1
androidComponents {
// 2
onVariants(selector().all()) { variant ->
// 3
val addCurrentDateTask = project.tasks.register(
name = variant.name + "AddCurrentDateTask",
type = AddCurrentDateTask::class
) {
// 4
buildFlavor.set(variant.flavorName.orEmpty())
buildType.set(variant.buildType)
}
// 5 and 6
val assembleTaskName = "assemble${variant.name.capitalized()}"
// 7
project.tasks.matching { it.name == assembleTaskName }.configureEach {
finalizedBy(addCurrentDateTask)
}
}
}
Here’s what’s happening:
- With the old Variants API, you’d use
android.
to access Android build properties and artefacts. Using the new API, you wrap the whole thing with thisandroidComponents
. - You use the
onVariants
callback to access the variant objects, passing inselector().all()
so that all available variants are considered. For instance, if you just wanted to run this code for release variants, you could pass inselector().withBuildType("release")
. - You finally create a task of type
AddCurrentDateTask
, using both the class name and the variant’s name to register a different task for each variant. - In the task’s scope, you set the values for the task’s properties.
- You get the variant name and capitalize the word. As you can see, it’s a lot simpler with Kotlin. :]
- You use the capitalized variant name to figure out the
assembleX
task name. For instance,assemblePaidDebug
,assembleFreeRelease
, etc. - Using the value from the previous step, you find the actual task through the
matching
function in thetasks
object. Then, you configure the matched task to befinalizedBy
your task. ThisfinalizedBy
method is a built-inTask
method that tells Gradle to run whatever task (or tasks) you pass into it when the task you call it on finishes.
Here’s the corresponding Groovy (Java!) version:
// 1
androidComponents {
// 2
onVariants(selector().all(), { variant ->
// 3
TaskProvider addCurrentDateTask = project.tasks.register(
variant.getName() + "AddCurrentDateTask",
AddCurrentDateTask.class
) {
// 4
buildType.set(variant.getBuildType())
buildFlavor.set(variant.getFlavorName())
}
// 5
String capitalizedVariantName = variant.getName()
.substring(0, 1)
.toUpperCase()
.concat(variant.getName().substring(1))
// 6
String assembleTaskName = "assemble$capitalizedVariantName"
// 7
project.tasks.matching { it.name == assembleTaskName }.configureEach {
finalizedBy(addCurrentDateTask)
}
})
}
Something important regarding onVariants
: This callback is executed after the variant artifacts are created, and it cannot be changed. In case you need to perform any changes to the artifacts before they’re created, you can use beforeVariants
.
Sync the project. Go to the terminal and remove the generated outputs folder again:
rm -rf app/build/outputs/apk
Next, run the command:
./gradlew assembleDebug
When the command finishes, check out the results with the commands:
ls -R app/build/outputs/apk
You’ll see that you now have generated APKs with the date on their names:
free paid
app/build/outputs/apk/free:
debug
app/build/outputs/apk/free/debug:
01-10-2023_app-free-debug.apk app-free-debug.apk output-metadata.json
app/build/outputs/apk/paid:
debug
app/build/outputs/apk/paid/debug:
01-10-2023_app-paid-debug.apk app-paid-debug.apk output-metadata.json
Creating Custom Plugins
It’s usually a good idea to factor out your code into smaller pieces so it can be reused. Similarly, you can factor out your tasks into a custom behavior for the building process as a plugin. This will allow you to reuse the same behavior in other modules you may add to your project.
To create a plugin, add the following class below the AddCurrentDateTask
class in the module-level build.gradle.kts file:
class AddCurrentDatePlugin : Plugin<Project> {
override fun apply(target: Project) {
target.androidComponents {
onVariants(selector().all()) { variant ->
val addCurrentDateTask = target.tasks.register(
name = variant.name + "AddCurrentDateTask",
type = AddCurrentDateTask::class
) {
buildFlavor.set(variant.flavorName.orEmpty())
buildType.set(variant.buildType)
}
val assembleTaskName = "assemble${variant.name.capitalized()}"
target.tasks.matching { it.name == assembleTaskName }.configureEach {
finalizedBy(addCurrentDateTask)
}
}
}
}
}
In plain old Groovy world, you could go with a Java version:
class AddCurrentDatePlugin implements Plugin<Project> {
@Override
void apply(Project target) {
target.androidComponents {
onVariants(selector().all(), { variant ->
TaskProvider addCurrentDateTask = target.tasks.register(
variant.getName() + "AddCurrentDateTask",
AddCurrentDateTask.class
) {
buildType.set(variant.getBuildType())
buildFlavor.set(variant.getFlavorName())
}
String capitalizedVariantName = variant.getName()
.substring(0, 1)
.toUpperCase()
.concat(variant.getName().substring(1))
String assembleTaskName = "assemble$capitalizedVariantName"
target.tasks.matching { it.name == assembleTaskName }.configureEach {
finalizedBy(addCurrentDateTask)
}
})
}
}
}
As you can see, the code inside target.androidComponents
is exactly the same code you have where you defined the task. That said, you can delete the androidComponents
block you defined earlier.
To use the plugin, you now need to apply it. At the top of the build.gradle.kts, between the plugins
and android
blocks, add:
apply<AddCurrentDatePlugin>()
In a build.gradle file, you would need to do this instead:
apply plugin: AddCurrentDatePlugin
Sync Gradle, delete the apk folder like before, and run ./gradlew assembleDebug
again. You’ll see that you got the same output as before: the APK files with the current date prefixed to their names:
free paid
app/build/outputs/apk/free:
debug
app/build/outputs/apk/free/debug:
01-10-2023_app-free-debug.apk app-free-debug.apk output-metadata.json
app/build/outputs/apk/paid:
debug
app/build/outputs/apk/paid/debug:
01-10-2023_app-paid-debug.apk app-paid-debug.apk output-metadata.json