1. 配置 BuildTypes

配置 buildTypes 主要用于区分 debug 和 release 包。配置 buildTypes 需要使用到签名信息,为了确保签名信息的安全,可以把签名信息放在 local.properties 文件中,如下所示。

local.properties 文件通常会被配置为 git 忽略文件,不会提交到代码仓库。

# 签名信息
storePassword=123456
keyPassword=123456
keyAlias=key0
storeFile=keystore.jks

sdk.dir=C\:\\Develop\\AndroidSDK

打开 app 下的 build.gradle 配置签名及 buildTypes 如下:

Groovy 配置

// 读取 local.properties 配置信息
def keystorePropertiesFile = rootProject.file("local.properties")
def keystoreProperties = new Properties()
keystoreProperties.load(new FileInputStream(keystorePropertiesFile))

android {
    // ...
    // 签名配置
    signingConfigs {
        release {
            def appStoreFile = rootProject.file(keystoreProperties['storeFile'])
            def appStorePass = keystoreProperties['storePassword']
            def appKeyAlias = keystoreProperties['keyAlias']
            def appKeyPass = keystoreProperties['keyPassword']
            storeFile file(appStoreFile)
            storePassword appStorePass
            keyAlias appKeyAlias
            keyPassword appKeyPass
            v1SigningEnabled true
            v2SigningEnabled true
        }
    }

    // 构建类型配置
    buildTypes {
        release {
            minifyEnabled true
            signingConfig signingConfigs.release
            proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro'
            android.applicationVariants.all { variant ->
                variant.outputs.all {
                    outputFileName = "渠道测试-${variant.versionName}-${variant.versionCode}-${variant.buildType.name}.apk"
                }
            }
        }

        debug {
            minifyEnabled false
            signingConfig signingConfigs.debug
            proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro'
        }
    }
}

Kotlin DSL 配置

// 读取 local.properties 配置信息
val localProperties = gradleLocalProperties(rootDir)

android {
    // ...
    // 签名配置
    signingConfigs {
        val release by creating {
            storeFile = rootProject.file(localProperties["storeFile"].toString())
            storePassword = localProperties["storePassword"].toString()
            keyAlias = localProperties["keyAlias"].toString()
            keyPassword = localProperties["keyPassword"].toString()
        }
    }

    // 构建类型配置
    buildTypes {
        val release by getting {
            isMinifyEnabled = true
            signingConfig = signingConfigs.getByName("release")
            proguardFiles(getDefaultProguardFile("proguard-android-optimize.txt"), "proguard-rules.pro")
            android.applicationVariants.all { variant ->
                variant.outputs.all {
                    if (this is com.android.build.gradle.internal.api.ApkVariantOutputImpl) {
                        outputFileName = "渠道测试-${variant.versionName}-${variant.versionCode}-${variant.buildType.name}.apk"
                    }
                }
                true
            }
        }
        getByName("debug") {
            isMinifyEnabled = true
            signingConfig = signingConfigs.getByName("release")
            proguardFiles(getDefaultProguardFile("proguard-android-optimize.txt"), "proguard-rules.pro")
        }
    }
}

配置完成后,即可在 Build -> Generate Signed Bundle or APK 中看到配置的打包选项,如图 1.1 所示。

图1.1

由于前面通过 outputFileName 指定了APK文件名,由于中文乱码,运行项目可能会出现以下报错信息。

Installation did not succeed.
 The application could not be installed.

List of apks:
[0] 'C:\WorkSpace\Android\FlavorsDemo\app\build\intermediates\apk\debug\娓犻亾娴嬭瘯-1.0-1.apk'
Installation failed due to: 'Invalid File: C:\WorkSpace\Android\FlavorsDemo\app\build\intermediates\apk\debug\娓犻亾娴嬭瘯-1.0-1.apk'
Retry
Failed to launch an application on all devices

找到 Android Studio 安装目录下的 bin/studio64.exe.vmoptions 文件,在末尾添加如下内容,重启 Android Studio 即可。

-Dfile.encoding=UTF-8

2. 渠道配置

修改 app 下的 build.gradle 文件中的打包文件名配置并新增渠道配置如下:

Groovy 配置

// ...
android {
    // ...
    buildTypes {
        release {
            // ...
            // 修改打包文件名配置
            android.applicationVariants.all { variant ->
                variant.outputs.all {
                    outputFileName = "渠道测试-${variant.flavorName}-${variant.versionName}-${variant.versionCode}-${variant.buildType.name}.apk"
                }
            }
        }
        // ...
    }
    
    // 定义一个纬度
    // 解决 All flavors must now belong to a named flavor dimension
    flavorDimensions "default"

    // 配置渠道
    productFlavors {
        product1 {
            dimension "default"
        }
        product2 {
            dimension "default"
        }
    }
}

