[Learn Android Studio 汉化教程]第十三章:Gradle

When Android was initially released, Google developed a build system based on Apache Ant as part of the SDK. Ant is an established technology with several years of enhancements and a huge community of contributors. Over the years, other build systems emerged, some becoming popular with thriving communities. Among these build systems, Gradle emerged as the next evolutionary step for Java development. This chapter explores Gradle and gives examples of how to best use it for developing and maintaining your Android apps.
Before expounding on Gradle, this chapter explains what a build system is and why you might need improvements on the existing build system. The process of creating apps or any software has historically involved writing code in a particular programming language and then compiling that code into an executable form.
<翻译>最初Android 发布的时候,Google 便开发了一个基于Apache Ant的组建系统,并作为SDK的一部分。随着近些年不断的加强和大量的社区贡献者,Ant已成了一个比较成熟的技术。这些年来,也出现了其他的组建系统,一些变得流行起来,社区活跃起来。在这些组建系统中,Gradle出现了,就被看作为JAVA开发中下一代进步的组建系统。本章节探索了Gradle和举例说明如何最大限度用于开发和维护你的Android应用程序。在解释Gradle之前,本章解释了什么是组建系统,为何需要改善现存的组建系统。创建app或者其他软件这一过程都涉及到在特定的编程语言编写代码、然后编译这些代码称为一个可执行的格式。
Note There is a lab later in this chapter which explains the use of Gradle in a multi-module project. We invite you to clone the lab for this project using Git in order to follow along, though you will be recreating this project with its own Git repository from scratch. If you do not have Git installed on your computer, see Chapter 7. Open a Git-bash session in Windows (or a terminal in Mac or Linux) and navigate to C:\androidBook\reference\ (If you do not have a reference directory, create one. On Mac navigate to /your-labs-parent-dir/reference/) and issue the following git command:
git clone https://bitbucket.org/csgerber/gradleweather.git GradleWeather.
<翻译>注:对于多模块工程,有一个lab,解释了Gradle的使用。为了跟进,我们邀请你使用Git克隆这个工程的lab,尽管你将从零开始使用它自己的Git仓库,重建这个工程中。如果你还没有在你的电脑上安装Git,参考第7章,在windows打开一个Git-bash(命令行)会话(或者在Mac、Linux打开终端)目录切换到 C:\androidBook\reference\ (若没有则新建,在Mac则切换到 /your-labs-parent-dir/reference/)并执行以下git命令:git clone https://bitbucket.org/csgerber/gradleweather.git GradleWeather.

Modern software development involves not only linking and compilation but also testing,
packaging, and eventual distribution of your end product. A build system fills these
emergent needs by providing the necessary tools to accomplish these tasks. Consider the
list of emergent requirements many developers face today: supporting variations of the end
product (a debug version, a release version, a paid version, and a free version), managing
third-party software libraries and components included as part of the product, and adding
conditions to the overall process based on external factors.
The Android build system was originally written in Ant. It was based on a rather flat project
structure that didn’t offer much support for things such as build variations, dependency
management, and publishing the output of a project to a central repository. Ant has an XML
tag-based programming model that is simple and extensible, though many developers find
it cumbersome. In addition, Ant uses a declarative model. Although Ant follows some of the
principles of functional programming, many developers are comfortable with the imperative
model common in most modern programming languages. In short, things like loop constructs,
conditional branching, and reassignable properties (the Ant equivalent of variables) are not
directly supported.
A Gradle build is written in the Groovy programming language, which builds on top of Java’s
core runtime and API. Groovy loosely follows Java’s syntax which, when combined with its
syntax, lowers the learning curve. This adds to Groovy’s appeal because it is so close to the
Java language that you can port most of your Java code to Groovy with minimal change.
This also adds to Gradle’s strengths because you can add Groovy code at any point in
a Gradle build. With Groovy syntax being so close to Java, you can practically add Java
syntax in the middle of a Gradle build script to achieve the effect you want. Groovy also
adds closures to Java’s syntax. A closure is a block of code surrounded by curly braces
that can be assigned to a variable or passed to a method. Closures are a central part of the
Gradle build system that you’ll learn more about shortly.
Gradle Syntax
Gradle build scripts are actually Groovy script files that follow certain conventions. As such,
you can include any valid Groovy statement in your build. However, most are composed of
statements that follow simple syntax based on blocks. The basic structure of Gradle build
scripts comprises configuration and task blocks. Task blocks define code that is executed
at various points during the build. Configuration blocks are special Groovy closures that
add properties and methods to underlying objects at runtime. You can include other types
of blocks in your Gradle build scripts, but these are outside the scope of this book. You will
mostly work with configuration blocks, because the tasks involved in a Gradle Android build
are already defined. Configuration blocks take the following form:
label {
//Configuration code...
}
where label is the name of a specific object, and the curly braces define the configuration
block for the object. The code inside the configuration block takes the following form:
{
stringProperty "value"
numericProperty 123.456
buildTimeProperty anObject.someMethod()
objectProperty {
//nested configuration block
}
}
The block can access the individual properties of the object and assign values to them.
These properties can be strings, numerics, or objects themselves. String properties can take
literal values or values returned from Groovy method invocations. Literal values follow rules
similar to Java. However, string literals may be indicated with double quotes, single quotes,
or any other means that Groovy uses to represent strings. Object properties use nested
blocks to set their individual properties.
Gradle build scripts follow a certain standard. Under this standard, the top of the build script
is where you declare Gradle plug-ins. These are components written in Groovy/Gradle that
add to or extend Gradle features. A plug-in declaration follows the form of apply plugin:
'plugin.id', where plugin.id is the identifier for the Gradle plug-in you wish to use.
The Gradle tasks and configuration blocks follow the plug-in definitions in any order. It is
customary to declare the Android plug-in, which is an object available in the build script via
the android property. The project’s dependencies usually follow the Android configuration.
The dependencies list all of the libraries that support any external APIs, declared plug-ins,
or components that your project uses. The following is an example of a Gradle build script.
You’ll learn more about the specifics later.
Listing 13-1. A Gradle Build Script Example
apply plugin: 'com.android.application'
android {
compileSdkVersion 20
buildToolsVersion '20.0.0'
defaultConfig {
applicationId "com.company.package.name"
minSdkVersion 14
targetSdkVersion 20
versionCode 1
versionName "1.0"
}
buildTypes {
release {
minifyEnabled false
proguardFiles getDefaultProguardFile('proguard-android.txt'),
'proguard-rules.pro'
}
}
}
dependencies {
compile fileTree(dir: 'libs', include: ['.jar'])
compile 'com.android.support:support-v4:20.+'
}
IntelliJ Core Build System
Android Studio is built on the IntelliJ IDEA platform and inherits most of its functionality from
IntelliJ’s core. It adds further Android-specific functionality to the core in the way of plug-ins.
A plug-in is a software component that can be downloaded from the IntelliJ plug-in
repository and installed or removed in a pluggable fashion, almost like Lego blocks. These
plug-ins serve to enhance IntelliJ’s functionality, and each one can be enabled or disabled
by using the Settings window. The IntelliJ Gradle plug-in melds IntelliJ’s core build system
to the Gradle build system. Actions that would usually trigger an application build instead
invoke Gradle while the output is fed back through the IntelliJ core and formatted in a
manner that is familiar to IntelliJ.
Gradle Build Concepts
The Gradle build system is a general tool for building software packages from a collection
of source files. It defines some high-level concepts for building software that are consistent
for most projects. The most common concepts include projects, source sets, build artifacts,
dependency artifacts, and repositories. A project is a location on your hard drive that
contains a collection of all the project source code. A Gradle build will have a set of source
files that are represented as source sets. It will optionally have a list of dependencies.
These dependencies are software artifacts that can include anything from JAR or ZIP
archives to text files, to precompiled binary files. The artifacts are fetched from a repository.
A repository is a collection of artifacts that are organized in a special way to allow the build
system to find a given artifact. It can be a location on your hard drive or a special web site
that organizes artifacts by a standard convention. Each artifact can optionally include its own
set of dependencies that may be included in the build. The build combines the source sets
with the dependency artifacts to generate build artifacts. The build can optionally publish
these artifacts to a repository to make them available for other developers or teams.
Gradle Android Structure
Gradle Android projects have a hierarchical structure that nests subprojects or modules
in individual folders under the project root. This is analogous with how Android Studio’s
IntelliJ underpinnings have traditionally managed projects. With both Gradle and the IntelliJ
environment, a simple project could contain a single module, named app, and a few other
folders and files, or it could contain multiple modules with various names. The similarities
end there, as Gradle allows infinite nesting of modules. In other words, a project could
contain a module that also contains nested modules. As a result, the Android Studio build
system runs Gradle under the covers. The following list provides a brief description of the
individual files and folders contained in a typical Gradle Android project. This list focuses
mainly on the files you would be concerned with changing:
 .gradle: Temporary Gradle output, caches, and other supporting
