How is the Android "productFlavors" mechanism implemented?

I’m trying to write a plugin for Android, but my Gradle knowledge isn’t quite good enough yet. Specifically, I’d like to allow dynamic “blocks” (closures?) nested under some plugin extension. Case of point, in Android you can define dynamic “product flavors” like this:

android {
    productFlavors{
        brandA {
            applicationId = "com.branda"  
        }
        brandB {
            applicationId = "com.brandb"  
        }
    }
}

…where brandA, brandB etc. are definitions being picked up and used by the plugin.

I know how handle “hardwired” flavors:
android.productFlavors.extensions.create(“brandA”,MyCustomPluginExtention)
…but this obviously won’t cut it. So my question is, how can I handle such a dynamic situation - clearly it’s possible because I see plugins use it.

EDIT: I get the impression, that methodMissing(…) is the way forward, although that seems to require a construct on the form:

android {
    productFlavors{
        brandA "com.branda"  
}

The ‘productFlavors’ block here is actually an instance of NamedDomainObjectContainer. You can create one of these by calling Project.container() and passing a container type. The type is generally a POJO, with the requirement that it must define a property called “name” and that it must have a constructor that takes the name as an argument.

class Foo {
  String name
  String bar

  Foo(String name) {
    this.name = name
  }
}

project.extensions.add('foos', project.container(Foo))

This would create a DSL that looks like:

foos {
  myFoo {
    bar = 'some value'
  }
}

Thanks for answering Mark, but how would Foo look like, with one more level:

moo{
    foos {
      myFoo {
        bar = 'some value'
      }
    }
}

Updated: I followed what was outlined in this small snippet: https://books.google.dk/books?id=jbgVAAAAQBAJ&pg=PA28&lpg=PA28&dq=dsl+NamedDomainObjectContainer&source=bl&ots=91YQTSJ_Xb&sig=lsufS_sGH1X8z5X8NAL02Xe7IbQ&hl=da&sa=X&ved=0CCkQ6AEwADgKahUKEwidxsG1t4HIAhWJkywKHVo1A-U#v=onepage&q=dsl%20NamedDomainObjectContainer&f=false

I have no idea how or why it works, but I get the impression it’s often like that with Gradle.

You would define an extension type with a container as a property. You can take a look at an example of what that might look like here:

Yeah it’s something like that I ended up doing… now I just have to figure out how to use the DSL to define dynamic build tasks inside the apply(…) method:

class DistributionExtension{
    final NamedDomainObjectContainer<ProductFlavor> productFlavors

    DistributionExtension(productFlavors){
        this.productFlavors = productFlavors
    }

    def productFlavors(Closure closure){
        this.productFlavors.configure(closure)
    }
}

class ProductFlavor {
    final String name
    String uploadUUID

    ProductFlavor(String name) {
        this.name = name
    }
}

class DistributionPlugin implements Plugin<Project> {
    def void apply(Project project) {
        
        def productFlavors = project.container(ProductFlavor)
        
        project.configure(project){
            extensions.create("distributions", DistributionsExtension, productFlavors)
        }
        
        productFlavors.each { distribution ->
            project.task("distribute$distribution.name") <<{
                println "Test"
            }
        }
    }    
}

…with this DSL:

distributions {
    productFlavors{
        A {
            uploadUUID = 'bff3750c-efeb-430b-b523-0781a6b700a3'
        }
        B {
            uploadUUID = '45771f0d-202c-47e9-9904-8aae8f4115bf'
        }
        C {
            uploadUUID = '991e998a-99f2-4c08-b086-c7ca3f934308'
        }
    }
}

…but to no avail; productFlavors is empty, so no tasks (deployA, deployB…) gets created. It makes me wonder, just where can I create new dynamic tasks based on a DSL - it doesn’t look like it’s in the apply(…) section.

“each” iterates on the collection “right now”, so when inside the “apply” method of your plug-in, the collection is empty.
You want to use “productFlavors.all{}” in your case

Ahh, that difference had eluded me! Thanks for the assistance guys.

Final little twist… what if I need to add two extension types, to parse something like this (two distinct collections under the root):

distributions {
    productFlavors{
        A {
            uploadUUID = 'bff3750c-efeb-430b-b523-0781a6b700a3'
        }
        B {
            uploadUUID = '45771f0d-202c-47e9-9904-8aae8f4115bf'
        }
        C {
            uploadUUID = '991e998a-99f2-4c08-b086-c7ca3f934308'
        }
    }
    buildTypes{
        debug{}
        release{}
    }
}

Groovy won’t let me do:

class DistributionsExtension{
    final NamedDomainObjectContainer<TpaProductFlavor> productFlavors
    final NamedDomainObjectContainer<TpaBuildType> buildTypes

    DistributionsExtension(productFlavors){
        this.productFlavors = productFlavors
    }
    
    DistributionsExtension(buildTypes){
        this.buildTypes = buildTypes
    }
      
    def productFlavors(Closure closure){
        this.productFlavors.configure(closure)
    }
    
    def buildTypes(Closure closure){
        this.buildTypes.configure(closure)
    }
}

…because of constructor clash. Also, it’s not at all clear to me, how I would go about installing this extension:

...
def productFlavors = project.container(ProductFlavor)
def buildTypes = project.container(BuildType)

project.configure(project){
    extensions.create("distributions", DistributionsExtension, productFlavors)
    extensions.create("distributions", DistributionsExtension, buildTypes)
}

?

Your constructor should take multiple arguments. Also, when calling extensions.create() the last argument is actually a multiple argument list of constructor args.

DistributionsExtension(productFlavors, buildTypes){
    this.productFlavors = productFlavors
    this.buildTypes = buildTypes
}

extensions.create("distributions", DistributionsExtension, productFlavors, buildTypes)

Ahh… had not noticed it was a vararg type. Thanks again, I’m learning so much about Gradle I’ll have to write a blog entry about it soon.

What about checking for dynamic properties when navigating such a DSL? I thought I could just write if(distriutions.productFlavors[‘debug’] == null){ … }

…but this fails:
ProductFlavor with name ‘debug’ not found.

I also tried with .hasProperty(‘debug’) but this is always false. I get the feeling I need to check for an item in a collection instead… somehow?!

Right. NamedDomainObjectContainer extends Collection, and therefore you can call contains
but in your case, you want to find an element from this collection by name, so you’d better use

distributions.productFlavors.findByName(‘debug’)