Kotlin DSL 配置

// ...
android {
    // ...
    buildTypes {
        val release by getting {
            // ...
            // 修改打包文件名配置
            android.applicationVariants.all { variant ->
                variant.outputs.all {
                    if (this is com.android.build.gradle.internal.api.ApkVariantOutputImpl) {
                        outputFileName = "渠道测试-${variant.flavorName}-${variant.versionName}-${variant.versionCode}-${variant.buildType.name}.apk"
                    }
                }
                true
            }
        }
        // ...
    }
    
    // 定义一个维度
    // 解决报错: All flavor must now belong to a named flavor dimension
    flavorDimensions += listOf("default")
    
    // 渠道配置
    productFlavors {
        create("product1") {
            dimension = "default"
        }
        create("product2") {
            dimension = "default"
        }
    }
}

这样我们打包的时候就可以选择不同渠道、不同类型的包,如图2.1所示。

图2.1

3. 为渠道包定制名称图标

每个渠道可以指定不同的包名,版本信息等,如下所示。

Groovy 配置

// ...
android {
    // ...
    // 配置渠道
    productFlavors {
        product1 {
            dimension "default"
            // 每个渠道可以指定不同的包名,版本信息等
            applicationId "com.viifo.flavorsdemo.product1"
            versionCode 1
            versionName "1.0"

            // buildConfigField 会配置到 BuildConfig 文件中, 在代码中使用用于判断是什么渠道
            // 实际上配置了渠道 BuildConfig 已经存在渠道信息变量 FLAVOR, 这里的 PRODUCT_FLAVORS 只做演示
            buildConfigField("String", "PRODUCT_FLAVORS", "\"product1\"")
            // 修改 AndroidManifest.xml 里渠道变量, 在 AndroidManifest。xml 中使用 ${app_icon} 引用
            manifestPlaceholders = [app_icon: "@mipmap/ic_launcher"]
            // 动态添加 string.xml 字段
            // 必须要保证 string.xml 中没有该字段,否则会出现同名的字符串,引起冲突
            resValue "string", "app_name", "渠道测试1"

        }
        product2 {
            dimension "default"
            // 每个渠道可以指定不同的包名,版本信息等
            applicationId "com.viifo.flavorsdemo.product2"
            versionCode 1
            versionName "1.0"

            // buildConfigField 会配置到 BuildConfig 文件中, 在代码中使用用于判断是什么渠道
            // 实际上配置了渠道 BuildConfig 已经存在渠道信息变量 FLAVOR, 这里的 PRODUCT_FLAVORS 只做演示
            buildConfigField("String", "PRODUCT_FLAVORS", "\"product2\"")
            // 修改 AndroidManifest.xml 里渠道变量
            manifestPlaceholders = [app_icon: "@mipmap/ic_launcher"]
            // 动态添加 string.xml 字段
            // 必须要保证 string.xml 中没有该字段,否则会出现同名的字符串,引起冲突
            resValue "string", "app_name", "渠道测试2"
        }
    }
}

Kotlin DSL 配置

// ...
android {
    // ...
    // 渠道配置
    productFlavors {
        create("product1") {
            dimension = "default"
            // 每个渠道可以指定不同的包名,版本信息等
            applicationId = "com.viifo.flavorsdemo.product1"
            versionCode = 1
            versionName = "1.0"
            
            // buildConfigField 会配置到 BuildConfig 文件中, 在代码中使用用于判断是什么渠道
            // 实际上配置了渠道 BuildConfig 已经存在渠道信息变量 FLAVOR, 这里的 PRODUCT_FLAVORS 只做演示
            buildConfigField("String", "PRODUCT_FLAVORS", "\"product1\"")
            // 修改 AndroidManifest.xml 里渠道变量, 在 AndroidManifest。xml 中使用 ${app_icon} 引用
            manifestPlaceholders += mapOf("app_icon" to "@mipmap/ic_launcher")
            // 动态添加 string.xml 字段
            // 必须要保证 string.xml 中没有该字段,否则会出现同名的字符串,引起冲突
            resValue("String", "app_name", "渠道测试1")
        }
        create("product2") {
            dimension = "default"
            // 每个渠道可以指定不同的包名,版本信息等
            applicationId = "com.viifo.flavorsdemo.product2"
            versionCode = 1
            versionName = "1.0"
            
            // buildConfigField 会配置到 BuildConfig 文件中, 在代码中使用用于判断是什么渠道
            // 实际上配置了渠道 BuildConfig 已经存在渠道信息变量 FLAVOR, 这里的 PRODUCT_FLAVORS 只做演示
            buildConfigField("String", "PRODUCT_FLAVORS", "\"product2\"")
            // 修改 AndroidManifest.xml 里渠道变量, 在 AndroidManifest。xml 中使用 ${app_icon} 引用
            manifestPlaceholders += mapOf("app_icon" to "@mipmap/ic_launcher")
            // 动态添加 string.xml 字段
            // 必须要保证 string.xml 中没有该字段,否则会出现同名的字符串,引起冲突
            resValue("String", "app_name", "渠道测试2")
        }
    }
}