metadata are stored under this folder.
 app: Individual modules are nested, by name, in a folder under the root.
Each module folder contains a Gradle project file that generates output
used by the main project. The simplest Android project will include a
single Gradle project that generates an APK artifact.
 gradle: This folder contains the Gradle wrapper. The Gradle wrapper is
a JAR file that contains a version of the Gradle runtime compatible with
the current project.
 build.gradle: The overall project build logic lives in this file. It is
responsible for including any required subprojects and triggering the
build of each one.
 gradle.properties: Gradle and JVM properties are stored in this file.
You can use it to configure the Gradle daemon and manage how Gradle
spawns JVM processes during the build. You can also use this file to
help Gradle communicate when on a network with a web proxy.
 gradlew/gradlew.bat: These files are the operating system–specific files
used to execute Gradle via the wrapper. If Gradle is not installed on your
system, or if you don’t have a version compatible with your build, then it
is recommended to use one of these files to invoke Gradle.
 local.properties: This file is used to define properties specific to the
local machine, such as the location of the Android SDK or NDK.
 settings.gradle: This file is required with multiproject builds, or any
project defining a subproject. It defines which subprojects are included
in the overall build.
 Project.iml, .idea, .gitignore: You may notice any/all of these files in
the root directory upon creating a new project in Android Studio. While
these files (with the exception of .gitignore discussed in Chapter 7) are
not part of the Gradle build system, they are constantly updated as you
make changes to your Gradle files. They impact the way Android Studio
“sees” your project.
 build: All of the Gradle build output falls under this folder. This includes
the generated source. Gradle is organized and intentional in keeping all
output to a single folder. This simplifies the project, as the list of things
to exclude from your version control is less daunting, while cleanup is a
matter of deleting a single folder.
Project Dependencies
Gradle simplifies dependency management in a way that makes it easy to use and
reuse code across several projects, regardless of the platform. When a project grows in
complexity, it makes sense to break it into separate pieces, which are referred to as libraries
in Android. These libraries can be developed independently in separate Gradle projects or
collectively in a multimodule project in Android Studio. Because Android Studio handles
modules as Gradle projects, the lines can really blur, which leads to powerful possibilities for
code sharing. Calling objects in code developed by another team across the globe is nearly
identical to calling objects that exist locally in a separate module! When code in your project
needs to invoke code in another Gradle project or in another Android Studio module, you
need only to declare a dependency in your main project to tie the code together. The end
result is a seamless stitching together of separate pieces into a cohesive application.
Consider a simple case in which your application needs to invoke a method, bar, in an
external class Foo. With classic Android tools, you would have to locate the project that
defines class Foo. This could involve downloading from the Web, or even an arduous web
search if you aren’t quite sure of the project location or home page of the project. You would
then have to do the following:
 Save the downloaded project to your development computer
 Possibly build it from source
 Find its output JAR file and copy or move it into the libs folder of
