Groovy(二):Gradle for Android

来源:互联网 发布:画像素的软件 编辑:程序博客网 时间:2024/05/17 22:14

上一篇(Groovy(一): build.gradle为何物? )主要是我们熟悉的build.gradle,项目开发基本上够用了,如果还想深入了解下来龙去脉,可以继续学习(Gradle for Android 下载本书)

这里写图片描述

如果你也像我一样从eclipse换到as来进行项目开发的,如果对其中的gradle比较感兴趣,那么就跟随我一起来学习下:

一.从 Gradle 和 AS 开始

如果你想创建一个Android project基于gradle,那么你必须写一个构建脚本,这个文件通常称之为build.grade,你可能已经觉察到了,当我们查看这一脚本,gradle会为我们提供很多默认的配置以及通常的默认值,而这极大的简化了我们的工作,例如ant和maven,使用他们的时候,我们需要编写大量的配置文件,而这很恶心。而gradle得默认配置,如果你需要使用自己的配置,完全可以简单的去重写他们就好。

Gradle脚本不是像传统的xml文件那样,而是一种基于Groovy的动态DSL,而Groovy语言是一种基于jvm的动态语言。

你完全不用担心,你在使用gradle的时候,还需要去学习Groovy语言,该语言很容易阅读,并且如果你已经学习过java的话,学习Groovy将不会是难事,如果你想开始创建自己的tasks和插件,那么你最好对Groovy有一个较深的理解,然而由于其基于jvm,所以你完全可能通过纯正的java代码或者其他任何基于jvm的语言去开发你自己的插件,关于插件开发,我们后续将会有相关介绍。

Project和tasks
在grade中的两大重要的概念,分别是project和tasks。每一次构建都是有至少一个project来完成,所以Android studio中的project和Gradle中的project不是一个概念。每个project有至少一个tasks。每一个build.grade文件代表着一个project。tasks在build.gradle中定义。当初始化构建进程,gradle会基于build文件,集合所有的project和tasks,一个tasks包含了一系列动作,然后它们将会按照顺序执行,一个动作就是一段被执行的代码,很像Java中的方法。

构建的生命周期
一旦一个tasks被执行,那么它不会再次执行了,不包含依赖的Tasks总是优先执行,一次构建将会经历下列三个阶段:

1.初始化阶段:project实例在这儿创建,如果有多个模块,即有多个build.gradle文件,多个project将会被创建。
2. 配置阶段:在该阶段,build.gradle脚本将会执行,为每个project创建和配置所有的tasks。
3. 执行阶段:这一阶段,gradle会决定哪一个tasks会被执行,哪一个tasks会被执行完全依赖开始构建时传入的参数和当前所在的文件夹位置有关。

build.gradle的配置文件
基于grade构建的项目通常至少有一个build.gradle,那么我们来看看Android的build.gradle:

// Top-level build file where you can add configuration options common to all sub-projects/modules.buildscript {    repositories {        jcenter()    }    dependencies {        classpath 'com.android.tools.build:gradle:2.3.3'        classpath 'com.neenbedankt.gradle.plugins:android-apt:1.8'        // NOTE: Do not place your application dependencies here; they belong        // in the individual module build.gradle files    }}

这个就是实际构建开始的地方,在仓库地址中,我们使用了JCenter,JCenter类似maven库,不需要任何额外的配置,grade还支持其他几个仓库,不论是远程还是本地仓库。

构建脚本也定义了一个Android构建工具,这个就是Android plugin的来源之处。Android plugin提供了所有需要去构建和测试的应用。每个Android应用都需要这么一个插件:

apply plugin: 'com.android.application'

插件用于扩展gradle脚本的能力,在一个项目中使用插件,这样该项目的构建脚本就可以定义该插件定义好的属性和使用它的tasks。

注意:当你在开发一个依赖库,那么你应该使用’com.android.library’,并且你不能同时使用他们2个,这将导致构建失败,一个模块要么使用Android application或者Android library插件,而不是二者。

当使用Android 插件的时候,Android标签将可以被使用,如下所示:

android {    compileSdkVersion 25    buildToolsVersion "25.0.3"}

grade项目通常在根文件夹中包含一个build.gradle,使用的代码在app这个文件夹中,这个文件夹也可以使用其他名字,而不必要定义为app,例如当你利用Android studio创建一个project针对一个手机应用和一个Android wear应用的时候,模块将被默认叫做application和wearable。

gradle使用了一个叫做source set的概念,官方解释:一个source set就是一系列资源文件,其将会被编译和执行。对于Android项目,main就是一个source set,其包含了所有的资源代码。当你开始编写测试用例的时候,你一般会把代码放在一个单独的source set,叫做androidTest,这个文件夹只包含测试。

开始使用Gradle Wrapper
grade只是一个构建工具,而新版本总是在更迭,所以使用Gradle Wrapper将会是一个好的选择去避免由于gradle版本更新导致的问题。Gradle Wrapper提供了一个windows的batch文件和其他系统的shell文件,当你使用这些脚本的时候,当前gradle版本将会被下载,并且会被自动用在项目的构建,所以每个开发者在构建自己app的时候只需要使用Wrapper。所以开发者不需要为你的电脑安装任何gradle版本,在mac上你只需要运行gradlew,而在windows上你只需要运行gradlew.bat。

你也可以利用命令行./gradlew -v来查看当前gradle版本。下列是wrapper的文件夹:

myapp/
 ├── gradlew
 ├── gradlew.bat
 └── gradle/wrapper/
    ├── gradle-wrapper.jar
   └── gradle-wrapper.properties

可以看到一个bat文件针对windows系统,一个shell脚本针对mac系统,一个jar文件,一个配置文件。配置文件包含以下信息:

