Reputation: 18120
I used to have the following project flavors:
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:
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.
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.
Have src/AppleFree and src/ApplePremium inherit from an src/Apple parent directory somehow.
Issue: Not sure how I would do this.
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:
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
}
}
Upvotes: 21
Views: 7948
Reputation: 5141
Here's how I implemented product flavors inheritance.
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)
Upvotes: 0
Reputation: 80010
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.
Upvotes: 13