4. 为渠道包定制资源

以上方法使用新增 string.xml 字符串的方式修改应用名称不便于国际化,且渠道包部分页面布局可能有不同的要求。这种情况下就需要为不同的渠道包指定不同的资源,如下所示。

Groovy 配置

// ...
android {
    // ...
    // 配置渠道
    productFlavors {
        product1 {
            dimension "default"
            // 每个渠道可以指定不同的包名,版本信息等
            applicationId "com.viifo.flavorsdemo.product1"
            versionCode 1
            versionName "1.0"

        }
        product2 {
            dimension "default"
            // 每个渠道可以指定不同的包名,版本信息等
            applicationId "com.viifo.flavorsdemo.product2"
            versionCode 1
            versionName "1.0"
        }
    }
    
    // 指定渠道资源目录
    sourceSets {
        // 默认使用资源( 配置夜间模式资源 res-night )
        main.res.srcDirs  = ['src/main/res', "src/main/res-night"]
        // 默认使用 assets 资源
        main.assets.srcDirs  = ['src/assets']

        // 渠道资源配置:渠道名称.res.srcDirs
        // 资源目录需要手动创建
        product1.res.srcDirs = ['src/main/res-product1']
        product1.assets.srcDirs  = ['src/assets-product1']
        product2.res.srcDirs = ['src/main/res-product2']
        product2.assets.srcDirs  = ['src/assets-product2']
    }
}

Kotlin DSL 配置

// ...
android {
    // ...
    // 渠道配置
    productFlavors {
        create("product1") {
            dimension = "default"
            // 每个渠道可以指定不同的包名,版本信息等
            applicationId = "com.viifo.flavorsdemo.product1"
            versionCode = 1
            versionName = "1.0"
        }
        create("product2") {
            dimension = "default"
            // 每个渠道可以指定不同的包名,版本信息等
            applicationId = "com.viifo.flavorsdemo.product2"
            versionCode = 1
            versionName = "1.0"
        }
    }
    
    // 指定渠道资源目录
    sourceSets {
        // 默认使用资源( 配置夜间模式资源 res-night )
        getByName("main") {
            res.srcDirs("src/main/res", "src/main/res-night")
            assets.srcDirs("src/assets")
        }
        // 渠道资源配置
        getByName("product1") {
            res.srcDirs("src/main/res-prod")
            assets.srcDirs("src/assets-product1")
        }
        getByName("product2") {
            res.srcDirs("src/main/res-other")
            assets.srcDirs("src/assets-product2")
        }
    }
    
}

这样就可以为不同的渠道定制不同的资源文件了(包括 drawable资源、mipmap资源、layout资源等),如图 4.1 所示。

图4.1

5. flavorDimensions

关于 flavorDimensions ,实际上是对不同维度和渠道的排列组合,如下所示:

Groovy 配置

// ...
android {
    // ... 
    // 定义纬度
    flavorDimensions "product", "style"
    // 配置渠道
    productFlavors {
        product1 {
            dimension "product"
        }
        product2 {
            dimension "product"
        }
        style1 {
            dimension "style"
        }
        style2 {
            dimension "style"
        }
    }
}

Kotlin DSL 配置

// ...
android {
    // ...
    // 定义纬度
    flavorDimensions += listOf("product", "style")
    // 渠道配置
    productFlavors {
       create("product1") {
           dimension = "default"
       }
       create("product2") {
            dimension = "default"
       }
       create("style1") {
            dimension = "style"
       }
       create("style2") {
            dimension = "style"
       }
    }
}

即有两个维度分别是 productstyleproduct 维度衍生出两个不同产品,style 维度衍生出两个不同的样式。维度和渠道排列组合后可组合成 产品1样式1产品1样式2产品2样式1产品2样式2,如图 5.1 所示。

图5.1