Wed Jun 14 16:22:36 CST 2017
distributionBase=GRADLE_USER_HOME
distributionPath=wrapper/dists
zipStoreBase=GRADLE_USER_HOME
zipStorePath=wrapper/dists
distributionUrl=https://services.gradle.org/distributions/gradle-3.3-all.zip

你可以改变该url来改变你的gradle版本。

使用基本的构建命令
使用你的命令行,导航到你的项目,然后输入:

$ ./gradlew tasks

All tasks runnable from root project

Android tasks

androidDependencies - Displays the Android dependencies of the project.
signingReport - Displays the signing info for each variant.
sourceSets - Prints out all the source sets defined in this project.

Build tasks

assemble - Assembles all variants of all applications and secondary packages.
assembleAndroidTest - Assembles all the Test applications.
assembleDebug - Assembles all Debug builds.

这一命令将会列出所以可运行的tasks,你也可以添加–all参数,来查看所有的task。当你在开发的时候,构建项目,你需要运行assemble task通过debug配置:

$ gradlew assembleDebug

该任务将会创建一个debug版本的app,同时Android插件会将其保存在MyApp/app/build/ outputs/apk目录下。

除了assemble,还有三个基本的命令:

  • check 运行所以的checks,这意味着运行所有的tests在已连的设备或模拟器上。
  • build 是check和assemble的集合体。
  • clean 清楚项目的output文件。

在grade文件中配置,如果你有任何依赖的jar包,你需要告诉gradle它在哪儿,假设jar包会在一个叫做libs的文件夹内,那么你应该这么配置:

dependencies {
compile fileTree(dir: ‘libs’, include: [‘*.jar’])
}

该行意为:将libs文件夹中所有的jar文件视为依赖包。

当然,对于上述的命令,在android studio中是可以直接查看的:
这里写图片描述

二.理解Gradle脚本

setting.gradle解析
当你的app只有一个模块的时候,你的setting.gradle将会是这样子的:

include ':app'

setting.gradle文件将会在初始化时期执行,关于初始化时期,可以查看上一篇博客,并且定义了哪一个模块将会被构建。举个例子,上述setting.gradle包含了app模块,setting.gradle是针对多模块操作的,所以单独的模块工程完全可以删除掉该文件。在这之后,Gradle会为我们创建一个Setting对象,并为其包含必要的方法,你不必知道Settings类的详细细节,但是你最好能够知道这个概念。

根目录的build.gradle
该gradle文件是定义在这个工程下的所有模块的公共属性,它默认包含三个方法:

// Top-level build file where you can add configuration options common to all sub-projects/modules.buildscript {    repositories {        jcenter()        mavenCentral()    }    dependencies {        classpath 'com.android.tools.build:gradle:2.3.3'        // NOTE: Do not place your application dependencies here; they belong        // in the individual module build.gradle files    }}allprojects {    repositories {        jcenter()    }}task clean(type: Delete) {    delete rootProject.buildDir}

buildscript方法是定义了全局的相关属性,repositories定义了jcenter作为仓库。一个仓库代表着你的依赖包的来源,例如maven仓库。dependencies用来定义构建过程。这意味着你不应该在该方法体内定义子模块的依赖包,你仅仅需要定义默认的Android插件就可以了,因为该插件可以让你执行相关Android的tasks。

allprojects方法可以用来定义各个模块的默认属性,这里是声明了所有project默认的仓库源。

task clean声明了一个任务,任务名叫clean(也可以改为其它),任务类型是Delete(也可以是Copy),就是每当修改settings.gradle文件后点击同步,就会删除rootProject.buildDir下的文件(实际上我看到的效果是清除了External Libraries里的包,然后又添加了一次)。

模块内的build.gradle
模块内的gradle文件只对该模块起作用,而且其可以重写任何的参数来自于根目录下的gradle文件。该模块文件应该是这样:

