Dynamically generating product flavors
Asked Answered
A

4

20

I've created an Android App which needs to be build in many (30+) flavors.

My idea was to generate the different productFlavors directly from the folder structure in my src directory, since the configuration is always very similar (basically just another packageName, a new launcher icon and some strings change).

The src folder look like this:

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

If I had to create the gradle properties by hand it would look somehow like this:

android {

    ....

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

}

Everytime I try to change the productFlavors configuration after its creation I get either an error or the changes / additions are ignored silently.
This could a be problem caused by me, because my Gradle / Groovy experience is very limited, or this isn't possible.

I mostly get error, saying that GroupableProductFlavorDsl_Decorated could not be manipulated the way I want.

What I'm trying to archive should somehow look like this:

android {

    ....

    def flavors = getMyFlavorsFromFileSystem()

    productFlavors {

    }

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

}

Note: I know this question is basically an duplicate of an older question, which sadly was never answered. Since Gradle is kind of new to the Android world, I'm hoping to get more answers as since the last time the question was asked, because more developers are using Gradle now.

Update:

Here some very simple approaches I tried:

Variant 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.

*/

Variant 2:

android {

    productFlavors {

    }

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

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

/*

no error, but does not work

*/

Variant 3:

android {

    productFlavors {

    }

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

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

/*

no error, but does not work

*/

All of them as a Gist.

Addictive answered 7/1, 2014 at 16:35 Comment(8)
"Everytime I try to mess with the productFlavors configuration I get either an error or the changes / additions are ignored silently" -- it will be very difficult to help you when you do not explain what "mess with" means, what errors you get, what the "changes / additions" are that are ignored silently, etc.Aphesis
why create so many builds? Is your code not able to recognize which build it runs on and adjust it's methods accordingly? The Google/Android folks have done a good job in making the support libs work across flavorsOutcome
@Outcome I need that many builds because these apps are sold as white label applications to customers (different packageName, different Google Developer Account, diffrent keyStore). My company works in the print to web sector, so yes: I need that many builds.Addictive
(Warning: UGLY HACK) I would create a Bash script that modifies the gradle script and then builds the app.Emoryemote
@Aphesis I updated the section of the questionAddictive
"I mostly get error, saying that GroupableProductFlavorDsl_Decorated could not be manipulated the way I want" -- that was not especially useful. Your question is akin to visiting your doctor, indicating that you do not feel well, then spending 30 minutes yammering about your favorite football club without providing any symptoms. What are you doing specifically that generates errors, and what is the complete error output from the Gradle build?Aphesis
@Aphesis I added 3 very simple examples of what I've tried. One gives and error, the other two just don't do anything. No error, but also not productFlavors.Addictive
Check out my solution here : https://mcmap.net/q/663147/-dynamically-generating-productflavors-and-sourcesets-using-a-list-of-names-with-properties-in-a-csv-txt-file worked prettry well for me for mor than 300 flavorsBlinding
A
20

Solved by trial and error:

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
            }
        }
    }

}
Addictive answered 7/1, 2014 at 17:56 Comment(8)
Can you share the code that reads the filesystem to get the packageNames?Iceman
@SeanBarbeau I'd rather not. It's a little bit more complicated as I described above. (Reading directories, checking for various files, parsing some YAML and JSON...)Addictive
@Addictive Are you still creating the src/ folders manually or they are created somewhere else. If they are not created manually, would you mind to post that piece of code that creates the folder structure. Thx.Thorite
@Thorite src folders are created by hand. They contain different app icons and custom language strings.Addictive
Hi, I'm trying to accomplish the same thing. What is the $name for? (or rather, what is it? )Gallion
@Gallion its a variable and how loops can be done in Groovy.Addictive
Thanks, TheHippo. Figured it out after posting the comment (sorry about that.) By the way, did we do the same way of getting the package name? -- from my answer, or did you accomplish it in some other way?)Gallion
Check out my solution here : https://mcmap.net/q/663147/-dynamically-generating-productflavors-and-sourcesets-using-a-list-of-names-with-properties-in-a-csv-txt-file worked prettry well for me for mor than 300 flavorsBlinding
G
8

I know there's already an answer for this, but I kind of combined Chris.Zou's approach with TheHippo. And add my own method for doing this.

Basically, when dealing with flavors, we normally work with different directories under /app/src/ which contains resources. And since the directory name is equal to the package name, I simply listed the directories under that folder (excluded "main" and "androidTest").

So here's my complete, working gradle script:

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
        }
    }
}

Edit:

  • Would also like to add, the com.mypackagename is basically the root path for all flavors.
  • I have a separate script that copy-pastes the a flavor directory to the /app/src/ folder.
Gallion answered 15/10, 2015 at 15:40 Comment(0)
S
4

Since the question author didn't want to share his code for reading files. I'm gonna write about what I did. I put all the variants name in a file named "app/build_variants.txt", one line for each, something like this:

flavor1
flavor2
flavor3
flavor4
flavor5

And in "app/build.gradle":

//...other configs...

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

//...other configs

This will have the same result as the following code:

//...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

The key here is the line new File('app/build_variants.txt').eachLine { "$it" { resValue "string", "build_variant_name", "$it" } }. It reads the file 'app/build_variants.txt' and for each line, generate a product flavor with the name in the line it reads. the $it is the line it passes in. It just groovy's closure syntax. If you want a deeper understanding. I strongly recommend watching @Daniel Lew's video about groovy and gradle. It's awesome!.

Sneakbox answered 22/6, 2015 at 4:19 Comment(3)
Question was about how to generate flavor dynamically and not about how to read a file from a gradle build script.Addictive
@Addictive My answer also include the code to generate flavor dynamically, plus the code for reading a file, which may be help for people who whats to keep their product flavors in a separate file.Sneakbox
what if each line contains comma separated multiple values. How can i split and put them seperately in the generated flavors? In your example you are putting the whole line (which is just the flavor name). Which worked for me. But i want to pass along the application id and the resValues as well from the file. Is that possible?Blinding
C
1

This answer is very late but may be useful to someone. We have 200+ flavors and we manage them by only one .json file with dynamic flavors.

  1. Create keystores and projects_info folders inside app folder. Now create projects_info.json file inside projects_info folder and insert information for every flavor.
  • Put KeyStore file(s) in keystores folder.
  • Create different flavors folders in src folder and put respected code/resources.
{
  "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. Add this code into app level build.gradle file. And sync project with 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."
}
Coworker answered 12/11, 2022 at 10:57 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.