动态生成产品风味

19

我创建了一个Android App,需要构建多个(30+)口味。

我的想法是直接从src目录中的文件夹结构生成不同的productFlavors,因为配置总是非常相似的(基本上只是另一个packageName、一个新的启动图标和一些字符串的更改)。

src文件夹的结构如下:

└── src
    ├── flavor1
       ├── flavor2.keystore
       ├── res
    ├── flavor2
       ├── res
       ├── flavor2.keystore    
    └── main
        ├── AndroidManifest.xml
        ├── java
        └── res

如果我必须手动创建Gradle属性,它看起来会像这样:

android {

    ....

    productFlavors {
        flavor1 {
            packageName 'com.example.flavor1'
        }
        flavor2 {
            packageName 'com.example.flavor2'
        }
    }

}
每当我尝试在创建productFlavors配置后更改它时,要么会出现错误,要么更改/添加会被无声地忽略。
这可能是由于我的Gradle/Groovy经验非常有限,或者这是不可能实现的。
我大多数时候会遇到错误,提示无法按我想要的方式操作GroupableProductFlavorDsl_Decorated
我想要实现的目标应该类似于这样:
android {

    ....

    def flavors = getMyFlavorsFromFileSystem()

    productFlavors {

    }

    flavors.each { name, config ->
        productFlavors[name] << config
    }

}

注意: 我知道这个问题基本上是一个旧问题的重复,而那个问题很遗憾从未得到答案。由于Gradle在Android世界中有点新,我希望能够得到更多答案,因为现在有更多的开发人员正在使用Gradle。

更新:

下面是我尝试的一些非常简单的方法:

变体1:

android {

    productFlavors {

    }

    productFlavors['flavor1'] << {
        packageName "com.example.flavor1"
    }

    productFlavors['flavor2'] << {
        packageName "com.example.flavor2"
    }
}

/*

A problem occurred evaluating root project 'MyProject'.
> GroupableProductFlavorDsl with name 'flavor1' not found.

*/

变体2:

android {

    productFlavors {

    }

    productFlavors['flavor1'] = {
        packageName "com.example.flavor1"
    }

    productFlavors['flavor2'] = {
        packageName "com.example.flavor2"
    }
}

/*

no error, but does not work

*/

变体3:

android {

    productFlavors {

    }

    productFlavors['flavor1'] = [packageName: "com.example.flavor1"]

    productFlavors['flavor2'] = [packageName: "com.example.flavor2"]
}

/*

no error, but does not work

*/

他们都被放在一个Gist上。


为什么要创建这么多构建?你的代码不能识别它运行的构建并相应地调整方法吗?Google/Android的开发人员在使支持库跨越不同版本方面做得很好。 - Martin
1
@Martin,我需要这么多的构建是因为这些应用程序被以白标应用程序的形式销售给客户(不同的包名,不同的Google开发者账户,不同的密钥库)。我的公司从事印刷到网络领域的工作,所以是的:我需要这么多的构建。 - TheHippo
1
(警告:丑陋的黑客手段)我会创建一个Bash脚本,修改gradle脚本并构建应用程序。 - Julian Suarez
@CommonsWare 我更新了问题的部分。 - TheHippo
我经常会遇到错误,提示无法按照我想要的方式操作GroupableProductFlavorDsl_Decorated,这并不是特别有用。你的问题类似于去看医生,表示你感觉不舒服,然后花费30分钟唠叨你最喜欢的足球俱乐部,而没有提供任何症状。你具体在做什么会产生错误,并且Gradle构建的完整错误输出是什么? - CommonsWare
显示剩余3条评论
4个回答

20

通过试错法解决:

android {

    // let's assume these are return by a function which reads the filesystem
    def myFlavors = [
        flavor1: [
            packageName: "com.example.flavor1"
        ],
        flavor2: [
            packageName: "com.example.flavor2"
        ]
    ]

    productFlavors {
        myFlavors.each { name, config ->
            "$name" {
                packageName config.packageName
            }
        }
    }

}

@SeanBarbeau 我宁愿不这么做。如我上述所述,这需要进行一些更为复杂的操作。(读取目录,检查各种文件,解析一些 YAML 和 JSON 文件...) - TheHippo
嗨,我也在尝试做同样的事情。$name是什么?或者更确切地说,它是什么? - hadez30
@hadez30 是一个变量,以及在 Groovy 中如何使用循环。 - TheHippo
谢谢,TheHippo。在发表评论后我想到了解决方法(对此很抱歉)。顺便问一下,我们获取包名的方式是一样的吗?是从我的答案中得到的,还是你用其他方法实现的? - hadez30
请查看我的解决方案:https://dev59.com/CpXfa4cB1Zd3GeqPgYrG#36294908 对于超过300种变体,它对我非常有效。 - Viral Patel
显示剩余3条评论

8
我知道已经有关于这个问题的答案,但我结合了Chris.Zou和TheHippo的方法,并添加了自己的方法来解决这个问题。
基本上,当处理不同口味(flavors)时,我们通常需要在/app/src/目录下使用不同的目录来存储资源。由于目录名等于包名,所以我简单地列出了该文件夹下的目录(排除了"main"和"androidTest")。
所以这是我的完整、可工作的gradle脚本:
def map = [:]

