6

This question in continuation of my other question, which i want to improve further.

I am being able to group flavors (having common configuration) under sourceSets with the following code :

(got it from a genius in the linked question above)

import com.android.build.gradle.api.AndroidSourceSet
android {
    sourceSets {
        [flavor2, flavor4].each { AndroidSourceSet ss ->
            ss.assets.srcDirs = ['repo-assets/flavor2']
            ss.res.srcDirs = ['repo-res/flavor2']
        }
    }
}

Now, I was wondering if the list [flavor2, flavor4] could be pulled from any of the following:

  • An XML file over which i can iterate to get all flavors (which i'll put there)
  • A CSV file over which i can iterate and fetch values.
  • A custom class which i can write in a separate file and fetch data from static members in the class.

In addition to the flavor name, I intend to store the following in the external source (one of above):

  • application id (which i will pull to productFlavors)
  • ad unit ids (two per flavor)
  • some other custom values like category etc.

PORPOSE: I want to write a generic piece of code to iterate and dynamically create the productFlavors and sourceSets. I have generalized sourceSets to almost 90% and one block is now sufficing for all flavors.

It looks something like this now:

sourceSets {
    [flavor1, flavor2, flavor3 ...].each { AndroidSourceSet ss ->
                ss.assets.srcDirs = ['repo-assets/' + ss.name.split('_')[0]]
                ss.res.srcDirs = ['repo-mipmap/' + ss.name.split('_')[0] , 'repo-strings/' + ss.name]
            }
}

Also want to do the same thing for productFlavors as hinted above.

STUCK AT: getting the list [flavor2, flavor4] in the code above from an external source (along with a few additional fields per flavor as listed above).

I see methods like

productFlavors.add()
productFlavors.addAll()

but am not quite sure about how to go about using these. As the methods are available I'm sure it is possible to do what I'm trying to.

Has anyone done this and has some pointers?

Community
  • 1
  • 1
Viral Patel
  • 32,418
  • 18
  • 82
  • 110
  • May be [Dynamically generating product flavors](https://stackoverflow.com/a/74412271/2289835) and [Github sample code](https://github.com/RumitPatel/android-dynamic-flavours) helpful. – Rumit Patel Nov 13 '22 at 12:49

2 Answers2

10

I finally got it to work this way:

Created a custom class MyFlavor within build.gradle and added each flavor from the csv file to an ArrayList of MyFlavor

class MyFlavor {
    public String flavorname;
    public String basename;
    public String appid;
    public String bannerid;
    public String interstitialid;
    public String category;
}

def ArrayList<MyFlavor> myFlavors = new ArrayList<>();

new File('app/flavors.csv').eachLine {
    String[] values = "$it".split(',');
    MyFlavor f = new MyFlavor();
    f.flavorname = values[0];
    f.basename = values[0].split('_')[0];
    f.appid = values[1];
    f.bannerid = values[2];
    f.interstitialid = values[3];
    if(values[0].contains('_')) f.category= "state"
    else f.category = "country";
    myFlavors.add(f);
}

Then iterated over the ArrayList to dynamically create the productFlavors and sourceSets as follows:

productFlavors {
    myFlavors.each { MyFlavor f ->
        "$f.flavorname" {
            applicationId = "$f.appid"
            buildConfigField 'String', 'BANNER_ID', "\"" + "$f.bannerid" + "\""
            buildConfigField 'String', 'INTERSTITIAL_ID', "\"" + "$f.interstitialid" + "\""
            buildConfigField 'String', 'CATEGORY', "\"" + "$f.category" + "\""
        }
    }
}
sourceSets {
    myFlavors.each { MyFlavor f ->
        "$f.flavorname" {
            assets.srcDirs = ['repo-assets/' + "$f.basename"]
            res.srcDirs = ['repo-mipmap/' + "$f.basename" , 'repo-strings/' + "$f.flavorname"]
        }

    }
}

Hope this helps someone. I was successful in getting rid of 1000s of lines of code (because i have a lot of flavors) and bring it down to these 10s of lines you see here.

Viral Patel
  • 32,418
  • 18
  • 82
  • 110
  • 1
    I'm working on an white label app which was bloating the gradle file. This solution helps a lot. Thanks – muthuraj Dec 14 '17 at 18:42
2

For reading XML and CSV files you have the full power of Groovy on your hand. All Gradle scripts are meant to be written in Groovy. .each { Type var -> is part of that too. First result: https://stackoverflow.com/a/2622965/253468:

Given a CSV file like this:

#name,appId,ad1,ad2,category
flavor1,app.id.flavor1,adunit1324523,adunit2234234,messenger
flavor2,app.id.flavor2,adunit42346451,adunit4562,editor
flavor3,app.id.flavor2.gpe,adunit345351,adunit3545342,messenger

Groovy can load it like this:

import com.android.build.gradle.BaseExtension
import com.android.build.gradle.api.AndroidSourceSet
import com.android.build.gradle.internal.dsl.ProductFlavor
// TODO get a real CSV parser, this is hacky
new File("flavors.csv").splitEachLine(",") { fields ->
    if (fields[0].charAt(0) == '#' as char) return; // skip comments
    def flavorName = fields[0];
    def baseName = flavorName.split('_')[0];
    def appId = fields[1];
    BaseExtension android = project.android // project.getExtensions().getByName('android'); 
    // productFlavors is declared as Collection, but it is a NamedDomainObjectContainer
    // if [flavorName] doesn't work, try .maybeCreate(flavorName) or .create(flavorName)
    ProductFlavor flavor = android.productFlavors[flavorName];
    AndroidSourceSet sourceSet = android.sourceSets[flavorName];
    flavor.applicationId = appId;
    sourceSet.res.srcDirs = [] // clear
    sourceSet.res.srcDir 'repo-mipmap/' + baseName
    sourceSet.res.srcDir 'repo-strings/' + flavorName
}

Types are imported for readability and code completion, you can replace any variable type with def and it'll still work. These types are just what is being used when you're doing the regular android { ... } configuration. Internal types may change at any time, in fact I'm working with 1.5 they may already have changed in 2.0.

Community
  • 1
  • 1
TWiStErRob
  • 44,762
  • 26
  • 170
  • 254
  • will need to create instances of the `ProductFlavor` (class) and `AndroidSourceSet` (interface) and then add the properties and finally add the object using `android.productFlavors.add(` and same for sorucesets. But the problem now is that the constructor of `ProductFlavor` asks for weird parameters that i dont know where to get. And the `AndroidSourceSet` interface asks for 10s of method implementation. – Viral Patel Mar 29 '16 at 19:21
  • also tried creating a dummy flavor normally and then assigning it to a new `ProductFlavor` instance. But it wont allow me to change the properties saying they are read only. – Viral Patel Mar 29 '16 at 19:29
  • No need to create them manually, look at `NamedDomainObjectContainer.maybeCreate` that is what happens when you write `productFlavors { flavor1 {}}` – TWiStErRob Mar 29 '16 at 19:54
  • 1
    got it to work using a different approach (posted another answer). But thanks so much for your inputs. It gave me a direction. – Viral Patel Mar 29 '16 at 20:55