AS不仅允许为app或library创建module,也会创建Android Wear、Android TV、Google App Engine等。所有的这些模块都可应用到一个单独项目中。例如,你可能想要创建一个集成了Android Wear而且后台使用Google Cloud Endpoints的app。这种情况下,要创建一个有三个module的项目:一个用于app,一个用于后台,还有一个用于Android Wear集成。了解多模块项目如何被组织和构建可以极大的缩减开发周期。
本章中,我们将会涉及多模块构建的理论以及展示一些在实际项目中非常有用的例子:
- 多模块构建的剖析
- 给项目添加模块
- 技巧和最佳实践
多模块build剖析
一般来说,一个多模块项目靠根目录的子目录中包含的所有的模块工作。为了告诉gradle项目是如何被构成以及哪些目录下包含模块,需要在项目的根目录下提供一个settings.gradle文件。每个模块也会提供自己的build.gradle文件。我们已经在第二章学习了settings.gradle和build.gradle是如何工作的,所以这里我们只会聚焦于如何在多模块项目中使用它们。
一个多模块项目看起来如下:
project
├─── setting.gradle
├─── build.gradle
├─── app
│ └─── build.gradle
└─── library
└─── build.gradle
这是建立多模块项目最简单也是最直接的方式。settings.gradle文件声明了项目中所有的module,如是下观:
include ':app', ':library'
确保了app和library模块都被在build配置中添加。你所需要做的只是添加模块目录的名称。
为了添加library模块作为app模块的依赖,需要把如下代码添加到app模块的build.gradle文件中:
dependencies {
compile project(':library')
}
为了给模块添加依赖,需要使用project()方法,并使用模块路径作为参数。
如果想要使用子目录组织模块,gradle可被配置满足你的需求。例如,你可能有如下目录结构:
project
├─── setting.gradle
├─── build.gradle
├─── app
│ └─── build.gradle
└─── libraries
├─── library1
│ └─── build.gradle
└─── library2
└─── build.gradle
app模块依旧如之前一般位于根目录下,但是项目现在有两个不同的库。这些库模块并没有位于根目录下,而是在一个特殊的库目录下。对于这种目录结构,可在settings.gradle中如是声明app和library模块:
include ':app', ':libraries:library1', ':libraries:library2'
可以看到到在子目录中声明模块是多么容易啊!所有路径都是关联到根目录(settings.gradle文件所在即是)的。路径中冒号代替斜杠。
当在子目录中添加一个模块作为其他模块的依赖,应该总是从根目录中引用。也就是说在之前的例子中如果app模块依赖library1,app模块的build.gradle文件应该如是下观:
dependencies {
compile project(':libraries:library1')
}
如果在子目录中声明依赖,所有的路径仍然要被关联到根目录。原因在于gradle从项目根路径下开始构造项目的依赖模型。
build生命周期再观
了解build过程模型是如何被构造的使得很容易理解多模块项目是如何组成的。我们已经在第一章讨论过了build的生命周期,所以你已经了解了基础,但是有些细节对于多模块构建尤其重要。
在第一阶段—初始化阶段,gradle寻找settings.gradle文件。如果文件不存在,gradle设想你只有一个单模块要构建。但如果有多个模块,settings.gradle就是定义包含单个模块的子目录的地方。如果这些模块包含自己的build.gradle文件,gradle将会处理并把它们合并到build过程模型中。这也解释了为什么应该总是在模块中使用与根目录关联的路径声明依赖。gradle将会一直尝试从根目录中解决依赖。
一旦你理解了构建过程模型是如何被组合的,有好几种配置多模块项目构建的策略都会很清晰。你应该在根目录下的build.gradle文件中为所有的模块配置设置。这使得获取一个项目中整个的构建配置概览很容易,但也有可能会变得很凌乱,尤其当模块需要不同插件,而且每个插件都有自己的DSL时。另一种方式就是每个模块都有自己的build.gradle文件。这种策略保证了模块之间不会紧密耦合。也使得更容易跟踪构建改变,因为不需要计算改变要应用到哪些模块中。
最后一种策略就是混合算法。在项目的根目录下有个build.gradle文件,主要定义一些针对所有模块的公共属性,和一个每个模块用来配置仅应用到特定模块的设置的build文件。AS采用的就是这种方式。在根目录下创建了一个build.gradle文件和一个针对模块的build.gradle文件。(此处我觉得应该是根目录下有个build文件,每个模块下都有自己的build文件)
模块task
只要在项目中有多个模块,运行task之前就要再三考虑。当你在命令行接口中从项目的根目录运行一个task,gradle就会判断哪些模块具有这个名字的task,然后为所有模块运行该task。例如,如果有一个移动app模块和一个Android Wear模块,运行gradlew assembleDebug将会分别构建移动app模块和Android Wear模块的debug版本。当你把目录改成为其中的一个模块的路径时,然而,gradle将只会为那个特殊的模块运行task,甚至当你在项目的根目录使用gradle wrapper时。
切换目录运行特定模块的task实在令人生厌。幸运的是,还有其他的方式。可以使用具有模块名称的task名称运行针对特地模块的task。例如,为了只构建Android Wear模块,可以使用如下命令:
gradlew:wear:assembleDebug
给项目增加模块
添加一个新的模块就像在AS中经历一个向导程序一样简单。向导程序也建立了基础的build文件。一些情况下,添加一个新模块甚至可能会导致AS编辑你的app模块的build文件。例如,当添加一个Android Wear模块时,IDE设想你想要为你的Android app使用它,并且会在build文件中添加一行代码引用Android Wear模块。
AS中New Module对话框如是下观:
在下面的段落中,我们将会展示可以被添加到Android项目中的不同的模块,解释它们的自定义属性和说明它们是如何改变构建进程的。
添加Java库
添加一个新的Java library模块时,AS生成的build.gradle文件如是下观:
apply plugin: 'java'
dependencies {
compile fileTree(dir: 'libs', include: ['*.jar'])
}
Java library模块使用Java插件代替我们习惯看到的Android插件。这意味着大量的Android特有的属性和task都无法使用,但是对于Java library这些也都用不到。
build文件也会建立基础的依赖管理,所以可以添加JAR文件到libs目录下而无需任何特殊的配置。可以使用在第三章学习到的添加更多的依赖。依赖配置并不依赖于Android插件。
为了添加一个名为javalib的Java library模块作为你的app模块的依赖,简单的在app模块的build配置文件中添加如下代码:
dependencies {
compile project(':javalib')
}
者告诉gradle在build中导入名为javalib的模块。如果在app模块中添加这个依赖,javalib模块将一直先于app模块被构建。
添加Android库
我们在第三章简单地提到过Android library,在第三章我们称之为library项目。两个名字在本文档和其他各种材料中都被使用。在本节,我们将会使用Android library,因为它是在AS的New Module对话框中使用的名称。
对于一个Android library,默认的build.gradle文件第一行代码如是下观:
apply plugin: 'com.android.library'
一个Android library不仅包含了library的java代码,也包含了所有的Android资源,例如manifest文件、strings和layouts。在app中应用一个Android library后,可以在app中使用library中所有的类和资源。
集成Android可穿戴
如果想要添加app的深度集成到Android Wear中,需要添加一个额外的Android Wear模块。有意思的是Android Wear模块也使用Android应用插件。这意味着所有的属性和task都是可用的。
与常规的Android app模块中build.gradle文件的唯一不同之处就在于依赖配置:
dependencies {
compile fileTree(dir: 'libs', include: ['*.jar'])
compile 'com.google.android.support:wearable:1.1.0'
compile 'com.google.android.gms:play-services-wearable:6.5.87'
}
每个Android Wear app依赖一些Google提供的Wear特有的库。为了在Android app中使用Android Wear app,需要把它和app一起打包。可通过在app中添加依赖实现:
dependencies {
wearApp project(':wear')
}
wearApp配置确保了Wear模块的APK被添加到了Android app的最终APK中而且已经完成了必备的设置。
使用Google App Engine
Google App Engine是个云平台。如果没有建立自己的服务器的话,可以在上面运行web程序。可免费使用一定级别的,因此是个不错的测试环境。Google App Engine也提供一个叫做Cloud Endpoint的服务,它主要用来创建RESTful服务。使用Google App Engine的Cloud EndPoint使得为你的app建立后台很容易。App Engine Gradle插件通过为app生成一个客户端库使之更加容易,意味着你不需要写任何与APII相关的代码。这使得对于app后台来说,Google App Engine是个不错的选择。所以在接下来的章节中我们将会了解App Engine Gradle插件是如何工作的以及我们可以如何充分利用Cloud Endpoints。
为了创建一个带有Cloud Endpoints的新的Google App Engine模块,从File|new Module中打开New Module对话框…选择Google Cloud Module。当建立module时,可以改变Cloud Endpoints的添加类型。然后,然后选择将会使用这个后台的客户端模块。
关于Google App Engine和Cloud Endpoints的完整解释超出了本书的范围;我们将只会了解App Engine模块和客户端app模块的Gradle集成。
分析build文件
这个模块的build.gradle文件比较大,所有我只会了解最值得关注的地方,从新的build脚本依赖开始:
buildscript {
dependencies {
classpath 'com.google.appengine:gradle-appengineplugin:1.9.18'
}
}
App Engine插件需在build脚本的classpath中定义。在之前添加Android插件时我们已看到过。当各得其所时,可以把App Engine与其他两种插件一起使用
apply plugin: 'java'
apply plugin: 'war'
apply plugin: 'appengine'
Java插件主要用于为Cloud Endpoints生成JAR文件。WAR插件是运行和分发整个后端必备的。WAR插件生成一个WAR文件,这就是Java Web程序如何是分布式的。最后,App Engine插件添加了一些构建、运行和部署整个后端的task。
下一个重要的块定义了App Engine模块的依赖:
dependencies {
appengineSdk 'com.google.appengine:appengine-java-sdk:1.9.18'
compile 'com.google.appengine:appengine-endpoints:1.9.18'
compile 'com.google.appengine:appengine-endpoints-deps:1.9.18'
compile 'javax.servlet:servlet-api:2.5'
}
第一个依赖使用appengineSdk指明应该在这个模块中使用哪个sdk。endpoints依赖对于Cloud Endpoints工作是必须的。如果选择在模块中使用Cloud EndPoints,这些就是全部需要添加的。servlet依赖是任何Google App Engine模块都要求具有的。
在appengine块中配置任何App Engine特有的设置:
appengine {
downloadSdk = true
appcfg {
oauth2 = true
}
endpoints {
getClientLibsOnBuild = true
getDiscoveryDocsOnBuild = true
}
}
设置downloadSdk属性值为true使得运行本地开发服务更容易,因为如果没有的话会自动下载SDK。如果已经在设备上设置Google App Engine SDK,可以设置downloadSdk为false。
appcfg块是用来配置App Engine SDK的。在一个典型的Google App Engine安装中,可以使用appcfg命令行工具手动配置一些设置。使用appcfg块,代替命令行工具,可使配置更加轻便。任何曾经构建过该模块的人将会有相同的配置而无需执行任何额外的命令。
endpoints块包含一些Cloud Endpoints特定的设置。
关于Google App Engine和Cloud Endpoints配置的详细说明超出了本书范围,如果想了解更多,可以查看这篇文档:https://cloud.google.com/appengine/docs
在app中使用后台
当你创建App Engine模块时,AS自动给Android app模块的build文件添加一个依赖,如是下观:
dependencies {
compile project(path: ':backend', configuration: 'androidendpoints')
}
语法类似于之前的(当音乐Java 和Android库时)。使用project定义依赖,但是有两个参数。path参数是默认参数。我们之前使用过,但是没有指定名称。一个Google App Engine可有不同类型的输出。可以使用configration指定你想要的输出。我们需要App Engine模块生成Cloud Eendpoints,所以使用android-endpoints配置。在内部,该配置运行_appengineEndpointsAndroidArtifact task。这个task生成了一个包含可以在app模块中使用的类的JAR文件。这个JAR文件不仅仅包含了在Cloud Endpoints使用的模式,也包含了API方法。这种集成就是使得多模块项目很好工作的原因,因为它加速了缩减时间。App Engine模块中的gradle task也使之更易运行和部署后台。
自定义task
App Engine插件添加了很多task,但是你将使用最多的就是appengineRun和appengineUpdate。
appengineRun task用来启动一个本地开发服务器,以便可以在上传到Google App Engine之前在本地测试整个后台。第一次运行这个task,因为gradle需要下载App Engine SDK,build可能会花费一些时间。我们之前使用downloadSdk设置该行为。为了停止服务器,可以使用appengineStop。
你一旦准备部署后台到Google App Engine并在产品中使用,可以使用appengineUpdate。这个task处理所有的部署细节。如果在appengine配置块中设置了oauth2 = true,这个task将会打开一个浏览器窗口。所以你可以登陆google账号并获取认证令牌。如果不想每次需要部署的时候都这样,可以使用Google账号登陆到AS中并使用IDE部署后台。AS运行相同的gradle task,但是将会为你负责认证。
技巧和最佳实践
有几种方式可以使得处理多模块项目更加容易,处理好几个模块时也需要注意一些事情。意识到这些可以节约你的时间和减少挫折。
从AS中运行模块task
正如我们在第二章看到的,可以直接从从AS中运行gradle task。当你有多个模块时,AS会识别到它们并将展示出来:
gradle工具窗口使得运行特定模块的task更加容易。但是没有同时为所有模块运行同一task的选项,所以如果想这样做的话,命令行接口仍然是更快的。
加速多模块构建
当构建一个多模块项目时,gradle依次处理所有模块。随着电脑的核越来越多,我们可以以并行的方式更快的构建进程。这个特性已存在于gradle中,但默认不可用。
如果想要把并行构建应用到项目中,需要在项目的根目录下的gradle.properties文件中配置parallel属性。
org.gradle.parallel=true
gradle基于可用的内核,尝试选择正确的线程数量。为了阻止可能以并行的方式在相同模块中执行两个task导致的问题,每个线程都有一个完整的模块。
并行构建是个隐藏的功能。这意味着它正在被继续开发而且实现可能会随时改变。这个特性刚成为gradle的一部分,但已被广泛使用。因此,可以设想它的实现不会彻底的消失或改变。
因人而异,通过简单的启动并行构建,可能会节约大量的构建时间。然而,有个问题。为了并行构建高效工作,需要确定模块之间是解耦的。
模块耦合
正如我们在第二章看到的,在build.gradle文件中使用allprojects属性可以为项目中的所有模块定义属性。当你有个多模块项目时,可以在任意模块中使用allprojects应用属性到项目中的所有模块里。gradle甚至使得在一个模块中引用另一模块中的属性成为可能。这些强大的特性使得多模块构建的维护容易的多。但负面作用是模块之间变得耦合。
两个模块只要它们访问对方的task或属性,就会被视为耦合。这会有好几种后果。例如,放弃了可移植性。如果你曾经决定从项目中提取library,在复制所有项目级设置前,是不可能构建library的。模块耦合也会影响到并行构建。在任意模块中使用allprojects将使得并行构建无效。当你给所有模块添加项目级属性前要意识到这些。
可以通过不直接访问其他模块的task或属性避免耦合。如果需要的话,可以使用根模块作为中介以便模块仅仅耦合到根模块而非其他模块。
总结
我们通过了解多模块构建的结构开启本章学习之旅。然后,了解了如何在单项目中建立多个模块。我们也看到添加新的模块影响到了构建task是如何运行的。
我们接着看到了一些新模块的实际例子,以及它们都是怎么被集成到一个项目中的。最后,我们提到一些在项目中更好的处理多模块的要诀和技巧。
在下一章节,我们将会建立不同种类的测试,并会看到如何使用gradle使得运行这些测试更容易。我们将会不仅考虑直接在Java虚拟机上,而且还会在实体机和模拟器上运行单元测试