Chapter 6
Android Build System

WHAT'S IN THIS CHAPTER?

  • Android build system: Gradle
  • Using Gradle
  • Managing dependencies
  • Configuring Android Plugin for Gradle
  • Writing a Gradle plugin

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.

USING GRADLE

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.

Anatomy of Gradle

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:

  • Android settings, such as compileSdkVersion and buildToolsVersion
  • Product flavors and defaultConfig, which can override applicationId, minSdkVersion, targetSdkVersion, and test information
  • Build types such as debug, version name, and ProGuard configuration
  • Dependencies such as external, local, or other modules

Listing 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.

DEPENDENCY MANAGEMENT WITH GRADLE

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:

  • Compile—Declares dependencies that are required to compile the project from source code.
  • Runtime—Declares dependencies that are needed during the execution of the compiled code. Typically, the dependency is packaged with your compiled code but is not used during compilation.
  • testCompile—Declares dependencies that are only required during the compilation of test source but will be left out while running the app.
  • testRuntime—Declares dependencies that would be required while running the tests. Once again, they will be left out while running the app.

External 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:

  • Committing compiled binaries, which result in waste of disk space in source control, waste of network resources during commits, and waste of both when upgrading to a newer version.
  • Copying/cloning source code into project, which results in a copy/paste fork of the target library. This approach will result in a project that is very hard to upgrade and is prone to cloning the bugs in the project.

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.

Local Dependencies

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.

Legacy Maven Dependencies

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.

ANDROID PLUGIN FOR 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.

Configuring Android Plugin for Gradle

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.

Build Configuration

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:

  • Dependencies—Dependency management is an important part of build systems, which enable dynamic, versioned, and transitive dependency management. We covered dependency management earlier in this chapter.
  • Android Manifest options—Android Manifest is the heart of every Android application. The most trivial configuration details, such as application ID, supported compile/target/minimum SDK version, and application version info, will end up in AndroidManifest.xml.
  • Build type—The Android build system is designed to build different binaries depending on your platform or application properties. This gives you the flexibility to build different applications or versions from one code base as well as different build options of the same code, such as debuggable or obfuscated builds.
  • Signing—Applications need to be signed to be eligible to upload to the Google Play Store. By configuring signing settings, the build system can build ready-to-publish APKs without further user interaction.
  • Testing—The build system can run your test during build and also package an APK file containing the test resources in your project.
  • ProGuard—The flexibility of running in a virtual machine such as JVM, ART, Dalvik, and so on introduces easy-to-obfuscate portable byte code instructions. Prior to ProGuard, many Android applications suffered from decompilation and reverse engineering. ProGuard obfuscation not only is necessary for security but also shrinks the final APK size because variable, method, and class names are shortened.

Build Tasks

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.

  • Assemble—Builds the project output, including code generation tasks and compilation
  • Check—Runs checks (such as lint) and tests
  • Build—Runs both assemble and check
  • Clean—Cleans up the project

Flavors

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:

  • Variations of an app such as free, demo, or paid versions.
  • Apps with different app IDs from the same code base. Gradle is flexible enough to describe flavor-specific source and resource folders.
  • Binaries for different CPU architectures such as ARM, x86, or MIPS.

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:

  1. Navigate to the src folder under your application and create two folders, demo and full, in addition to the main folder that's already there.
  2. Create a folder named java and place your default package structure inside that. Each folder named for a flavor will inherit all the code inside the main folder but will also add the code inside its own folder.
  3. Create a class with a constant field demo in the demo folder and full in the full folder.
  4. Now it is time to select a flavor and use our flavor-specific code. Click the Select build variant option from the Build menu to open the window shown in Figure 6.1. Select demo from the bottom left of your IDE.
    Build variant selection screen.

    Figure 6.1 Build variant selection

    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.

  5. Add different resources in your flavors.

    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

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.

Automated Tests

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.

GRADLE PLUGINS

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 Your Own Gradle Plugin

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 Android Plugin for Gradle

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.

SUMMARY

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

..................Content has been hidden....................

You can't read the all page of ebook, please click here login for view all page.
Reset
3.149.238.159