WHAT'S IN THIS CHAPTER?
Android Studio has introduced many changes to the Android development lifecycle that are limited not only to the IDE and tools but also to the build system. A Gradle-based build system was introduced with the initial release of Android Studio.
Prior to Android Studio, the Android ecosystem did not have one default build system. Some developers relied on Apache Ant scripts, whereas other developers preferred more sophisticated Maven builds. Another popular way to build Android apps uses mk files, which were widely used by developers using the Native Development Kit (NDK).
A common and yet simple approach followed by developers was to copy libraries (jar or aar files) into the libs folder and let Eclipse build tools to handle the build. However, this approach created problems when the project was integrated with source control systems. Although Maven addressed most of the dependency and automated test/build issues, it introduced another layer of complexity and performance problems.
In this chapter, you learn how to use Gradle effectively to control builds, manage dependencies and, even better, how to add custom tasks by writing your own plugins.
The Gradle build system was first released in 2007. Unlike Maven, which relies on XML, Gradle uses a Groovy-based domain-specific language for project configuration.
Basically, Gradle offers a simpler syntax to declare dependencies and build properties. It can easily be extended and used for complicated tasks and large projects. Gradle uses a directed acyclic graph to determine the order of the tasks. Gradle is widely used to build Java, Scala, and, of course, Groovy projects.
Gradle met Android in the release of Android Studio. Android Studio comes with a Gradle wrapper for seamless integration with Gradle. The Android build system offers an Android Plugin for Gradle, which not only takes care of all IDE-based compiles and builds but Gradle can also run standalone even when Android Studio is not installed. This allows Android projects to be easily integrated with Continuous Integration servers such as Hudson and Jenkins.
Gradle build configuration is defined in the build.gradle files in Android projects. Build files exist both in modules and in the project to configure properties related to the given scope. A build file typically contains Android plugins to configure your project.
Project scope is defined in the build.gradle file and is mainly used to declare project-wide repositories and dependencies, as shown in Listing 6.1.
This build file adds mavenCentral
as a repository and the classpath
dependency for the Android Plugin for Gradle version 1.3.
In addition to the project-scope build.gradle file, each module has its own build.gradle file for project-specific configuration. The module-scope build file is where the Android Plugin for Gradle really kicks in and works its magic. The module build file offers the user numerous options, such as the capability to override the manifest settings and to change the app package, source, resources, and ID.
The Android Plugin for Gradle can configure the following:
compileSdkVersion
and buildToolsVersion
defaultConfig
, which can override applicationId
, minSdkVersion
, targetSdkVersion
, and test informationListing 6.2 shows a typical module Gradle file.
The first part of the build script declares repositories and dependencies for the module. As discussed previously, you can use this configuration on both the project and module scope. These dependencies are Gradle dependencies and should not be mixed with Android project dependencies. In this example, we simply add the Android plugin for Gradle version 1.3.0 to make Gradle and Android Studio work in harmony to build our Android project.
Next, you need to apply the Android Plugin for Gradle you have just added as a dependency. The apply plugin:
task followed by the plugin name does the magic. You can also choose to apply other Gradle plugins, which would offer other tasks and functionality. This is covered in the “Writing Your Own Gradle Plugin” section later in this chapter.
Once the Android Plugin for Gradle is applied, you can declare Android dependencies for the given module. In this example, you use four support libraries from Google, which provides support to use new widgets, APIs and libraries on older versions of Android. With the help of support libraries, you can keep your minSdk
level to target older versions while being able to use cool newly released functionality.
You are almost there; finally, you can configure the Android plugin for Gradle in the Android
block. The Android Plugin for Gradle offers many capabilities, which we cover in this chapter.
Listing 6.2 gives a basic example that sets SDK and tool versions as well as declaring a version of Java for the compile options. Although you may not need to tweak those configurations daily, you definitely need to learn the details in order to have full control of your project. For example, Retrolambda, a popular third-party open source library that lets you use Java 8 syntax on Android, requires you to set the Java version to 8 in order for the Android Plugin for Gradle and Android Studio to function properly.
Gradle offers a great way to handle project dependencies without the need to copy source code from project to project. Even better is that Gradle's way to declare dependencies is very simple when compared to Maven, yet still very flexible and customizable. Gradle really shines when it comes to dealing with dependencies.
Gradle offers different scopes for declaring dependencies:
Working with external dependencies might be the most important offering of build systems. Unlike local dependencies, external dependencies are available on repositories.
The most common approaches for dealing with external dependencies are as follows:
Gradle resolves external dependencies within given repositories, either public or private. Gradle allows you to work with a range of versions of the dependency, or you can target the specific version you want to work with. In addition to this flexibility, Gradle also offers much simpler syntax to declare dependencies when compared to XML-based Maven syntax.
A typical Gradle dependency is declared with the library name followed by the version number. The following code snippet adds a supported library as a dependency:
dependencies {
compile "com.android.support:support-v4:+"
}
The “+
” character in the example tells Gradle that any version of support library is okay for the project. In such a case, Gradle will look for the most recent available version of the given project.
However, most of the time you need to declare a specific version of the target library to ensure compatibility and reproducibility. For example, to have Gradle download version 23.1.0 of the target library, type the version number as shown in the following code:
dependencies {
compile "com.android.support:support-v4:23.1.0"
}
On the other hand, you may be looking to get minor version updates while still using a major version. For example, you might want the most recent update based on version 23.1 (i.e., 23.1.X). Once again Gradle lets you to use the “+
” character for fine-tuning version numbers.
dependencies {
compile "com.android.support:support-v4:23.1.+"
}
This example will retrieve the most recent support library based on version 23.1 but will not move to version 24 even if it is available. You can use the “+
” sign for any digit or digits in the version number.
Although using Gradle is very easy and straightforward, you may need to have more control over the transitive dependencies. Gradle dependencies introduce their own dependencies, which would either form a tree or graph until a dependency does not need another dependency.
Usually transitive dependencies, which form a tree structure, do not impose any problem because each dependency has only one parent dependency. However if the transitive dependencies form a graph in which one dependency has more than one parent that requires that dependency, you may need to tweak dependency settings in order to provide the most suitable version for the needed dependency. Let's assume your project has two dependencies, A and B, which both require the dependency of C. If either A or B declares an incompatible version of C for the other, you would need to exclude the dependency from the graph.
This may also be an issue if your project already has a newer version of a dependency that is needed by another dependency. In the following code example, the project uses support library v4 23.1; let's assume dependencyA
introduces an older version of the given support library.
dependencies {
compile "com.android.support:support-v4:23.1.+"
compile ("com.dependencyA:1.+") {
exclude group: 'com.android.support', module: ' support-v4'
}
}
This way, you ask dependencyA
not to include support-v4
because you know a newer version is already there.
As a best practice, you would need to upload jar or aar dependencies to private repositories even if they are not available on public repositories. However, if you still need to add a local jar or aar file as a dependency, you can point to the local library within parenthesis.
dependencies {
compile "com.android.support:support-v4:23.1.+"
compile files ("com.dependencyA_local.jar")
}
Although having a local binary file dependency is highly discouraged, you may need local modules, which already exist in source control, as dependencies. Gradle can easily declare dependencies between modules. The following example declares moduleA
as a dependency of the project.
dependencies {
compile "com.android.support:support-v4:23.1.+"
compile :com.moduleA
}
Real projects might have a mixture of local module dependencies and dependencies from repositories.
On occasion, you may not be able to find a Gradle reference to a dependency, but you can find the Maven reference. This used to be a common problem in the early days of the Gradle–Android flirtation.
Converting a Maven reference into a Gradle reference is pretty easy and straightforward. The example in Listing 6.3 declares for log4j-api
and log4j-core
version 2.4.1.
To convert a Maven dependency to Gradle, you must start with mapping the groupId
with a group, followed by mapping name with name and, finally, version with version, as shown in the following code.
dependencies {
compile group: 'org.apache.logging.log4j', name: 'log4j-api', version: '2.4.1'
compile group: 'org.apache.logging.log4j', name: 'log4j-core', version: '2.4.1'
}
Gradle also offers a simpler syntax, which allows a colon (:
) to be used between each property without using the property names.
dependencies {
compile 'org.apache.logging.log4j:log4j-api:2.4.1'
compile 'org.apache.logging.log4j:log4j-core:2.4.1'
}
The “:
” notation is the most accepted and widely used dependency declaration syntax in the Android ecosystem. Once you get used to Gradle dependency syntax, you could easily convert any Maven-based project or dependency to Gradle.
Gradle is great but what makes it better for Android is the Android Plugin for Gradle. So far in this chapter, you have used many features and properties of Android Plugin for Gradle. This section covers Android Plugin for Gradle in detail.
The new Android build system comes with Android Plugin for Gradle integrated with Android Studio. It can also be run independently, so it can easily be integrated with continuous integration servers. Either way, the build system will build the same APK described in the build.gradle file.
The “Anatomy of Gradle” section earlier in this chapter covered the basics of the Android Plugin for Gradle. As an Android developer, you may never develop and build applications without customizing the Android plugin, although it introduces many great capabilities without customization.
The build.gradle file holds the build configuration for your project. You have already seen how to add dependencies, but the Android plugin for Gradle offers much beyond that.
The Android Plugin can control and configure the following items in your project:
AndroidManifest.xml
.The Android build system is based on a set of hierarchical build tasks, which invoke child tasks in order to complete the whole build flow.
The following items are the top-level build tasks described by the Android build system.
Flavors, or build variants, are a flexible option provided by the Android build system. By default, each app comes with two different flavors: debug and release.
The following additional flavors can be defined for different purposes:
To create a new flavor, you need to add your flavor definitions to the build.gradle file. Listing 6.4 declares two flavor versions for your app: demo (a free version) and full (the paid version).
You can also add a flavor by selecting the Edit flavors option from the Build menu. Click the plus (+) sign to define a new flavor. The new flavor with a default name will be created with empty options such application id, min sdk, and so on, which can be used to override the settings of the application defaults.
You have created a flavor that can be used while packaging your app and because you changed the app ID of both apps, they can be deployed to the Play Store as different apps. Now let's look at the changes needed for different app IDs.
First let's add a source file for each flavor:
demo
in the demo folder and full
in the full folder.Notice that the demo folder turned blue, indicating that you can use the demo flavor code in the src/main
folder. If you select full, you can use the code inside the src/full
folder.
Create a rex/drawable
folder inside each flavor and copy a different ic_launcher.png
to each to override the default icon. Notice the small yellow icon on the res folder, which shows that it's part of the active app's resources.
With the help of flavors, you can customize anything between builds, such as app ID, sources, resources, SDK version, UI layouts, assets—basically anything inside the main and flavor folders.
ProGuard is another great feature integrated into the Android build system. ProGuard is a tool for both security and performance. Before ProGuard, most Android applications were unprotected against decompilation and reverse engineering. ProGuard obfuscates your code by renaming classes, methods, and fields and removing unused code. The resulting APK is not only harder to reverse engineer but also smaller in size.
ProGuard is enabled by default but only for the release version of your app. To enable ProGuard, the minifyEnabled
property must be set to true
in buildTypes
.
buildTypes {
release {
minifyEnabled true
proguardFiles getDefaultProguardFile('proguard-android.txt'),
'proguard-rules.pro'
}
}
To configure ProGuard, you have two options. Android Studio adds proguard-rules.txt
to the root of the project at project creation. This configuration file holds global ProGuard settings for whole modules in your project. For module-based configuration, proguard-rules.pro
can be used.
To configure ProGuard not to obfuscate some part of the project, use the -keep
option. Any class, interface, method, or field can be kept out of obfuscation with the -keep
option.
-keep class com.expertandroid.chapter6.MyActivity
Listing 6.5 shows ProGuard options for the popular okhttp library from Square. To keep attributes, use -keepattributes Signature
and -keepatributes *Annotation*
. Refer to the ProGuard documentation for the full list of attribute settings. To keep classes and interfaces out of obfuscation, use keep class
and keep interface
.
The previous section in this chapter covered product flavors. ProGuard also supports flavor-specific configuration. To add a new ProGuard configuration to the previous flavor example, you can add full-rules.pro
.
productFlavors {
demo {
applicationId = "com.experandroid.chapter6.demo"
}
full {
applicationId = " com.experandroid.chapter6.full"
proguardFile 'full-rules.pro'
}
}
Using ProGuard is essential for applications that will be released to the public. ProGuard not only protects your code against reverse engineering but also helps with securing your app.
As mentioned previously in this chapter, Gradle executes all tests during the check task. The Android build system will execute both Android tests (built on JUnit) and JUnit tests. Chapter 8 covers testing and integrating tests; Chapter 10 covers Gradle and continuous integration.
The Android Plugin for Gradle is basically a Gradle plugin. Gradle plugins can be written with Java, Scala, and, of course, with Groovy. Each plugin can be put to work using the apply
keyword with your plugin's name.
Writing a plugin of your own can customize the build process the way you want and is surprisingly something very easy to achieve.
Gradle plugins implement the Plugin<
Project
>
interface; the apply (Project p)
method needs to be implemented. As you might guess from the syntax, the targeted project is passed as a parameter to the apply
method. Listing 6.6 adds the task customTask
to your project, which currently only prints a log about starting the execution.
Now that your plugin is ready, you need to call apply
to use it.
Apply plugin: CustomPlugin
Once the build process has executed, your plugin will run and print your log message. Alternatively, you can choose to run Gradle from the command line.
Gradle –q customTask
Let's add some more functionality to your plugin. Previously, you implemented different build flavors. Listing 6.7 will list all product flavors declared in your project.
At this point you've added your plugin source code to the build script. This a very simple way to add a new plugin, but your new plugin is only available in your project. To promote reusability, create a separate project for the plugin that will be packaged as a jar and can easily be added to other projects.
Extending the Android plugin can be useful and painful at the same time. The Android Plugin is just another Gradle plugin and is subject to change. Be aware that any change to the Android Plugin for Gradle may break your plugin's functionality. Listing 6.8 simply extends the Android plugin while displaying a simple log message.
Another great way to extend the Android plugin is to use afterEvaluate
, which adds the defined closures to the end of the configuration phase. For example, let's say you want to create a report after running your extended task example:
afterEvaluate { project ->
project.tasks.extendedAndroidPlugin << {
println 'Your lint report is being generated'
}
}
afterEvaluate
can be used for adding hooks into any tasks. The execution order is based on first-in first-out, and the plugin does not have any control on the execution order.
This chapter dug into some of the specifics of Gradle. We started with the basic syntax of Gradle and then focused on how to manage remote, local, and even Maven dependencies through Gradle.
In our exploration of the Android Plugin for Gradle, we showed you how to change its configuration, control the build tasks, and create flavors for different build settings from the same code base. Next, we moved to another important topic, ProGuard, and showed how to configure ProGuard for specific needs.
Finally, we covered the Gradle plugin system by showing how to write a Gradle plugin as well as extending Android Plugin for Gradle.
Entire books have been written about Gradle; this chapter's coverage really just scratches the surface of what is possible with advanced knowledge of the Groovy and Gradle lifecycles