21

I used to have the following project flavors:

  1. Apple
  2. Orange

Originally the only difference was the applicationId/packageName. Now there is a single java file that is different. A custom ArrayAdapter to be exact. The solution was to create src/Apple and src/Orange and both inherit from src/main. I removed the java file from src/main and put a copy into src/Apple and src/Orange and modified it appropriately. All was good in the world.

Fast forward a few weeks, now there are about 10 java files that differ between Apple and Orange. Again... no big deal. Easy to handle. Separate java files in src/Apple and src/Orange.

Fast forward to today. I need to modify things up a bit, because I want to have a free and premium version of each. The free and premium versions only differ by a URL. I was going to simply create the new types called:

  1. AppleFree
  2. ApplePremium
  3. OrangeFree
  4. OrangePremium

I have a dilema though. Since now src/Apple and src/Orange have 10 different files that have been changed... if I change any java file in AppleFree I have to make sure I do the same in ApplePremium. I'm kind of at a crossroads and hope my question makes sense at this point. I have come up with three possible solutions, but I'm not sure how I would implement them/what would be the correct approach/the solution is not what I want.

Solution 1:

Use an if statement if (BuildConfig.FLAVOR==appleFree) {//use free Url} else {// use premium url}

Issue: Both Urls are technically compiled into the apk. I do not want this.

Solution 2:

Have src/AppleFree and src/ApplePremium inherit from an src/Apple parent directory somehow.

Issue: Not sure how I would do this.

Solution 3:

Add the free and premium url right in build.gradle like so?

    productFlavors {
            appleFree {
                applicationId "com.example.apple.free"
                versionName "1.0"
                url "http://freeurl.com"
                versionCode 1
            }
            applePremium {
                applicationId "com.example.apple.premium"
                versionName "1.0"
                url "http://premiumurl.com"
                versionCode 1
            }
            orangeFree {
                applicationId "com.example.orange.free"
                versionName "1.0"
                versionCode 1
                url "http://freeurl.com"


            }
            orangePremium {
                applicationId "com.example.orange.premium"
                url "http://premiumurl.com"
                versionName "1.0"
                versionCode 1

            }
        } 

Issue: Not sure how to make that work.

Any tips are helpful.

EDIT:

Final Solution?

flavorGroups 'fruit', 'paid'

productFlavors {
    apple {
        flavorGroup 'fruit'
    }
    orange {
        flavorGroup 'fruit'
    }
    free {
        flavorGroup 'paid'
    }
    premium {
        flavorGroup 'paid'
    }
    appleFree {
        applicationId "com.example.apple.free"
        versionName "1.0"
        buildConfigField 'String', 'BASE_URL', 'http://freeurl.com'
        versionCode 1
    }
    applePremium {
        applicationId "com.example.apple.premium"
        versionName "1.0"
        buildConfigField 'String', 'BASE_URL', 'http://premiumurl.com'
        versionCode 1
    }
    orangeFree {
        applicationId "com.example.orange.free"
        versionName "1.0"
        versionCode 1
        buildConfigField 'String', 'BASE_URL', 'http://freeurl.com'
    }
    orangePremium {
        applicationId "com.example.orange.premium"
        buildConfigField 'String', 'BASE_URL', 'http://premiumurl.com' 
        versionName "1.0"
        versionCode 1
    }
    }
EGHDK
  • 17,818
  • 45
  • 129
  • 204
  • For solution 1, the compiler should optimize out the code that isn't used, so it won't actually make it to the binary. – Scott Barta Oct 29 '14 at 16:58
  • Really? I believe I asked around on #android-dev irc and it seemed that no one was exactly sure about it. It would be nice if didn't make it to the binary. Anyway to make sure? (Besides decompiling) Taking an ADT devs word on it should suffice for me. Though, that leaves me with one more issue with solution #1. How do I make `appleFree` and `applePremium` from `productFlavors {}` be associated with `src/apple`? – EGHDK Oct 29 '14 at 17:02
  • It's a feature of the Java compiler that it optimize out unused code paths for `if` statements that have static final conditions. I know that `BuildConfig.DEBUG` works for sure. I would double-check `BuildConfig.FLAVOR` since it's a `String` but I expect it would work. I haven't had time to write up a full answer, but you should investigate flavor groups: http://tools.android.com/tech-docs/new-build-system/user-guide#TOC-Multi-flavor-variants – Scott Barta Oct 29 '14 at 17:06
  • Thanks for flavor groups. Makes sense that something like that would exist. I will try decompiling a sample app with BuildConfig.FLAVOR later on today, and if you are right about the compiler then it seems Solution 1 may be the way to go. Thanks for pointing me in the right direction. – EGHDK Oct 29 '14 at 17:10
  • Based on my test, it doesn't look like it will strip code if you do a conditional on a static final String. Booleans do work as expected. – Scott Barta Oct 29 '14 at 17:32
  • @ScottBarta I commented on your answer a few minutes ago, just wanted to ping you so I can at least know that you are notified of the comments. Not sure if you get a notification from a comment on an answer. Thanks – EGHDK Jan 29 '15 at 20:02
  • @ScottBarta started this new question a few days ago, so I added a bounty. You may be of service here: http://stackoverflow.com/questions/28237197/how-to-properly-set-multiple-applicationids-or-package-names-in-my-manifests – EGHDK Feb 04 '15 at 16:03

2 Answers2

13

There are many possible solutions to your problem. The most native-Gradle solution would be to use Flavor Dimensions, as documented in http://tools.android.com/tech-docs/new-build-system/user-guide#TOC-Multi-flavor-variants

This is also similar to what you were thinking about with Solution 2.

It would work something like this:

flavorDimensions 'fruit', 'paid'

productFlavors {
    apple {
        dimension 'fruit'
    }
    orange {
        dimension 'fruit'
    }
    free {
        dimension 'paid'
    }
    premium {
        dimension 'paid'
    }
}

This will give you build variants (and source folders) where it does the combination of all possibilities out of each flavor dimension, maintaining the same order as how the groups are specified in your flavorDimensions statement (i.e. it's appleFree, not freeApple), thus:

* appleFree
* applePremium
* orangeFree
* orangePremium

in your src/ folder, you can have these possibilities:

* src/main
* src/apple
* src/orange
* src/free
* src/premium
* src/appleFree
* src/applePremium
* src/orangeFree
* src/orangePremium

Solution 3

You can use the buildConfigField to specify constants that go in the BuildConfig class on a flavor-by-flavor basis:

productFlavors {
    appleFree {
        buildConfigField 'String', 'MY_URL', 'value1'
    }
    applePremium {
        buildConfigField 'String', 'MY_URL', 'value2'
    }
    orangeFree {
        buildConfigField 'String', 'MY_URL', 'value3'
    }
    orangePremium {
        buildConfigField 'String', 'MY_URL', 'value4'
    }

Solution 1

I was trying to work up something along the lines of Solution 1, but it won't work well for your exact use case. If you have an if condition in Java that tests against a boolean that's declared static final then the compiler can determine statically whether the code is reachable or not, and it will strip it if it's not. Thus:

static final boolean DEBUG = false;

...

if (DEBUG) {
    // do something
}

The code in // do something won't get compiled at all. This is an intentional and documented behavior on the part of the Java compiler, and allows you to write expensive debug code that won't get compiled into your release binary. BuildConfig.DEBUG is declared as static final for this exact reason.

There's a BuildConfig.FLAVOR, but it's defined as String, and you don't get the same benefit:

static final String FLAVOR = "orange";

...

if (FLAVOR.equals("apple")) {
    // do something
}

The compiler isn't smart enough to do static analysis, see that // do something is unreachable, and not compile it. Note that it will work fine at runtime, but that dead code will be included in your binary.

If it suits you, though, you could steal the buildConfigField approach from above and define an extra boolean variable in some variants that could allow code to be conditionally compiled in. This is more complex than defining the string directly as in Solution 3, but if you find yourself wanting to differentiate behavior without going through the trouble of making flavor-specific subclasses, you could go this route.

Ícaro
  • 1,432
  • 3
  • 18
  • 36
Scott Barta
  • 79,344
  • 24
  • 180
  • 163
  • One more thing... I think I'm going to go for a mixed approach to this. I want to use Solution 3, because I can see all of the varying factors between builds in one file. I still have the problem of if I change 1 file in appleFree I have to go and make that change in applePremium. How would I solve that? I can't use your approach in Solution 2 because then I can't give my unique applicationId's in my build.gradle. – EGHDK Oct 29 '14 at 18:27
  • You're talking about common code between all apple types? Maybe your best bet is to pick up flavor groups from solution 1 but do the BuildConfig customizations from solution 3. I **think** you can specify `buildConfigFields` per-variant when using flavor groups. I know you **cannot** specify them on flavor + build type combinations. Test it and see what happens. – Scott Barta Oct 29 '14 at 18:31
  • I can do `buildConfigFields` for things in the manifest? (i.e. `applicationId`, `versionName`, `versionCode`) And yes, the code between AppleFree and ApplePremium is identical EXCEPT for the url. Same goes for Orange. All orange variants have the same code except for the URL. – EGHDK Oct 29 '14 at 18:34
  • No, `buildConfigField` only sets what goes into the `BuildConfig` Java class. You can specify things like `applicationId` using the same build file DSL statements you're used to, on a flavor-by-flavor basis. Try it out and refer to the docs I linked to. – Scott Barta Oct 29 '14 at 18:35
  • Alright. Can you take a look at my edit? under "Final Solution?" Would that be plausible? – EGHDK Oct 29 '14 at 18:39
  • Looks reasonable -- try it out and see how it works. If you like, go ahead and edit my answer (maybe add it as a new solution?) instead of appending it to your question -- that keeps the Q&A format for Stack Overflow nice and clean. – Scott Barta Oct 29 '14 at 18:46
  • Thought it would work... but I get a gradle build error that orangeFree has no flavor dimension. – EGHDK Oct 29 '14 at 18:48
  • 1
    That's too bad -- it looks like it wants to treat orangeFree as a new thing, and not the combination of orange + Free. That hybrid approach may not work. – Scott Barta Oct 29 '14 at 18:51
  • So close. So no other way to basically make a buildFlavor parent huh? – EGHDK Oct 29 '14 at 18:52
  • Well the flavor group is the closest thing to what you want and does give you that parent-child relationship. You could make the URL a string resource. You lose the ability to specify all values of that URL in one place. If you really, really want to have that in the build file, you could look into writing build script, cribbing from http://stackoverflow.com/questions/22506290/buildconfigfield-depending-on-flavor-buildtype to get inspiration, but it would take some work to figure it out. If you go that route and run into problems, I'd recommend starting a new question for it. – Scott Barta Oct 29 '14 at 18:56
  • Alright! I want the parent-child relationship, but I want the ability to to set the applicationId (at the very, very least) of every child in the same build.gradle file. I may play around with that for a little while, and may start a new question based on that. Thanks again! – EGHDK Oct 29 '14 at 18:59
  • It occurs to me that you could also specify things like applicationId in the manifest instead of the build file, and have per-flavor manifests where you set it. The values in the build file override the manifest, but you can set it in the manifest if you don't have values in the build file. (This confuses a lot of new Android-Gradle users). You don't have everything in one place, but on the other hand, the script to make the build do that will be difficult to write and fragile. – Scott Barta Oct 29 '14 at 19:04
  • Yes, I think that may be the safest route to go. I will try a few things tonight and see what works best for my scenario. Thanks – EGHDK Oct 29 '14 at 19:06
  • Hard for me to read this in a comment. This deserves a separate question, maybe -- I don't have time this afternoon to look at it. – Scott Barta Jan 29 '15 at 22:30
  • Thanks. Would appreciate if you could take a look at this. Just added a new question: http://stackoverflow.com/questions/28237197/how-to-properly-set-multiple-applicationids-or-package-names-in-my-manifests – EGHDK Jan 30 '15 at 13:42
  • 1
    the functions have been renamed to flavorDimensions and flavorDimension – wutzebaer Apr 13 '17 at 10:47
0

Here's how I implemented product flavors inheritance.

  • stageLt (extends baseLt) - app for Lithuania using stage API
  • productionLt (extends baseLt) - app for Lithuania using production API
  • stagePl (extends basePl) - app for Poland using stage API
  • stagePl (extends basePl) - app for Poland using production API

android {
    flavorDimensions "default"

    productFlavors {
        def API_URL = "API_URL"
        def PHONE_NUMBER_PREFIX = "PHONE_NUMBER_PREFIX"
        def IBAN_HINT = "IBAN_HINT"

        baseLt {
            buildConfigField "String", PHONE_NUMBER_PREFIX, '"+370"'
            resValue "string", IBAN_HINT, "LT00 0000 0000 0000 0000"
        }
        basePl {
            buildConfigField "String", PHONE_NUMBER_PREFIX, '"+48"'
            resValue "string", IBAN_HINT, "PL00 0000 0000 0000 0000 0000 0000"
        }

        stageLt {
            dimension "default"
            applicationId "lt.app.stage"
            buildConfigField "String", API_URL, '"https://www.limetorrents.pro/"'
        }
        productionLt {
            dimension "default"
            applicationId "lt.app"
            buildConfigField "String", API_URL, '"https://yts.mx/"'
        }

        stagePl {
            dimension "default"
            applicationId "pl.app.stage"
            buildConfigField "String", API_URL, '"https://1337x.to/"'
        }
        productionPl {
            dimension "default"
            applicationId "pl.app"
            buildConfigField "String", API_URL, '"http://programming-motherfucker.com/"'
        }
    }

    // base product flavors will not show up in 'Build Variants' view.
    variantFilter { variant ->
        if (variant.name.startsWith("base")) {
            setIgnore(true)
        }
    }
}

void setupProductFlavor(baseFlavor, flavor) {
    flavor.buildConfigFields.putAll(baseFlavor.buildConfigFields)
    flavor.resValues.putAll(baseFlavor.resValues)
    flavor.manifestPlaceholders.putAll(baseFlavor.manifestPlaceholders)
    // Note that other product flavor properties ('proguardFiles', 'signingConfig', etc.) are not merged.
    // E.g. if in base product flavor you declare 'signingConfig', it will not be copied to child product flavor.
    // Implement if needed.
}

// Merge 'parent product flavors' with 'child product flavors'
setupProductFlavor(android.productFlavors.baseLt, android.productFlavors.stageLt)
setupProductFlavor(android.productFlavors.baseLt, android.productFlavors.productionLt)
setupProductFlavor(android.productFlavors.basePl, android.productFlavors.stagePl)
setupProductFlavor(android.productFlavors.basePl, android.productFlavors.productionPl)
Egis
  • 5,081
  • 5
  • 39
  • 61