new File("./app/src").eachFile() { file->

    def fileName = file.getName()
    if( fileName == "main" || fileName == "androidTest") {
        return
    }

    map.put(fileName, 'com.mypackagename.' + fileName )


}
productFlavors {
    map.each { flavorName, packagename ->
        "$flavorName" {
            applicationId packagename
        }
    }
}

编辑:

  • 还想补充一点,com.mypackagename基本上是所有flavor的根路径。
  • 我有一个单独的脚本将某个flavor目录复制到/app/src/文件夹中。

4

由于问题作者不想分享他的用于读取文件的代码,我将写出我的解决方法。我将所有变量名称放在一个名为"app/build_variants.txt"的文件中,每行一个,类似于以下内容:

flavor1
flavor2
flavor3
flavor4
flavor5

在“app/build.gradle”文件中:
//...other configs...

android {
    productFlavors {
        new File('app/build_variants.txt').eachLine { "$it" { resValue "string", "build_variant_name", "$it" } }
    }
}

//...other configs

这与以下代码将产生相同的结果:
//...other configs...

android {
    productFlavors {
        flavor1 { resValue "string", "build_variant_name", "flavor1" } 
        flavor2 { resValue "string", "build_variant_name", "flavor2" } 
        flavor3 { resValue "string", "build_variant_name", "flavor3" } 
        flavor4 { resValue "string", "build_variant_name", "flavor4" } 
        flavor5 { resValue "string", "build_variant_name", "flavor5" } 
    }
}

//...other configs

关键在于这行代码:new File('app/build_variants.txt').eachLine { "$it" { resValue "string", "build_variant_name", "$it" } }。它读取了文件'app/build_variants.txt',对于每一行生成一个名称为该行文本的产品风味。其中$it是它所传递的行。这就是Groovy闭包语法。如果你想更深入地了解,强烈推荐观看@Daniel Lew's video关于Groovy和Gradle的视频。太棒了!

问题是关于如何动态生成变体,而不是关于从Gradle构建脚本中读取文件的。 - TheHippo
2
@TheHippo 我的回答还包括动态生成flavor的代码,以及读取文件的代码,这可能对想要将产品flavor保留在单独文件中的人有所帮助。 - Chris.Zou
如果每行都包含逗号分隔的多个值。我该如何拆分并将它们分别放入生成的口味中?在您的示例中,您正在放置整行(仅为口味名称)。这对我有用。但是我还想从文件中传递应用程序ID和resValues。这可能吗? - Viral Patel

1
这个答案虽然很晚了,但对某些人可能仍有用处。我们有200多种口味,并通过一个带有动态口味的.json文件来管理它们。
  1. 在app文件夹内创建keystoresprojects_info文件夹。现在在projects_info文件夹中创建projects_info.json文件,并为每种口味插入信息。
  • 将KeyStore文件放入keystores文件夹中。
  • src文件夹中创建不同的口味文件夹,并放置相应的代码/资源。
{
  "projectsInfoJSONArray": [
    {
      "projectVariantName": "project1",
      "versionCode": 1,
      "versionName": "1.0",
      "applicationId": "com.project1",
      "storeFile": "keystores/MyKeyStore1.jks",
      "storePassword": "yourStorePassword",
      "keyAlias": "yourKeyAlias",
      "keyPassword": "yourKeyPassword",
      "isLive": 1
    },
    {
      "projectVariantName": "project2",
      "versionCode": 2,
      "versionName": "1.2",
      "applicationId": "com.project2",
      "storeFile": "keystores/MyKeyStore2.jks",
      "storePassword": "yourStorePassword",
      "keyAlias": "yourKeyAlias",
      "keyPassword": "yourKeyPassword",
      "isLive": 1
    }
  ]
}
  1. 将以下代码添加到应用级别的build.gradle文件中。然后使用gradle同步项目。
def applicationDefinitions = []

def projectsInfoFile = file('projects_info/projects_info.json')
def projectsInfoJSON = new JsonSlurper().parseText(projectsInfoFile.text)
def projectsInfoArray = projectsInfoJSON.projectsInfoJSONArray
def projectsInfoMap = [:]
projectsInfoArray.each { projectInfo ->
    projectsInfoMap[projectInfo.projectVariantName] = projectInfo

    applicationDefinitions.add(['name': projectInfo.projectVariantName, 'applicationId': projectInfo.applicationId])
}

applicationDefinitions.each { applicationDefinition ->
    def signingConfig = projectsInfoMap[applicationDefinition['name']]
    if (signingConfig.isLive == 1) android.productFlavors.create(applicationDefinition['name'], { flavor ->
        flavor.applicationId = applicationDefinition['applicationId']
        flavor.versionCode = signingConfig.versionCode
        flavor.versionName = signingConfig.versionName

        flavor.signingConfig = android.signingConfigs.create(applicationDefinition['name'])
        flavor.signingConfig.storeFile = file(signingConfig.storeFile)
        flavor.signingConfig.storePassword = signingConfig.storePassword
        flavor.signingConfig.keyAlias = signingConfig.keyAlias
        flavor.signingConfig.keyPassword = signingConfig.keyPassword
    }) else println "===> " + signingConfig.projectVariantName + " is Not LIVE. Excluding it from build."
}

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接