your project
 Likely check it into source control
 Add it to your library build path if your IDE or tool set doesn’t automate
this for you
 Write the code to call the method
All of these steps are prone to error, and many would need to be repeated if the project uses
JARs or code from other projects. Also different versions of the project can sometimes be in
different locations or incompatible with other projects that you have already included in your
app. If the project is maintained by another team in your company, you could run into issues
with the lack of a prebuilt JAR, which means you would need to combine the build file from
another team with your build file, which could dramatically increase the time and complexity
involved in building your app!
With Android Studio and Gradle, you can skip all of the chaos. You need only to declare the
project as a dependency in your build file, and then write the code to call the method. To
understand how dependencies are declared, recall the example Gradle build file introduced
earlier in this chapter that included the following block:
dependencies {
compile fileTree(dir: 'libs', include: ['
.jar'])
compile 'com.android.support:support-v4:20.+'
}
The first compile line instructs Gradle to grab all the JAR files under the libs folder as part of
the compile step. This is similar to how classic Ant build scripts worked with dependencies and
is included primarily for compatibility with older projects. The second compile line tells Gradle
to find version 20 or higher of the support-v4 library organized by the com.android.support
group from the repository and make it available in the project. Remember that a repository is an
abstract location containing a collection of prebuilt artifacts. Gradle will download dependency
artifacts from the Internet as needed and make them available in the classpath for the compiler
as well as package them with your resulting app.
Case Study: The Gradle Weather Project
In this section, you will examine a project, Gradle Weather, that will expose various types of
Gradle builds incrementally. This project displays the weather forecast. While some of the
implementation uses moderately advanced features, we will focus primarily on the build files
that stitch the app together and truncate many of the source listings. There are branches for
each step of the walk through. The Git repository for this project is marked with branches
for the individual steps in this study. You can refer to these steps throughout the chapter by
checking them out one by one or by looking at the changelists associated with them in the
Git log. Feel free to explore the source in depth.
We begin Gradle Weather with a minimalistic implementation that presents a fake weather
forecast. Open the Git log and find the branch named Step1. Right click this branch and
choose new branch from the context menu to create a new branch as shown in
Figure 13-4. Name this branch mylocal. You will make commits against this branch as
you follow along. Built from the FullScreen Activity template, Gradle Weather uses the
SystemUiHider logic that is generated as part of this template. It launches with a splash
screen that runs on a 5-second timer and simulates the loading of the weather forecast
by drawing data from a hard-coded plain old Java object called TemperatureData. This
TemperatureData object is given to an Adapter class to populate a list view filled with
forecasts. (The ListView component is discussed in depth in Chapter 8.) TemperatureData
uses a TemperatureItem class that describes the forecast for a given day. The build script
code for the project follows the same standard Gradle Android project structure defined
previously. First you’ll examine the files in the root folder responsible for the Gradle build.
Figure 13-1 and Listings 13-2 through 13-5 detail the code behind the core files controling
the build.
Listing 13-2. Settings.gradle
include ':app'
Listing 13-3. Root build.gradle
buildscript {
repositories {
jcenter()
}
dependencies {
classpath 'com.android.tools.build:gradle:0.12.+'
// NOTE: Do not place your application dependencies here; they belong
// in the individual module build.gradle files
}
}
allprojects {
repositories {
jcenter()
}
}
Listing 13-4. local.properties
sdk.dir=C\:\Android\android-studio\sdk
Listing 13-5. app\build.gradle
apply plugin: 'com.android.application'
android {
compileSdkVersion 20
buildToolsVersion '20.0.0'
defaultConfig {
applicationId "com.apress.gerber.gradleweather"
minSdkVersion 14
targetSdkVersion 20
versionCode 1
versionName "1.0"
}
buildTypes {
release {
minifyEnabled true
proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro'
}
}
}
dependencies {
compile fileTree(dir: 'libs', include: ['.jar'])
compile 'com.android.support:support-v4:20.+'
}
The settings.gradle file only defines the path to the app subproject. Next is build.gradle,
which includes a buildscript { ... } block. The buildscript block configures the current
build file. It includes the only subproject in the application, app. Next, the build.gradle file
defines all the build settings that apply globally to all subprojects. It defines a buildscript
block that includes the JCenter repository. This Internet-accessible Maven repository
contains many Android dependencies and open source projects. The file then sets a
dependency on Gradle 0.12 or greater. Finally, it sets all its child projects to use the same
JCenter repository.
The local.properties file includes only a setting for the location of the Android SDK. Last
we have app\build.gradle. This includes all the build configuration and logic for our app.
The first line engages the Android Gradle plug-in for use in the current build. It then applies
Android-specific configuration inside the android { ... } block. Inside this block, we set
the SDK version and build tools version. The SDK refers to the version of the Android SDK
APIs you wish to compile against, whereas the build tools version refers to the version
of the build tools used for things such as Dalvik Executable conversion (the dx step), ZIP
alignment, and so forth. The defaultConfig { ... } block defines the application ID (which
is used when you submit to the Google Play Store), the minimum SDK version that your app
is compatible with, the SDK that you are targeting, the app version, and version name.
The buildTypes { ... } block controls the output of your build. It allows you to override
different configurations that control the build output. Using this block, you can define
specific configurations for release to the Google Play Store.
The dependencies { ... } block defines all the dependencies for the app. The first
dependency line item is a local dependency that uses a special fileTree method call
which includes all the JAR files in the libs subfolder. The second line declares an external
dependency, which will be fetched from a remote repository. A special syntax is used to
declare external dependencies using the string given. This string is broken into sections
separated by colons. The first section is the group ID, which identifies the company or
organization that created the artifact. The second section is the artifact name. The last
section is the specific version of the artifact that your module depends on.
Gradle Weather defines a MainActivity class and three other classes responsible for
modeling and displaying the weather data. Listing 13-6 shows the code for this activity.
These classes include TemperatureAdapter, TemperatureData, and TemperatureItem. In the
initial version of the app, the weather is merely a pretend data set that is hard-coded in the
TemperatureData class.
Listing 13-6. MainActivity.java
public class MainActivity extends ListActivity implements Runnable{
private Handler handler;
private TemperatureAdapter temperatureAdapter;
private TemperatureData temperatureData;
private Dialog splashDialog;
String [] weekdays = { "Sunday","Monday","Tuesday",
"Wednesday","Thursday","Friday","Saturday" };
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
temperatureAdapter = new TemperatureAdapter(this);
setListAdapter(temperatureAdapter);
showSplashScreen();
handler = new Handler();
AsyncTask.execute(this);
}
private void showSplashScreen() {
splashDialog = new Dialog(this, R.style.splash_screen);
splashDialog.setContentView(R.layout.activity_splash);
splashDialog.setCancelable(false);
splashDialog.show();
}
private void onDataLoaded() {
((TextView) findViewById(R.id.currentDayOfWeek)).setText(
weekdays[Calendar.getInstance().get(Calendar.DAY_OF_WEEK)-1]);
((TextView) findViewById(R.id.currentTemperature)).setText(
temperatureData.getCurrentConditions().get(TemperatureData.CURRENT));
((TextView) findViewById(R.id.currentDewPoint)).setText(
temperatureData.getCurrentConditions().get(TemperatureData.DEW_POINT));
((TextView) findViewById(R.id.currentHigh)).setText(
temperatureData.getCurrentConditions().get(TemperatureData.HIGH));
((TextView) findViewById(R.id.currentLow)).setText(
temperatureData.getCurrentConditions().get(TemperatureData.LOW));
if (splashDialog!=null) {
splashDialog.dismiss();
splashDialog = null;
}
}
@Override
public boolean onCreateOptionsMenu(Menu menu) {
// Inflate the menu; this adds items to the action bar if it is present.
getMenuInflater().inflate(R.menu.main, menu);
return true;
}
@Override
public void run() {
temperatureData = new TemperatureData(this);
temperatureAdapter.setTemperatureData(temperatureData);
// Set Runnable to remove splash screen just in case
handler.postDelayed(new Runnable() {
@Override
public void run() {
onDataLoaded();
}
}, 5000);
}
}
MainActivity.java displays the splash screen temporarily while it pretends to load the
weather data. (This is done to plan for later revisions to the project, which will introduce
an actual load of data.) It then loads the data to the individual views onscreen by using the
TemperatureData class. The TemperatureData class contains a make-believe set of forecast
data, as illustrated in the partial code snippet that follows:
protected List<TemperatureItem> getTemperatureItems() {
List<TemperatureItem>items = new ArrayList<TemperatureItem>();
items.add(new TemperatureItem(drawable(R.drawable.early_sunny),
"Today", "Sunny",
"Sunny, with a high near 81. North northwest wind 3 to 8 mph."));
items.add(new TemperatureItem(drawable(R.drawable.night_clear),
"Tonight", "Clear",
"Clear, with a low around 59. North wind 5 to 10 mph becoming
light northeast in the evening."));
items.add(new TemperatureItem(drawable(R.drawable.sunny_icon),
"Wednesday", "Sunny",
"Sunny, with a high near 82. North wind 3 to 8 mph."));
//example truncated for brevity...
return items;
}
public Map<String, String> getCurrentConditions() {
Map<String, String> currentConditions = new HashMap<String, String>();
currentConditions.put(CURRENT,"63");
currentConditions.put(LOW,"59");
currentConditions.put(HIGH,"81");
currentConditions.put(DEW_POINT,"56");
return currentConditions;
}
The layout for the main activity includes a ListView that is populated by the
TemperatureAdapter class shown in Listing 13-7. This class accepts a TemperatureData
object, which it uses to pull a list of TemperatureItems. It creates a view for each
TemperatureItem by using the temperature_summary layout shown in Figure 13-2. Each
TemperatureItem, detailed in Listing 13-8, is merely a data holder object with getters for the
important data fields. These summaries are included in the activity’s main layout, which you
can see in Figure 13-3.
Listing 13-7. TemperatureAdapter.java
public class TemperatureAdapter extends BaseAdapter {
private final Context context;
List<TemperatureItem>items;
//This example is truncated for brevity...
@Override
public View getView(int position, View convertView, ViewGroup parent) {
View view = convertView != null ? convertView : createView(parent);
TemperatureItem temperatureItem = items.get(position);
((ImageView) view.findViewById(R.id.imageIcon)).setImageDrawable(temperatureItem.
getImageDrawable());
((TextView) view.findViewById(R.id.dayTextView)).setText(
temperatureItem.getDay());
((TextView) view.findViewById(R.id.briefForecast)).setText(
temperatureItem.getForecast());
((TextView) view.findViewById(R.id.description)).setText(
temperatureItem.getDescription());
return view;
}
private View createView(ViewGroup parent) {
LayoutInflater inflater = (LayoutInflater) context
.getSystemService(Context.LAYOUT_INFLATER_SERVICE);
return inflater.inflate(R.layout.temperature_summary, parent, false);
}
public void setTemperatureData(TemperatureData temperatureData) {
items = temperatureData.getTemperatureItems();
notifyDataSetChanged();
}
}
Listing 13-8. TemperatureItem.java
class TemperatureItem {
private final Drawable image;
private final String day;
private final String forecast;
private final String description;
public TemperatureItem(Drawable image, String day, String forecast,
String description) {
this.image = image;
this.day = day;
this.forecast = forecast;
this.description = description;
}
public String getDay() {
return day;
}
public String getForecast() {
return forecast;
}
public String getDescription() {
return description;
}
public Drawable getImageDrawable() {
return image;
}
}
Android Library Dependencies
While a trivial Android app may contain code developed by a single team, over time the
app will eventually mature to include features that are implemented by other developers or
teams. These can be made available externally in Android libraries. An Android library is a
special type of Android project in which you can develop a software component or series of
components that provide some behavior for your app—whether it is something as simple
as multiplying two numbers or as complicated as providing a social network portal that
lists friends and activities. Android libraries externalize features in a way that allows you to
plug and play without much hassle. Gradle’s robust repository system allows you to easily
locate and use code from other companies, open source libraries, or libraries from others
in your own organization. In this section, you will evolve our app by using an Android library
dependency that makes the network request for weather data. This change will not be
adequate for a milestone release, since it will not present the network data in a meaningful
way. However, it will suffice as a demonstration on how to use code from a library project in
an existing Android app. You will make further revisions that will present the data.
Adding Android libraries follows a flow similar to creating Android apps from scratch. Choose
File ➤ New Module to open the New Module Wizard illustrated in Figure 13-4. Then select
Android Library in the first dialog box. In the second dialog box, enter WeatherRequest
as the module name and choose the minimum SDK settings consistent with your app’s
requirements, as shown in Figure 13-5.
Choose Add No Activity from the next page of the wizard, shown in Figure 13-6. Click the
Finish button to add the library module to the project.
Figure 13-6. Choose the Add No Activity option
Step2 in the cloned repository has the new module which you can use as a reference. Your
new module will come complete with the following build.gradle file:
apply plugin: 'com.android.library'
android {
compileSdkVersion 20
buildToolsVersion "20.0.0"
defaultConfig {
minSdkVersion 14
targetSdkVersion 14
versionCode 1
versionName "1.0"
}
buildTypes {
release {
minifyEnabled false
proguardFiles getDefaultProguardFile('proguard-android.txt'),
'proguard-rules.pro'
}
}
}
dependencies {
compile fileTree(dir: 'libs', include: ['
.jar'])
}
The main difference between this build and the main build for the app is use of the Android
library plug-in. This plug-in generates a special Android archive file format, AAR, from the
module source. The AAR format, one of the new enhancements added to Android, allows
code to be shared between projects in the form of libraries. These libraries can be published
to an artifact repository by using the new Gradle build system. You can also declare a
dependency on any project that has a published AAR artifact and use it in your project.
A typical AAR file is merely a ZIP file with the .aar extension. It has the following structure:
 /AndroidManifest.xml (required)
 /classes.jar (required)
 /res/ (required)
 /R.txt (required)
 /assets/ (optional)
 /libs/.jar (optional)
 /jni/<abi>/
.so (optional)
 /proguard.txt (optional)
 /lint.jar (optional)
AndroidManifest.xml describes the contents of the archive, while classes.jar contains the
compiled Java code. Resources are found under the res directory. The R.txt file contains
the text output of the aapt tool.
An Android AAR file allows you to optionally bundle assets, native libraries, and/or JAR
dependencies, which was not possible in earlier versions of the SDK.
In the Step3 branch of the repository in our example, we’ve added a WeatherRequest module to
the project and changed the main app module to include this module as a dependency. This new
module contains a single class, NationalWeatherRequest, which makes a network connection
to the National Weather Service on behalf of the main app. This is a service that returns weather
information for whatever location provided. The location is given as latitude and longitude, and
the response is in XML format. Study the code in Listing 13-9 for a better understanding.
Listing 13-9. NationalWeatherRequest.java
public class NationalWeatherRequest {
public static final String NATIONAL_WEATHER_SERVICE =
"http://forecast.weather.gov/Ma ... 3B%3B
public NationalWeatherRequest(Location location) {
URL url;
try {
url = new URL(String.format(NATIONAL_WEATHER_SERVICE,
location.getLatitude(), location.getLongitude()));
} catch (MalformedURLException e) {
throw new IllegalArgumentException(
"Invalid URL for National Weather Service: " +
NATIONAL_WEATHER_SERVICE);
}
InputStream inputStream;
try {
inputStream = url.openStream();
} catch (IOException e) {
log("Exception opening Nat'l weather URL " + e);
e.printStackTrace();
return;
}
log("Dumping weather data...");
BufferedReader weatherReader = new BufferedReader(
new InputStreamReader(inputStream));
try {
for(String eachLine = weatherReader.readLine(); eachLine!=null;
eachLine = weatherReader.readLine()) {
log(eachLine);
}
} catch (IOException e) {
log("Exception reading data from Nat'l weather site " + e);
e.printStackTrace();
}
}
private int log(String eachLine) {
return Log.d(getClass().getName(), eachLine);
}
}
The new class retrieves the weather data and dumps it to the Android log as a basic
example of using an Android library. To include the new module in our project, the
build.gradle file in the app module must be edited. Find the dependencies block and
change it, as shown here:
dependencies {
compile fileTree(dir: 'libs', include: ['.jar'])
compile 'com.android.support:support-v4:20.+'
compile project(':WeatherRequest')
}
The compile project() line introduces a project dependency. The project location is
a relative path given as a parameter to the project() method, and this location uses
colons as a path delimiter. The preceding example is locating a project in a folder named
WeatherRequest within the main project folder GradleWeather. Gradle treats project
dependencies as additional work in the main build. Before building the app module, Gradle
will run against the WeatherRequest dependency project and then look inside this project to
find its output under the build/outputs folder. The WeatherRequest project outputs an AAR
file as its main output, which is consumed by the build in the app module. The AAR ZIP file
is exploded under the build/intermediates folder in the app module, and its contents are

included in its compiled output. You don’t usually have to understand the details of which
project file is included where. Just referencing another module in the dependencies block of
your main module is a high-level way to tell Gradle to include it as part of your app. Make
hese same changes to your local branch and commit them to get.
Java Library Dependencies
The next revision of our project, covered in Step4, includes a pure Java dependency. This
is a demonstration of the flexibility of both Android and the Gradle build system, as it opens
the door to including lots of preexisting code. Choose File ➤ New Module to open the New
Module Wizard illustrated in Figure 13-7. Then select Java Library in the first dialog box. In
the second dialog box, enter WeatherParse as the library name and click Finish, as shown in
Figure 13-8.
As you can see, adding a Java library module is similar to adding an Android module.
The main difference is apparent in the second dialog box, which has fewer options. This is
because a Java module will usually include only compiled Java class files and its output is
a jar file. Compare this to an Android library module that outputs aar files, which can include
layouts, native C/C++ code, assets, layout files, and more.
This begs the question, why would anyone want to use a Java module instead of an
Android library? The advantages are not obvious at first, but with a Java module, you have
the opportunity to reuse your Java code outside the Android platform. This could benefit
you in numerous scenarios. Consider a server-side web solution that defines a complex
image-processing algorithm for matching similar faces. Such an algorithm could be defined
separately as a Gradle project and consumed directly in your Android app to add the same
feature. Java modules can also be integrated with vanilla JUnit test cases. While Android
includes a derivative of the JUnit framework, these test cases must be deployed and
executed on a device or emulator, which quickly becomes a cumbersome process after a
few cycles. Using pure JUnit to test your Java code allows the tests to run directly within the
IDE at the click of a button. These tests usually run an order of magnitude faster than their
Android JUnit equivalents.
Our example project will evolve to include some involved XML parsing logic to make sense
of the XML response from the National Weather Service. Our WeatherParse uses the open
source kXML library to parse the response. This is the same library that is bundled with the
Android runtime. The challenge is to compile our parser outside the Android runtime where
kXML lives. While we can set a dependency for kXML, we also need to distribute and use
our Java library on the device without including a redundant copy of the kXML API. We will
address that problem later. For now, let’s look at the build.gradle file for the added Java
dependency:
apply plugin: 'java'
dependencies {
compile fileTree(dir: 'libs', include: ['
.jar'])
compile 'kxml:kxml:2.2.2'
testCompile 'junit:junit:4.11'
}
processTestResources << {
ant.copy(todir:sourceSets['test'].output.classesDir) {
fileset(dir:sourceSets['test'].output.resourcesDir)
}
}
There is not much here aside from the declaration of the Java plug-in. The Java plug-in
configures Gradle to produce a JAR file as its output while setting the build steps necessary
to compile, test, and package the class files. The dependencies { ... } block defines a
compile-time dependency for the kXML parser as well as JUnit. Gradle will generate a Java
JAR file including only the compiled classes from the project. The project also includes
two Java class files (one to invoke the parser and one to handle the parser events) as well
as a unit-test Java class. The test feeds a copy of a typical weather XML response from
the service into the parser and verifies that the parser can extract the weather information.
The copy of the response is saved under the resources folder. See the abbreviated unit-test
code snippet in Listing 13-10.
Listing 13-10. WeatherParseTest.java
public class WeatherParseTest extends TestCase {
private WeatherParser weather;
private String asString(InputStream inputStream) throws IOException {
BufferedReader reader = new BufferedReader(
new InputStreamReader(inputStream));
StringBuilder builder = new StringBuilder();
for(String eachLine = reader.readLine(); eachLine != null;
eachLine = reader.readLine()) {
builder.append(eachLine);
}
return builder.toString();
}
public void setUp() throws IOException, XmlPullParserException {
URL weatherXml = getClass().getResource("/weather.xml");
assertNotNull("Test requires weather.xml as a resource at the CP root.",
weatherXml);
String givenXml = asString(weatherXml.openStream());
this.weather = new WeatherParser();
weather.parse(new StringReader(givenXml.replaceAll("<br>", "<br/>")));
}
public void testCanSeeCurrentTemp() {
assertEquals(weather.getCurrent("apparent"), "63");
assertEquals(weather.getCurrent("minimum"), "59");
assertEquals(weather.getCurrent("maximum"), "81");
assertEquals(weather.getCurrent("dew point"), "56");
}
public void testCanSeeCurrentLocation() {
assertEquals("Should see the location in XML", weather.getLocation(),
"Sunnyvale, CA");
}
}
Any of the unit tests may be run by right-clicking the test method name and clicking the Run
option in the context menu. The feedback is immediate, as the test runs directly in the IDE
without the overhead of starting or selecting a device, uploading APK files, and launching.
When you run a unit test from a Java library in Android Studio, Gradle is invoked under
the covers and copies the resources from the resources folder into the output folder to be
located by the test. The setUp method in the test case leverages the copied weather.xml
file and reads it in as a string using a custom asString method. (In an added wrinkle, the
XML includes HTML <br> tags that need to be properly terminated by using Java’s
String replaceAll() method to prevent XML parse exceptions.) The setUp() method
continues to create a WeatherParser object while asking it to parse the XML. Two of the test
methods included in the preceding code demonstrate how the weather parser can then be
used to find the current temperature and current location from the response.
With a working weather-parsing Java library, you are free to change our Weather Request
Android library to make use of it. To do that, you need to do two things. First, you ensure
that the Java library is included in the top-level settings.gradle file under the GradleWeather
project root directory. Next, you set a dependency in the WeatherRequest gradle build to pull
in the WeatherParse project output. Again, the WeatherParse project is a Java library that
outputs a single JAR file, but there is a subtle detail to look out for. Our Java library includes
a dependency on kXML, which is considered transitive. We could declare the dependency in
the WeatherRequest module as follows:
dependencies {
compile fileTree(dir: 'libs', include: ['*.jar'])
compile project(':WeatherParse')
}
However, this will lead to the following compiler error:
Output:
UNEXPECTED TOP-LEVEL EXCEPTION:
com.android.dex.DexException:
Multiple dex files define Lorg/xmlpull/v1/XmlPullParser;
TOP-LEVEL EXCEPTION, a common cause of frustration for many developers, means that more
than one of the same file is being included in your APK. In this case, the exception comes
from Android already including the XmlPullParser defined in the kXML API as part of the
SDK. The Android SDK makes these and other APIs available during the compiling of any
Android application or library project. The reason we do not get errors when we build the
WeatherParse module is that it is defined as a Java library. Java library projects are compiled
with the Java SDK, and no Android APIs are included as part of the compile. To work around
the error, we need to exclude this transitive dependency from the list of dependencies
considered in the WeatherRequest module. We add the code shown in Figure 13-9 to the
Gradle build file for the WeatherRequest module to get rid of the error.
The project is now updated to parse the XML weather response and download the images
by using links from the XML. The NationalWeatherRequest object caches the URL object
as a member variable and adds a getWeatherXml method to use the URL, as shown in
Listing 13-11.
Listing 13-11. NationalWeatherRequest.java
public class NationalWeatherRequest {
public static final String NATIONAL_WEATHER_SERVICE =
"http://forecast.weather.gov/Ma ... 3B%3B
private final URL url;
//...
public String getWeatherXml() {
InputStream inputStream = getInputStream(url);
return readWeatherXml(inputStream);
}
private String readWeatherXml(InputStream inputStream) {
StringBuilder builder = new StringBuilder();
if (inputStream!=null) {
BufferedReader weatherReader = new BufferedReader(
new InputStreamReader(inputStream));
try {
for(String eachLine = weatherReader.readLine(); eachLine!=null;
eachLine = weatherReader.readLine()) {
builder.append(eachLine);
}
} catch (IOException e) {
log("Exception reading data from Nat'l weather site " + e);
e.printStackTrace();
}
}
String weatherXml = builder.toString();
log("Weather data " + weatherXml);
return weatherXml;
}
private InputStream getInputStream(URL url) {
InputStream inputStream = null;
try {
inputStream = url.openStream();
} catch (IOException e) {
log("Exception opening Nat'l weather URL " + e);
e.printStackTrace();
}
return inputStream;
}
Listing 13-12 details how the NationalWeatherRequestData object is updated to use the new
getWeatherXML method and give its results to the new WeatherParse Java component.
Listing 13-12. NationalWeatherRequestData.java
public NationalWeatherRequestData(Context context) {
this.context = context;
Location location = getLocation(context);
weatherParser = new WeatherParser();
String weatherXml = new NationalWeatherRequest(location).getWeatherXml();
//National weather service returns XML data with embedded HTML <br> tags
//These will choke the XML parser as they don't have closing syntax.
String validXml = asValidXml(weatherXml);
try {
weatherParser.parse(new StringReader(validXml));
} catch (XmlPullParserException e) {
e.printStackTrace();
} catch (IOException e) {
e.printStackTrace();
}
}
public String asValidXml(String weatherXml) {
return weatherXml.replaceAll("<br>","<br/>");
}
@Override
public List<TemperatureItem> getTemperatureItems() {
ArrayList<TemperatureItem> temperatureItems =
new ArrayList<TemperatureItem>();
List<Map<String, String>> forecast = weatherParser.getLastForecast();
if (forecast!=null) {
for(Map<String,String> eachEntry : forecast) {
temperatureItems.add(new TemperatureItem(
context.getResources().getDrawable(R.drawable.progress),
eachEntry.get("iconLink"),
eachEntry.get("day"),
eachEntry.get("shortDescription"),
eachEntry.get("description")
));
}
}
return temperatureItems;
}
The TemperatureAdapter class undergoes a major overhaul and becomes rather
complicated. It uses image links from WeatherRequest to download the images in the
background. See the definition in Listing 13-13.
Listing 13-13. TemperatureAdapter.java
public class TemperatureAdapter extends BaseAdapter {
private final Context context;
List<TemperatureItem>items;
public TemperatureAdapter(Context context) {
this.context = context;
this.items = new ArrayList<TemperatureItem>();
}
@Override
public int getCount() {
return items.size();
}
@Override
public Object getItem(int position) {
return items.get(position);
}
@Override
public long getItemId(int position) {
return position;
}
@Override
public View getView(int position, View convertView, ViewGroup parent) {
View view = convertView != null ? convertView : createView(parent);
TemperatureItem temperatureItem = items.get(position);
ImageView imageView = (ImageView) view.findViewById(R.id.imageIcon);
imageView.setImageDrawable(temperatureItem.getImageDrawable());
if(temperatureItem.getIconLink()!=null){
Animation animation = AnimationUtils.loadAnimation(
context, R.anim.progress_animation);
animation.setInterpolator(new LinearInterpolator());
imageView.startAnimation(animation);
((ViewHolder) view.getTag()).setIconLink(temperatureItem.getIconLink());
}
((TextView) view.findViewById(R.id.dayTextView)).setText(
temperatureItem.getDay());
((TextView) view.findViewById(R.id.briefForecast)).setText(
temperatureItem.getForecast());
((TextView) view.findViewById(R.id.description)).setText(
temperatureItem.getDescription());
return view;
}
class ViewHolder {
private final View view;
private String iconLink;
private AsyncTask<String, Integer, Bitmap> asyncTask;
public ViewHolder(View view) {
this.view = view;
}
public void setIconLink(String iconLink) {
if(this.iconLink != null && this.iconLink.equals(iconLink)) return;
else this.iconLink = iconLink;
if(asyncTask != null) {
asyncTask.cancel(true);
}
asyncTask = new AsyncTask<String,Integer,Bitmap>() {
@Override
protected Bitmap doInBackground(String... url) {
InputStream imageStream;
try {
imageStream = new URL(url[0]).openStream();
} catch (IOException e) {
e.printStackTrace();
return null;
}
return BitmapFactory.decodeStream(imageStream);
}
@Override
protected void onPostExecute(final Bitmap bitmap) {
if (bitmap == null) {
return;
}
new Handler(context.getMainLooper()).post(new Runnable() {
@Override
public void run() {
ImageView imageView = (ImageView) view
.findViewById(R.id.imageIcon);
imageView.clearAnimation();
imageView.setImageBitmap(bitmap);
}
});
asyncTask = null;
}
};
asyncTask.execute(iconLink);
}
}
private View createView(ViewGroup parent) {
LayoutInflater inflater = (LayoutInflater) context
.getSystemService(Context.LAYOUT_INFLATER_SERVICE);
View inflatedView = inflater.inflate(R.layout.temperature_summary,
parent, false);
inflatedView.setTag(new ViewHolder(inflatedView));
return inflatedView;
}
public void setTemperatureData(TemperatureData temperatureData) {
items = temperatureData.getTemperatureItems();
notifyDataSetChanged();
}
}
The ImageViews are each associated with a ViewHolder and initialized with a spinner icon and
a rotating animation that simulates an indefinite progress spinner. The majority of the work
is in the ViewHolder’s setIconLink method. This method triggers a download of the weather
icon in the background. When the download completes, the ImageView is updated with the
downloaded image. And the spinning animation is cancelled. Again, a lot of complexity is in
this class file just to handle the loading of the images. Wouldn’t it be better to simplify?
Third-Party Libraries
Sometimes you don’t have the availability or expertise to implement a tricky piece of
logic. Third-party libraries are often used to tackle these and other tough problems in
Android development. As mentioned earlier, calling code that has been developed by
another developer or team somewhere else on the globe is nearly identical to calling code
from another module in your project. We continue with the Step5 branch where we will
demonstrate how to use an open source component to the Gradle Weather project. Our app
downloads a series of images, each representing the conditions on a certain day. We start
with a minimalistic addition to the Gradle build under the app module shown in Figure 13-10.
That’s it! Immediately you will see a yellow prompt indicating that the Gradle file has
changed and a hyperlink text button that enables the project sync to begin. Click the link
illustrated in Figure 13-11 to allow Android Studio to sync the underlying IntelliJ project files
with the dependencies. Gradle will download them in the background.
After the project sync and download completes, the code can be changed to invoke the API.
Revisiting TemperatureAdapter from earlier, we can appreciate how easy it is to download
the weather icons in the background:
private final ImageLoader imageLoader;
List<TemperatureItem>items;
public TemperatureAdapter(Context context, ImageLoader imageLoader) {
this.context = context;
this.imageLoader = imageLoader;
this.items = new ArrayList<TemperatureItem>();
}
public void setIconLink(String iconLink) {
final ImageView imageView = (ImageView) view.findViewById(
R.id.imageIcon);
imageLoader.displayImage(iconLink, imageView,
new SimpleImageLoadingListener(){
@Override
public void onLoadingComplete(String imageUri, View view,
Bitmap loadedImage) {
imageView.clearAnimation();
super.onLoadingComplete(imageUri, view, loadedImage);
}
});
}
The constructor is updated to take an ImageLoader object and store it in an instance variable.
The setIconLink method merely gives the iconLink to the ImageLoader, which does all the
heavy lifting.
Opening Older Projects
Android Studio now includes robust import tools for migrating older projects into the newer
Gradle build system. This support is near transparent and happens automatically as you
open older projects. In Android Studio’s earlier beta days, many people got annoyed when
opening these older projects. Part of the frustration has been with the rapid update cycle
of Gradle, which can result in older builds sometimes failing to work. This happens when
you use a newer version of Gradle with an older build. Using the Gradle wrapper when
you import older projects is supposed to alleviate that pain somewhat, but at times this is
not feasible or effective. When you open an older project in an updated version of Android
Studio—for example, moving from version 0.8x to 1.x—you may have seen the unsupported
Android Gradle plug-in error shown in Figure 13-12.
You can click the Fix Plug-in Version and Re-import Project link, but you will be greeted with
the error in Figure 13-13, which is complaining about a missing DSL method, runProGuard().
Armed with your new knowledge of Gradle, you can surmise what a DSL method is, and you
now know to open your app’s build.gradle file to find this errant method call. Version 1.x
deprecated this call in favor of minifyEnabled.
Figure 13-13. DSL method not found error
Summary
You have explored the basics of the Gradle build system. We demonstrated a multimodule
Android project with different types of dependencies. You also saw how to incorporate regular
Java code with JUnit tests in an Android project. Finally, you learned how to open older
projects by using the import capabilities built into Android Studio. You walked through how to
fix some common problems with these older project imports. Gradle also includes a robust
dependency management system that allows you to reuse code between projects with little
effort. This chapter only scratches the surface of what is now possible in Android Studio with
Gradle. Feel free to explore on your own and enhance the example project further.
2016-07-04 13:30 添加评论 分享
已邀请:
0

ask

赞同来自:

楼主辛苦啦!
0

keyoo - 珍惜时间,创造价值

赞同来自:

分享知识,一起进步!

要回复问题请先登录注册

退出全屏模式 全屏模式 回复