Android configuration specific to each combination of 2 flavor dimensions - android

I am trying to add a second flavor dimension to an existing Android project. I already had a dimension for different environments (DEV, BETA, PROD), each with their own backend API and their own application id (to be able to install apps connected to several environments on the same device). And now I want to add another dimension for 2 variants of my app, a general one with all the features, and a specific one with a subset of features. And in addition to that, I still have the default debug and release build types I want to keep.
So here is what my configuration in build.gradle looks like so far:
android {
compileSdkVersion 30
defaultConfig {
applicationId "com.example"
minSdkVersion 21
targetSdkVersion 30
versionCode 56
versionName "1.1.6"
testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
manifestPlaceholders = [
appAuthRedirectScheme: 'com.example'
]
}
signingConfigs {...}
buildTypes {
debug {...}
release {...}
}
flavorDimensions "env", "variant"
productFlavors {
dev {
dimension "env"
applicationIdSuffix ".dev"
resValue 'string', 'backend_url', 'https://dev.example.com/api/v1/'
manifestPlaceholders = [
appAuthRedirectScheme: 'com.example.dev'
]
}
beta {
dimension "env"
applicationIdSuffix ".beta"
resValue 'string', 'backend_url', 'https://beta.example.com/api/v1/'
manifestPlaceholders = [
appAuthRedirectScheme: 'com.example.beta'
]
}
prod {
dimension "env"
resValue 'string', 'backend_url', 'https://example.com/api/v1/'
manifestPlaceholders = [
appAuthRedirectScheme: 'com.example'
]
}
general {
dimension "variant"
applicationId "com.example"
}
specific {
dimension "variant"
applicationId "com.example.specific"
}
}
...
}
As you can see, I will be able to have an application variant specific to each environment/variant flavor by combining applicationId and applicationIdSuffix.
But I also need to have the corresponding mapping in manifestPlaceholders, knowing that the appAuthRedirectScheme placeholder is not integrated inside of my project's manifest, but in the openId Appauth dependency, so I can't just have several manifests in the various flavor directories like I have read elsewhere.
Is there a way to define a build setting in build.gradle that is specific to each flavor dimension combination? In other words, I would like to have different values of manifestPlaceholders for devGeneral, devSpecific, betaGeneral, betaSpecific, prodGeneral and prodSpecific.

I don't specially advice this solution, but you could try the following approach:
applicationVariants.all { variant ->
if(variant.productFlavors.get(0).name == "dev") {
if (variant.buildType.name == "debug" ) {
variant.getMergedFlavor().manifestPlaceholders = [
appAuthRedirectScheme: 'com.example.dev'
]
}
if (variant.buildType.name == "release" ) {
variant.getMergedFlavor().manifestPlaceholders = [
appAuthRedirectScheme: 'com.example.dev.foo'
]
}
}
if(variant.productFlavors.get(0).name == "beta") { ... }
if(variant.productFlavors.get(0).name == "prod") { ... }
}

Related

Dividing app into flavors with different source and libraries

I'm dividing my app into multiple flavors, some of them should have HMS and some of them should not, so far I've managed to sucessfully divide source code but I can't get this part of gradle config to work as I expect it:
if(rootProject.ext.useHuaweiLib == true) {
implementation 'com.huawei.agconnect:agconnect-core:1.4.1.300'
implementation 'com.huawei.hms:push:5.0.1.300'
}
Here I'm adding libraries only if useHuaweiLib is true, here is flavor division in app module:
flavorDimensions "purpose"
productFlavors {
client {
dimension "purpose"
buildConfigField "boolean", "USE_HUAWEI_LIB", "false"
}
defaultConfig {
dimension "purpose"
buildConfigField "boolean", "USE_HUAWEI_LIB", "false"
}
huawei_client{
dimension "purpose"
buildConfigField "boolean", "USE_HUAWEI_LIB", "true"
rootProject.ext.useHuaweiLib = true
}
sdk {
dimension "purpose"
buildConfigField "boolean", "USE_HUAWEI_LIB", "false"
}
}
and variable declaration in root module:
ext {
buildToolsVersion = "32.0.0"
compileSdkVersion = 31
minSdkVersion = 23
targetSdkVersion = 30
versionCode = 1
releaseName = "1.0"
versionName = "1.0.0"
useHuaweiLib = false
}
As you can see the variable is false by default and is only set to true in huawei_client flavor, but for some reason even if I choose 'client' flavor I still get access
to HMS api. I want to be able to select a flavors with different set of sources and libs.
You can add the flavor as a prefix to all dependency imports. Ditch the if statement and just use this:
huawei_clientImplementation 'com.huawei.agconnect:agconnect-core:1.4.1.300'
huawei_clientImplementation 'com.huawei.hms:push:5.0.1.300'
This way, the two librarys will only be added to the huawei_client flavor.