apply plugin: 'com.android.application'android {    compileSdkVersion 22    buildToolsVersion "22.0.1"    defaultConfig {        applicationId "com.gradleforandroid.gettingstarted"        minSdkVersion 14        targetSdkVersion 22        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:appcompat-v7:22.2.0'}

plugin
该文件的第一行是Android应用插件,该插件我们前面已经介绍过,其是google的Android开发团队编写的插件,能够提供所有关于Android应用和依赖库的构建,打包和测试。

android
该方法包含了所有的Android属性,而唯一必须得属性为compileSdkVersion和buildToolsVersion:

compileSdkVersion:编译该app时候,你想使用到的api版本。
buildToolsVersion:构建工具的版本号。

构建工具包含了很多实用的命令行命令,例如aapt,zipalign,dx等,这些命令能够被用来产生多种多样的应用程序。你可以通过sdk manager来下载这些构建工具。

defaultConfig方法包含了该app的核心属性,该属性会重写在AndroidManifest.xml中的对应属性。

defaultConfig {       applicationId "com.gradleforandroid.gettingstarted"       minSdkVersion 14       targetSdkVersion 22       versionCode 1       versionName "1.0"}

第一个属性是applicationId,该属性复写了AndroidManifest文件中的包名package
name,但是关于applicationId和package
name有一些不同。在gradle被用来作为Android构建工具之前,package
name在AndroidManifest.xml有两个作用:其作为一个app的唯一标示,并且其被用在了R资源文件的包名。

Gradle能够很轻松的构建不同版本的app,使用构建变种。举个例子,其能够很轻松的创建一个免费版本和付费版本的app。这两个版本需要分隔的标示码,所以他们能够以不同的app出现在各大应用商店,当然他们也能够同时安装在一个手机中。资源代码和R文件必须拥有相同的包名,否则你的资源代码将需要改变,这就是为什么Android开发团队要将package name的两大功能拆分开。在AndroidManifest文件中定义的package name依然被用来作为包名和R文件的包名。而applicationid将被用在设备和各大应用商店中作为唯一的标示。

接下来将是minSdkVersion和targetSdkVersion。这两个和AndroidManifest中的很像。minSdkVersion定义为最小支持api。

versionCode将会作为版本号标示,而versionName毫无作用。

所有的属性都是重写了AndroidManifest文件中的属性,所以你没必要在AndroidManifest中定义这些属性了。

buildTypes方法定义了如何构建不同版本的app,我们将在后面介绍。

dependencies
依赖模块作为gradle默认的属性之一(这也是为什么其放在了Android的外面),为你的app定义了所有的依赖包。默认情况下,我们依赖了所有在libs文件下的jar文件,同时包含了AppCompat这个aar文件。我们将会在下一篇博客中讨论依赖的问题。

 
 
  
ok,现在基本上对gradle有了一个大体上的认识,那么现在开始自己的Task吧!

如果你想知道你多少tasks可以用,直接运行gradlew tasks,其会为你展示所有可用的tasks。当你创建了一个Android工程,那么将包含Android tasks,build tasks,build setup tasks,help tasks,install tasks,verification tasks等。

基本的tasks
android插件依赖于Java插件,而Java插件依赖于base插件。

base插件有基本的tasks生命周期和一些通用的属性。

base插件定义了例如assemble和clean任务,Java插件定义了check和build任务,这两个任务不在base插件中定义。

这些tasks的约定含义:

  • assemble: 集合所有的output
  • clean: 清除所有的output
  • check: 执行所有的checks检查,通常是unit测试和instrumentation测试
  • build: 执行所有的assemble和check

Java插件同时也添加了source sets的概念。
android插件继承了这些基本tasks,并且实现了他们自己的行为:

  • assemble 针对每个版本创建一个apk
  • clean删除所有的构建任务,包含apk文件
  • check 执行Lint检查并且能够在Lint检测到错误后停止执行脚本
  • build 执行assemble和check

默认情况下assemble tasks定义了assembleDebug和assembleRelease,当然你还可以定义更多构建版本。除了这些tasks,android 插件也提供了一些新的tasks:

  • connectedCheck 在测试机上执行所有测试任务
  • deviceCheck 执行所有的测试在远程设备上
  • installDebug和installRelease 在设备上安装一个特殊的版本
  • 所有的install task对应有uninstall 任务

BuildConfig和resources

android {    buildTypes {        debug {            buildConfigField "String", "API_URL",               "\"http://test.example.com/api\""               buildConfigField "boolean", "LOG_HTTP_CALLS", "true"     }       release {            buildConfigField "String", "API_URL",                "\"http://example.com/api\""               buildConfigField "boolean", "LOG_HTTP_CALLS","false"     }  }

类似这些定义的常量,当定义了这些属性后,你完全可以在代码中使用:BuildConfig.API_URL和BuildConfig.LOG_HTTP

最近,Android tools team也让其里面定义string变为可能:

android {       buildTypes {           debug {               resValue "string", "app_name", "Example DEBUG"           }           release {               resValue "string", "app_name", "Example"            }        }}

你可以在代码中使用这些string。其中“”不是必须的。

你在全局的gradle文件中定义一些属性,然后再模块中运用它们。比如你可以在根目录下这么定义:

ext {       compileSdkVersion = 22       buildToolsVersion = "22.0.1"}  

那么你在子模块中就可以使用这些属性了:

android {       compileSdkVersion rootProject.ext.compileSdkVersion       buildToolsVersion rootProject.ext.buildToolsVersion }

三.依赖管理

当我们讨论依赖的时候,我们通常说的是远程仓库,就像那些依赖库专门用来提供给其他开发者使用的依赖库。手动管理依赖将会为你带来很大麻烦。你必须定位到该依赖文件位置,然后下载jar文件,复制该文件到你的项目,然后引用它们。通常这些jar文件还没有具体的版本号,所以你还必须去记忆它们的版本号,这样当需要更新的时候,你才会知道需要替换成哪个版本。你同时必须将该依赖包放在svn或者git上,这样你的其他同事才可以不用手动去下载这些依赖jar。

使用远程仓库可以解决这些问题,一个仓库可以被视为一些文件的集合体。Gradle不会默认为你的项目添加任何仓库。所以你需要把它们添加到repositories方法体内。如果是使用的是Android studio,那么工具已经为你准备好了这一切:

repositories {    jcenter()}

Gradle支持三种不同的仓库,分别是:Maven和Ivy以及文件夹。依赖包会在你执行build构建的时候从这些远程仓库下载,当然Gradle会为你在本地保留缓存,所以一个特定版本的依赖包只需要下载一次。

一个依赖需要定义三个元素:group,name和version。group意味着创建该library的组织名,通常这会是包名,name是该library的唯一标示。version是该library的版本号,我们来看看如何申明依赖:

dependencies {       compile 'com.google.code.gson:gson:2.3'       compile 'com.squareup.retrofit:retrofit:1.9.0'}

上述的代码是基于groovy语法的,所以其完整的表述应该是这样的:

dependencies {      compile group: 'com.google.code.gson', name: 'gson', version:'2.3'      compile group: 'com.squareup.retrofit', name: 'retrofit',version: '1.9.0'}

为了方便,Gradle会默认预定义三个maven仓库:Jcenter和mavenCentral以及本地maven仓库。你可以同时申明它们:

repositories {       mavenCentral()       jcenter()       mavenLocal()}

Maven和Jcenter仓库是很出名的两大仓库。我们没必要同时使用他们,在这里我建议你们使用jcenter,jcenter是maven中心库的一个分支,这样你可以任意去切换这两个仓库。当然jcenter也支持了https,而maven仓库并没有。

本地maven库是你曾使用过的所有依赖包的集合,当然你也可以添加自己的依赖包。默认情况下,你可以在你的home文件下找到.m2的文件夹。除了这些仓库外,你还可以使用其他的公有的甚至是私有仓库。
有些组织,创建了一些有意思的插件或者library,他们更愿意把这些放在自己的maven库,而不是maven中心库或jcenter。那么当你需要是要这些仓库的时候,你只需要在maven方法中加入url地址就好:

repositories {       maven {           url "http://repo.acmecorp.com/maven2"       }}

同样的,Ivy仓库也可以这么做。Apache Ivy在ant世界里是一个很出名的依赖管理工具。如果你的公司有自己的仓库,如果他们需要权限才能访问,你可以这么编写:

repositories {       maven {           url "http://repo.acmecorp.com/maven2"           credentials {               username 'user'               password 'secretpassword'           }      } }

可能有些情况,你需要手动下载jar包,或者你想创建自己的library,这样你就可以复用在不同的项目,而不必将该library publish到公有或者私有库。在上述情况下,可能你不需要网络资源,接下来我将介绍如何是使用这些jar依赖,以及如何导入so包,如何为你的项目添加依赖项目。

dependencies {       compile files('libs/domoarigato.jar')}

如果你这么做,那会很愚蠢,因为当你有很多这样的jar包时,你可以改写为:

dependencies {       compile fileTree('libs')}

默认情况下,新建的Android项目会有一个lib文件夹,并且会在依赖中这么定义(即添加所有在libs文件夹中的jar):

dependencies {       compile fileTree(dir: 'libs', include: ['*.jar'])}

这也意味着,在任何一个Android项目中,你都可以把一个jar文件放在到libs文件夹下,其会自动的将其添加到编译路径以及最后的APK文件。

用c或者c++写的library会被叫做so包,Android插件默认情况下支持native包,你需要把.so文件放在对应的文件夹中:

app
├── AndroidManifest.xml
└── jniLibs
   ├── armeabi
   │   └── nativelib.so
   ├── armeabi-v7a
   │   └── nativelib.so
   ├── mips
   │   └── nativelib.so
   └── x86
      └── nativelib.so

如果你想分享一个library,该依赖包使用了Android api,或者包含了Android 资源文件,那么aar文件适合你。依赖库和应用工程是一样的,你可以使用相同的tasks来构建和测试你的依赖工程,当然他们也可以有不同的构建版本。应用工程和依赖工程的区别在于输出文件,应用工程会生成APK文件,并且其可以安装在Android设备上,而依赖工程会生成.aar文件。该文件可以被Android应用工程当做依赖来使用。

不同的是,你需要加不同的插件:

apply plugin: 'com.android.library'

我们有两种方式去使用一个依赖工程。一个就是在你的工程里面,直接将其作为一个模块,另外一个就是创建一个aar文件,这样其他的应用也就可以复用了。

如果你把其作为模块,那你需要在settings.gradle文件中添加其为模块:

include ':app', ':library'

在这里,我们就把它叫做library吧,如果你想使用该模块,你需要在你的依赖里面添加它,就像这样:

dependencies {       compile project(':library')}

如果你想复用你的library,那么你就可以创建一个aar文件,并将其作为你的工程依赖。当你构建你的library项目,aar文件将会在 build/output/aar/下生成。把该文件作为你的依赖包,你需要创建一个文件夹来放置它,我们就叫它aars文件夹吧,然后把它拷贝到该文件夹里面,然后添加该文件夹作为依赖库:

repositories {    flatDir {        dirs 'aars'     }}

这样你就可以把该文件夹下的所有aar文件作为依赖,同时你可以这么干:

dependencies {       compile(name:'libraryname', ext:'aar')}

这个会告诉Gradle,在aars文件夹下,添加一个叫做libraryname的文件,且其后缀是aar的作为依赖。

有些时候,你可能需要和sdk协调工作。为了能顺利编译你的代码,你需要添加SDK到你的编译环境。你不需要将sdk包含在你的APK中,因为它早已经存在于设备中,所以配置来啦,我们会有5个不同的配置:

  • compile
  • apk
  • provided
  • testCompile
  • androidTestCompile

compile是默认的那个,其含义是包含所有的依赖包,即在APK里,compile的依赖会存在。

apk的意思是apk中存在,但是不会加入编译中,这个貌似用的比较少。

provided的意思是提供编译支持,但是不会写入apk。

testCompile和androidTestCompile会添加额外的library支持针对测试。

这些配置将会被用在测试相关的tasks中,这会对添加测试框架例如JUnit或者Espresso非常有用,因为你只是想让这些框架们能够出现在测试apk中,而不是生产apk中。

除了这些特定的配置外,Android插件还为每个构建变体提供了配置,这让debugCompile或者releaseProvided等配置成为可能。如果你想针对你的debug版本添加一个logging框架,这将很有用。这些内容的详细介绍,我会在下一个博客里详细介绍。

在一些情形中,你可能想使用最新的依赖包在构建你的app或者library的时候。实现他的最好方式是使用动态版本。我现在给你们展示几种不同的动态控制版本方式:

dependencies {       compile 'com.android.support:support-v4:22.2.+'       compile 'com.android.support:appcompat-v7:22.2+'       compile 'com.android.support:recyclerview-v7:+'}

当然,除了手动在配置文件中添加依赖外,你还可以通过android studio来添加:
这里写图片描述
这里写图片描述
 
 

四.构建变体

当你在开发一个app,通常你会有几个版本。大多数情况是你需要一个开发版本,用来测试app和弄清它的质量,然后还需要一个生产版本。这些版本通常有不同的设置,例如不同的URL地址。更可能的是你可能需要一个免费版和收费版本。基于上述情况,你需要处理不同的版本:开发免费版,开发付费版本,生产免费版,生产付费版,而针对不同的版本不同的配置,这极大增加的管理难度。

Gradle有一些方便的方法来管理这些问题。我们很早之前谈过debug和release版本,现在我们谈到另外一个概念,不同的产品版本。构建版本和生产版本通常可以合并,构建版本和生产版本的合并版叫做构建变种。

这一章我们将学习构建版本,它能使得开发更有效率,并且学习如何使用它们。然后我们会讨论构建版本和生产版本的不同,以及如何将其合并。我们会探讨签名机制,如何针对不同的变种签名等。

在这一章,我们遵循如下规则:

  • Build types
  • Product flavors
  • Build variants
  • Signing configurations

在Gradle的Android插件中,一个构建版本意味着定义一个app或者依赖库如何被构建。每个构建版本都要特殊的一面,比如是否需要debug,application id是什么,是否不需要的资源被删除等等。你可以定义一个构建版本通过buildTypes方法。例如:

android {       buildTypes {           release {               minifyEnabled false               proguardFiles getDefaultProguardFile                 ('proguard-android.txt'), 'proguard-rules.pro'           }        }}

这个文件定义了该模块是release版本,然后定义了proguard的位置。该release版本不是唯一的构建版本,默认情况下,还有个debug版本。Android studio把它视为默认构建版本。

当默认的构建版本不够用的时候,创建版本也是很容易的一件事,创建构建版本你只需要在buildTypes写入自己的版本。如下所示:

android {    buildTypes {        staging {            applicationIdSuffix ".staging"            versionNameSuffix "-staging"            buildConfigField "String", "API_URL", "\"http://staging.example.com/api\""         }    }}

我们定义了一个staging版本,该版本定义了一个新的application id,这让其与debug和release版本的applicationID不同。假设你使用了默认的配置,那么applicationID将会是这样的:

  • Debug: com.package
  • Release: com.package
  • Staging: com.package.staging

这意味着你可以在你的设备上安装staging版本和release版本。staging版本也有自己的版本号。buildConfigField定义了一个新的URL地址。你不必事事都去创建,所以最可能的方式是去继承已有的版本。

android {       buildTypes {           staging.initWith(buildTypes.debug)           staging {               applicationIdSuffix ".staging"               versionNameSuffix "-staging"               debuggable = false           }         }}

当你创建了一个新的构建版本,Gradle也创建了新的source set。默认情况下,该文件夹不会自动为你创建,所有你需要手工创建。

app
└── src
├── debug
│ ├── java
│ │ └── com.package
│ │
│ ├── res
│ │ └── layout
│ │ └── activity_main.xml
│ └── AndroidManifest.xml
├── main
│ ├── java
│ │ └── com.package
│ │
│ ├── res
└── MainActivity.java
└── Constants.java
│ │
│ │
│ │
│ └── AndroidManifest.xml
├── staging
│ ├── java
│ │ └── com.package
├── drawable
└── layout
└── activity_main.xml
│ │
│ ├── res
│ │ └── layout
│ │ └── activity_main.xml
│ └── AndroidManifest.xml
└── release
├── java
│ └── com.package
│ └── Constants.java
└── AndroidManifest.xml

注意:当你添加一个Java类的时候,你需要知道以下过程,当你添加了CustomLogic.java到staging版本,你可以添加相同的类到debug和release版本,但是不能添加到main版本。如果你添加了,会抛出异常。

当使用不同的source sets的时候,资源文件的处理需要特殊的方式。Drawables和layout文件将会复写在main中的重名文件,但是values文件下的资源不会。gradle将会把这些资源连同main里面的资源一起合并。

举个例子,当你在main中创建了一个srings.xml的时候:

<resources>       <string name="app_name">TypesAndFlavors</string>       <string name="hello_world">Hello world!</string></resources>

当你在你的staing版本也添加了rings.xml:

<resources>       <string name="app_name">TypesAndFlavors STAGING</string></resources>

然后合并的strings.xml将会是这样的:

<resources>       <string name="app_name">TypesAndFlavors STAGING</string>       <string name="hello_world">Hello world!</string></resources>

当你创建一个新的构建版本而不是staging,最终的strings.xml将会是main目录下的strings.xml。

manifest也和value文件下的文件一样。如果你为你的构建版本创建了一个manifest文件,那么你不必要去拷贝在main文件下的manifest文件,你需要做的是添加标签。Android插件将会为你合并它们。

每一个构建版本都有自己的依赖包,gradle自动为每一个构建的版本创建不同的依赖配置。如果你想为debug版本添加一个logging框架,你可以这么做:

dependencies {   compile fileTree(dir: 'libs', include: ['*.jar'])   compile 'com.android.support:appcompat-v7:22.2.0'   debugCompile 'de.mindpipe.android:android-logging-log4j:1.0.3'}

你可以结合不同的构建版本着不同的构建配置,就像这种方式,这让你的不同版本的不同依赖包成为可能。

和构建版本不同,product flavors用来为一个app创建不同版本。典型的例子是,一个app有付费和免费版。product flavors极大简化了基于相同的代码构建不同版本的app。

如果你不确定你是否需要一个新的构建版本或者product flavors,你应该问你自己,你是否需要内部使用和外部使用的apk。如果你需要一个完全新的app去发布,和之前的版本完全隔离开,那么你需要product flavors。否则你只是需要构建版本。

创建product flavors非常的容易。你可以在productFlavors中添加代码:

android {    productFlavors {        red {             applicationId 'com.gradleforandroid.red'             versionCode 3        }        blue {             applicationId 'com.gradleforandroid.blue'             minSdkVersion 14             versionCode 4        }    }}

product flavors和构建版本的配置不同。因为product flavors有自己的ProductFlavor类,就像defaultConfig,这意味着你的所有productFlavors都分享一样的属性。

就像构建版本一样,product Flavors也有自己的代码文件夹。创建一个特殊的版本就像创建一个文件夹那么简单。举个例子,当你有的生产版本的blue flavors有一个不同的app图标,该文件夹需要被叫做blueRelease。

在一些例子中,你可能需要创建一些product flavors的合并版本。举个例子,client A和client B可能都想要一个free和paid的版本,而他们又都是基于一样的代码,但是有不一样的颜色等。创建四个不同的flavors意味着有重复的配置。合并flavors最简单的做法可能是使用flavor dimensions,就像这样:

 android {       flavorDimensions "color", "price"       productFlavors {           red {               flavorDimension "color"           }           blue {               flavorDimension "color"           }           free {               flavorDimension "price"           }           paid {               flavorDimension "price"           }       }}

当你添加了flavor dimensions,你就需要为每个flavor添加flavorDimension,否则会提示错误。flavorDimensions定义了不同的dimensions,当然其顺序也很重要。当你合并二个不同的flavors时,他们可能有一样的配置和资源。例如上例:

  • blueFreeDebug and blueFreeRelease
  • bluePaidDebug and bluePaidRelease
  • redFreeDebug and redFreeRelease
  • redPaidDebug and redPaidRelease

构建变体是构建版本和生产版本的结合体。当你创建了一个构建版本或者生产版本,同样的,新的变体也会被创建。举个例子,当你有debug和release版本,你创建了red和blue的生产版本,那么变体将会有四个:
这里写图片描述

创建构建变体
gradle让处理构建变体变得容易。

android {       buildTypes {           debug {               buildConfigField "String", "API_URL",               "\"http://test.example.com/api\""          }           staging.initWith(android.buildTypes.debug)           staging {               buildConfigField "String", "API_URL",                 "\"http://staging.example.com/api\""               applicationIdSuffix ".staging"           }       }       productFlavors {           red {               applicationId "com.gradleforandroid.red"               resValue "color", "flavor_color", "#ff0000"           }           blue {               applicationId "com.gradleforandroid.blue"               resValue "color", "flavor_color", "#0000ff"           }      }}

在这个例子中,我们创建了4个变体,分别是blueDebug,blueStaging,redDebug,redStaging。每一个变体都有其不同的api url以及颜色。例如:
这里写图片描述
这里写图片描述

变体过滤器

忽略某个变体也是可行的。这样你可以加速你的构建当使用assemble的时候,这样你列出的tasks将不会执行那么你不需要的变体。你可以使用过滤器,在build.gradle中添加代码如下所示:

android.variantFilter { variant ->       if(variant.buildType.name.equals('release')) {           variant.getFlavors().each() { flavor ->               if (flavor.name.equals('blue')) { variant.setIgnore(true);            }        }    }}

这里写图片描述
你可以看到blueFreeRelease和bluePaidRelease被排除在外,如果你运行gradlew tasks,你会发现所有的关于上述变体的tasks不再存在。
 
 
签名配置
在你发布你的应用之前,你需要为你的app私钥签名。如果你有付费版和免费版,你需要有不同的key去签名不同的变体。这就是配置签名的好处。配置签名可以这样定义:

android {       signingConfigs {           staging.initWith(signingConfigs.debug)           release {               storeFile file("release.keystore")               storePassword"secretpassword"               keyAlias "gradleforandroid"               keyPassword "secretpassword"           }      }}

在这个例子中,我们创建了2个不同的签名配置。debug配置是as默认的,其使用了公共的keystore和password,所以没有必要为debug版本创建签名配置了。staging配置使用了initWith()方法,其会复制其他的签名配置。这意味着staging和debug的key是一样的。

release配置使用了storeFile,定义了key alias和密码。当然这不是一个好的选择,你需要在 Gradle properties文件中配置。

当你定义了签名配置后,你需要应用它们。构建版本都有一个属性叫做signingConfig,你可以这么干:

android {       buildTypes {           release {               signingConfig signingConfigs.release           }        }}

上例使用了buildTypes,但是你可能需要对每个版本生成不同的验证,你可以这么定义:

android {       productFlavors {           blue {               signingConfig signingConfigs.release           }       }}

当然,你在flavor中定义这些,最好会被重写,所以最好的做法是:

android {       buildTypes {           release {               productFlavors.red.signingConfig signingConfigs.red               productFlavors.blue.signingConfig signingConfigs.blue           }       }}

五.多模块构建

通常情况下,一个工程包含多模块,这些模块会在一个父目录文件夹下。为了告诉gradle,该项目的结构以及哪一个子文件夹包含模块,你需要提供一个settings.gradle文件。每个模块可以提供其独立的build.gradle文件。我们已经学习了关于setting.gradle和build.gradle如何正常工作,现在我们只需要学习如何使用它们。

这是多模块项目的结构图:

project   ├─── setting.gradle   ├─── build.gradle   ├─── app   │    └─── build.gradle   └─── library        └─── build.gradle

这是最简单最直接的方式来创建你的多模块项目了。setting.gradle文件申明了该项目下的所有模块,它应该是这样:

include ':app', ':library'

这保证了app和library模块都会包含在构建配置中。你需要做的仅仅只是为你的模块添加子文件夹。

为了在你的app模块中添加library模块做为其依赖包,你需要在app的build.gradle文件中添加以下内容:

dependencies {      compile project(':library') }

为了给app添加一个模块作为依赖,你需要使用project()方法,该方法的参数为模块路径。

如果在你的模块中还包含了子模块,gradle可以满足你得要求。举个栗子,你可以把你的目录结构定义为这样:

project├─── setting.gradle├─── build.grade├─── app│    └─── build.gradle└─── libraries     ├─── library1     │    └─── build.gradle     └─── library2          └─── build.gradle

该app模块依然位于根目录,但是现在项目有2个不同的依赖包。这些依赖模块不位于项目的根目录,而是在特定的依赖文件夹内。根据这一结构,你需要在settings.xml中这么定义:

include ':app', ':libraries:library1', ':libraries:library2'

你会注意到在子目录下申明模块也非常的容易。所有的路径都是围绕着根目录,即当你添加一个位于子文件夹下的模块作为另外一个模块的依赖包得实惠,你应该将路径定为根目录。这意味着如果在上例中app模块想要依赖library1,build.gradle文件需要这么申明:

dependencies {       compile project(':libraries:library1')}

如果你在子目录下申明了依赖,所有的路径都应该与根目录相关。这是因为gradle是根据你的项目的根目录来定义你的依赖包的。

构建生命周期

理解了构建过程让你理解多模块的构建变得容易。我们很早前谈过关于构建的生命周期。所以现在你应该知道其基本的过程,但是一些很重要的细节可能你并不是很清楚。

在第一步骤中,即初始化阶段,gradle会寻找到settings.grade文件。如果该文件不存在,那么gradle就会假定你只有一个单独的构建模块。如果你有多个模块,settings.gradle文件定义了这些模块的位置。如果这些子目录包含了其自己的build.gradle文件,gradle将会运行它们,并且将他们合并到构建任务中。这就解释了为什么你需要申明在一个模块中申明的依赖是相对于根目录。

一旦你理解了构建任务是如何将所有的模块聚合在一起的时候,那关于几种不同的构建多模块策略就会变得简单易懂。你可以配置所有的模块在根目录下的build.gradle。这让你能够简单的浏览到整个项目的配置,但是这将会变得一团乱麻,特别是当你的模块需要不同的插件的时候。另外一种方式是将每个模块的配置分隔开,这一策略保证了每个模块之间的互不干扰。这也让你跟踪构建的改变变得容易,因为你不需要指出哪个改变导致了哪个模块出现错误等。

gradle的最大策略是混合。你可以在根目录下定义一个build文件去定义所有模块相同的熟悉,然后在每个模块中的build文件去配置只属于该模块的参数。Android studio遵循了该原则,其创建了一个build.gradle文件在根目录,然后再每个模块文件夹下创建了另外一个build文件。

模块tasks
当你在你的项目中有多个模块的时候,你需要在运行任务之前想一想。当你在命令行界面运行一个task的时候,gradle将会找到哪个模块将会执行这个任务。举个栗子,当你有个mobile app模块和一个Android Wear模块,你运行了gradlew assembleDebug任务。当你改变其中一个模块的文件夹位置,gradle将只会运行哪个特殊的模块,纵使你使用了gradle wrapper在根目录。举个栗子,当你运行../gradlew assembleDebug在Android wear模块的目录下,其只会构建Android wear模块。

切换不同的文件夹去执行不同的任务会让人很不爽,幸运的是,我们有其他的办法。你可以准备一个特别的task来执行你的模块。举个栗子,为了只构建Android Wear模块,你仅仅只需在根目录下运行 gradlew :wear:assembleDebug。
为你的项目添加模块

在Android studio中添加新模块是很容易的一件事,该视图同时也会为你创建build文件。如下图所示:
这里写图片描述

添加Java依赖库
当你新建了一个Java模块,build.grade文件会是这样:

apply plugin: 'java'   dependencies {       compile fileTree(dir: 'libs', include: ['*.jar'])}

Java模块使用了Java插件,这意味着很多Android特性在这儿不能使用,因为你不需要。

build文件也有基本的库管理,你可以添加jar文件在libs文件夹下。你可以添加更多的依赖库,根据第三章的内容。

给你的app模块添加Java模块,这很简单,不是吗?

dependencies {       compile project(':javalib')}

这告诉了gradle去引入一个叫做javelin的模块吧,如果你为你的app模块添加了这个依赖,那么javalib模块将会总是在你的app模块构建之前构建。
添加Android依赖库

同样的,我们利用Android studio的图形化界面创建Android模块,然后其构建文件如下:

apply plugin: 'com.android.library'

记住:Android依赖库不仅仅包含了Java代码,同样也会包含Android资源,像manifest和strings,layout文件,在你引入该模块后,你可以使用该模块的所有类和资源文件。

当你有多个模块,Android studio会分析出来,并且展示在cradle中:
这里写图片描述

六.单元测试

相信大家都有了单元测试的概念,那么好的单元测试不仅仅能够确保app的质量,同时还可以让新代码开发更加容易。Android studio和gradle android插件默认支持单元测试,但是在你使用它之前,你仍需配置一下。

JUnit

JUnit测试界非常流行,其使得测试代码容易编写和维护,但是记住,JUnit只能测试逻辑代码,针对和Android SDK相关的代码其会报相应的错误。

在你开始编写junit测试之前,你需要为其新建一个目录。通常呢,这个会被叫做test,其会和你的main文件夹平级。

app└─── src     ├─── main          ├─── java          │    └─── com.example.app          └─── res     └─── test          └─── java               └─── com.example.app

你可以在test目录下创建测试类。

我建议你使用JUnit 4,你可以将其作为依赖添加到你的依赖库。

dependencies {       testCompile 'junit:junit:4.12'}

注意到你使用了testCompile,这意味着该jar包只会在你测试的时候导入apk。

如果你有其他的构建版本呢,而你又只是想为特定版本添加该jar,你只需要这么做:

dependencies {       testPaidCompile 'junit:junit:4.12'}

当所有的事情都OK了,就是时候开始写测试代码了。下面是简单的测试代码:

import org.junit.Test;   import static org.junit.Assert.assertEquals;   public class LogicTest {       @Test       public void addingNegativeNumberShouldSubtract() {           Logic logic = new Logic();           assertEquals("6 + -2 must be 4", 4, logic.add(6, -2));           assertEquals("2 + -5 must be -3", -3, logic.add(2, -5));       }}

那么如何跑起来呢,也很简单,运行gradlew test。如果你只是想再特定版本中跑呢,那就加一个呗gradlew testDebug。如果测试失败,gradle将会打印相关错误,如果所有测试成功通过,那么会显示BUILD SUCCESSFUL 。

可能你会说,单个测试用例导致整个测试失败,这样不好,如果你想把整个测试案例都跑一遍,那也很简单啊:

$ gradlew test --continue

执行测试任务不仅仅是跑完所有的测试,而且其还会为你创建一份测试报告,你可以找到它app/build/reports/tests/debug/index.html。这份报告让你能够更快的发现问题,我觉得最重要的是当你将你的测试自动化后,这会非常有用,gradle会为每个构建版本都创建一份测试报告。如果你执行测试成功,你的测试报告会是这个样子:
这里写图片描述
说了这么多原始的做法,那么看看Android studio怎么运行测试的吧。右键项目或者选择开始按钮。。这个太基础不多说了,运行成功是这个样子:
这里写图片描述
好了,junit测试讲完了,是不是很简单。

如果你想测试你的关联Android sdk代码怎么办,单元测试不是一个好主意,幸运的是,有多个依赖包供你选择,其中最出名的是Robolectric,其可以让你更方便的测试Android功能,并且还不用在设备或者模拟器上运行。

Robolectric

通过使用Robolectrie,你可以编写测试类,这些类可以使用Android SDK和资源文件,当然其还是跑在jvm上,这会让你测试app更加迅速。

在开始使用Robolectrie之前,你需要添加依赖。注意除了Robolectric依赖,你需要添加JUnit包。

apply plugin: 'org.robolectric'   dependencies {       compile fileTree(dir: 'libs', include: ['*.jar'])       compile 'com.android.support:appcompat-v7:22.2.0'       testCompile 'junit:junit:4.12'       testCompile'org.robolectric:robolectric:3.0'       testCompile'org.robolectric:shadows-support:3.0'}

Robolectrie测试类也需要写在test文件夹下,举个例子:

 @RunWith(RobolectricTestRunner.class)   @Config(manifest = "app/src/main/AndroidManifest.xml", sdk = 18)   public class MainActivityTest {       @Test       public void clickingButtonShouldChangeText() {           AppCompatActivity activity = Robolectric.buildActivity             (MainActivity.class).create().get();           Button button = (Button)             activity.findViewById(R.id.button);           TextView textView = (TextView)             activity.findViewById(R.id.label);           button.performClick();           assertThat(textView.getText().toString(), equalTo             (activity.getString(R.string.hello_robolectric)));        } }

功能测试

神马是功能测试,其是用来测试一个app的多个模块是否能够正常工作。举个栗子,你可以创建一个功能测试来确保你点击某一按钮后是否会有一个新的activity。依然,我们会有很多框架。但是在这里,我推荐Espresso。
Espresso

google创建Espresso的目的就是在于简化开发人员编写功能测试用例。这个包是由Android support repository提供,所以你可以通过SDK Manager使用它。

在运行测试用例之前,你需要定义一个runner。google提供了AndroidJUnitRunner测试runner,这将帮助你在手机上运行Unit测试。测试runner可以帮你安装apk以及一个测试apk,执行所有测试,生成测试报告。

假设你下载了support library包,那么你需要这么定义:

defaultConfig {       testInstrumentationRunner         "android.support.test.runner.AndroidJUnitRunner"}

当然你需要添加一些依赖包:

dependencies {       compile fileTree(dir: 'libs', include: ['*.jar'])       compile 'com.android.support:appcompat-v7:22.2.0'       androidTestCompile 'com.android.support.test:runner:0.3'       androidTestCompile 'com.android.support.test:rules:0.3'       androidTestCompile         'com.android.support.test.espresso:espresso-core:2.2'       androidTestCompile         'com.android.support.test.espresso:espresso-contrib:2.2'}

注意到这些依赖包使用了androidTestCompile,其不同于testCompile。当你直接运行时,会报错:

Error: duplicate files during packaging of APK app-androidTest.apk     Path in archive: LICENSE.txt     Origin 1: ...\hamcrest-library-1.1.jar     Origin 2: ...\junit-dep-4.10.jar

其意思也很清楚,因为多个文件导致,你可以简单处理下:

 android {     packagingOptions {     exclude 'LICENSE.txt'  }}

注意:功能测试需要放在AndroidTest目录下,下面是测试用例:

@RunWith(AndroidJUnit4.class)   @SmallTest   public class TestingEspressoMainActivityTest {       @Rule       public ActivityTestRule<MainActivity> mActivityRule = new         ActivityTestRule<>(MainActivity.class);       @Test       public void testHelloWorldIsShown() {           onView(withText("Hello world!")).check             (matches(isDisplayed()));        } }

功能测试也有测试报告,当正确执行后,应该是这样的:
这里写图片描述
最后可能有朋友问,在Android studio中执行测试,那就附图吧:
这里写图片描述

这里写图片描述

测试覆盖率

一旦你在你的项目中使用到了测试,那么你肯定想知道你的测试覆盖量。很真实,依然有很多测试覆盖率工具,我推荐的是Jacoco。
Jacoco

有一份覆盖率报告,很简单。你只需要配置一下:

buildTypes {     debug {       testCoverageEnabled = true     }}

当你执行完构建,你可以在app/build/ outputs/reports/coverage/debug/index.html中找到,每个版本都会有一个报告。测试覆盖率会是这样的:
这里写图片描述

总结下:全部看完的话应该可以有个大概的了解,至少再遇到这方面的代码时,有理论支持了.后面有必要的话可以看看groovy的语法.

参考:https://segmentfault.com/a/1190000004229002