Best practice for creating mobile(ios, android) app artifact for non-prod and prod environments

I need to create a mobile app artifact for multiple env. The goal is to promote the same artifact across multiple envs (dev, qa, preprod and prod). The mobile artifact uses a saas url which changes from env to env. Please let me know the best practice to do so.
Currently when the artifact passes qa I create another artifact for pre-prod and finally for prod which is time-consuming and prone to mistakes.
I am thinking of creating an active env url and release version api. What is the best practice?
Thanks,
I think android-flavors will solve your problem.
I look like these examples below.
flavorDimensions "default"
productFlavors{
dev{
applicationId "com.amitgupta.trust_app_android.dev"
}
staging{
applicationId "com.amitgupta.trust_app_android.staging"
}
qa{
applicationId "com.amitgupta.trust_app_android.qa"
}
production{
applicationId "com.amitgupta.trust_app_android.production"
}
}
You make also use different URLs based on Different environment.
flavorDimensions "version"
productFlavors {
QA {
buildConfigField "String", "BASE_URL", '"http://qa.com/api/"'
}
production {
buildConfigField "String", "BASE_URL", '"http://production.com/api/"'
}
}
Please, look at these for its implementation.
https://medium.com/#hsmnzaydn/configuration-product-flavors-and-build-variants-in-android-bb9e54d459af
https://www.journaldev.com/21533/android-build-types-product-flavors
For iOS:
You can make use of Schemes and Build configurations in Xcode. Here's the official documentation: https://developer.apple.com/library/ios/recipes/xcode_help-project_editor/Articles/BasingBuildConfigurationsonConfigurationFiles.html
I hope this will be of any help.
The following is true if you use gradle:
For top level build.gradle, define appId parameter:
allprojects {
ext {
appId = 'com.my.app'
}
}
For app module's build.gradle, define flavors and use the above parameter:
android {
def globalConfig = rootProject.extensions.getByName("ext")
productFlavors {
dev {
applicationId globalConfiguration["appId"] + ".dev"
...
buildConfigField "String", "YOUR_ENDPOINT", "\"https://my.dev.env/\""
}
qa {
applicationId globalConfiguration["appId"] + ".qa"
...
buildConfigField "String", "YOUR_ENDPOINT", "\"https://my.qa.env/\""
}
}
Build app using specific flavor + use BuildConfig.YOUR_ENDPOINT in code.

Android - Changing Gradle.properties according to flavor

I'm building an Android app with React Native and I have troubles configuring some of my API key.
I have one of my API key in my Gradle.properties
RNGP_ANDROID_API_KEY=mysupersecretkey
And I have two different flavor: beta for testing and prod for production.
I would like to override the RNGP_ANDROID_API_KEY in my two flavors
Like this:
flavorDimensions 'env'
productFlavors {
beta {
dimension 'env'
applicationId "com.fake.package"
signingConfig signingConfigs.release
manifestPlaceholders=[google_api:"beta_key"]
}
prod {
dimension 'env'
applicationId "com.fake.package"
signingConfig signingConfigs.release
manifestPlaceholders=[google_api:"prod_key"]
}
}
It works well to override value in the manifest but I don't know how to change the Gradle.properties value.
Please let me know what I can do!
Instead of writing in Gradle.properties
Try to use it as below,
flavorDimensions 'env'
productFlavors {
beta {
dimension 'env'
applicationId "com.fake.package"
signingConfig signingConfigs.release
manifestPlaceholders=[google_api:"beta_key"]
buildConfigField 'String', 'RNGP_ANDROID_API_KEY', '"mysupersecretkey"'
}
prod {
dimension 'env'
applicationId "com.fake.package"
signingConfig signingConfigs.release
manifestPlaceholders=[google_api:"prod_key"]
buildConfigField 'String', 'RNGP_ANDROID_API_KEY', '"mysupersecretkey"'
}
}
You can access this variable by BuildConfig.RNGP_ANDROID_API_KEY

google play store - how to promote test app with different package name?

This is a question about package names in android. I currently have two build flavors in gradle. Production and Staging.
I have created a google play store account and i want users to alpha and beta test my app. The staging app currenly has a package name of:
com.mobile.myapp.staging while the production flavor has a package name of com.mobile.myapp.
so we have
com.mobile.myapp.staging vs com.mobile.myapp
in the end i clearly want to promote com.mobile.myapp to production not the staging. but i'd like the users to test with the staging variant for a long while (as its connected to staging apis . etc etc.)
How can i do this ? would i have to create two different apps in the google play store ? I am wondering if i have to do this as
they both have different package names. They both will be signed with the same keystore. Please help.
my gradle file looks like this:
apply plugin: 'com.android.application'
android {
compileSdkVersion 25
buildToolsVersion "25.0.3"
defaultConfig {
minSdkVersion project.ext.minimumSdkVersion
//check top level build.gradle file for attributes -
targetSdkVersion 25
applicationId "com.mobile.myapp"
versionCode 150010203
versionName 1.2.3
testInstrumentationRunner "android.support.test.runner.AndroidJUnitRunner"
//renderscriptTargetApi 25
//renderscriptSupportModeEnabled true
multiDexEnabled true
}
buildTypes {
release {
minifyEnabled false
proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro'
}
}
dexOptions {
javaMaxHeapSize "6g"
}//for out of memory gc overhead error
lintOptions {
abortOnError false
}
productFlavors {
def STRING = "String"
def BOOLEAN = "boolean"
def TRUE = "true"
def FALSE = "false"
def FLAVOR = "FLAVOR"
def RETROFIT_LOG_ALL = "RETROFIT_LOG_ALL"
def BASE_ENDPOINT = "BASE_ENDPOINT"
staging {
// applicationId "com.mobile.myapp.staging"
buildConfigField STRING, BASE_ENDPOINT, '"https://api.flyingSaucerxx-staging.com"'
buildConfigField BOOLEAN, RETROFIT_LOG_ALL, TRUE
manifestPlaceholders = [appDrawerName: "FlyingSaucer-Staging"]
applicationIdSuffix '.staging'
versionNameSuffix '-STAGING'
}
prod {
buildConfigField STRING, BASE_ENDPOINT, '"https://api.flyingSaucerxx.com"'
buildConfigField BOOLEAN, RETROFIT_LOG_ALL, FALSE
manifestPlaceholders = [appDrawerName: "FlyingSaucer"]
}
}
}
///.. dependencies below
It is not possible to use different package names in Google Play Store for the same app.
So the only option you have is to change package name of your staging app to production one. And submit it to alpha/beta testers. And sure watch out to not promote it to production.
Another option is to use other delivery channels like hockeyapp or crashlitics beta.

How to have two build flavors inherit from a root flavor in Android Studio?

I used to have the following project flavors:
Apple
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:
AppleFree
ApplePremium
OrangeFree
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
}
}
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.
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)

Categories

Resources