I am building 4 different flavors of my Android app.
I have a class Customization.java that is the same for 3 of them and different for 1.
Since I cannot put the same class both in the main folder and in the flavor folder, I now have to maintain 3 copies of the exact same class for those 3 flavors.
Is there any way that I could do with keeping just two versions of this class?
Things I have considered so far:
I looked at flavor dimensions, but turns out they are not applicable in this case.
Keeping just one file in one of the flavors and copying it through my build script.
I am wondering if there is something cleaner out of the box.
I would like to convert CommonsWare's comment to an answer. I'll then explain how the final directory setup should look like.
Well, you can override resources in flavors. So, have the common one
in main/res/layout/ and the flavor-specific one in
yourFlavorHere/res/layout/.
So, if the Customization activity's layout file is called activity_customization.xml, you'll leave its common copy shared among the three flavors under src/main/res/layout directory and place the modified layout xml to be used by, say flavorFour, under its corresponding source set directory src/flavorFour/res/layout.
The way this works is that since flavor one to three (unlike flavor four) haven't provided their own versions of activity_customization.xml, they'll inherit the one coming from the main source set.
It's the activity Java class that gets tricky. Another possibility for
that is to configure the flavors with the same activity implementation
to pull from two source directories: a flavor-specific one and a
common one with the common class implementation.
Unlike resources, Java code files are not merged or overridden. So, you can't have Java files with the same fully qualified class name under main as well as in any of your flavor source sets. If you do, you'll receive a duplicate class error.
To resolve this issue, the simplest solution is to move Customization activity out of the main and into each flavor source set. This works because the flavor directories are mutually exclusive (with each other, not with main) hence avoiding the conflict.
But this means three out of the four flavors have a duplicate copy of the activity - a maintenance nightmare - just because one of the flavors required some changes to it. To resolve this issue we can introduce another source directory that keeps just the common code files shared between the three flavors.
So, the build.gradle script would look something like
android {
...
productFlavors {
flavorOne {
...
}
flavorTwo {
...
}
flavorThree {
...
}
flavorFour {
...
}
}
sourceSets {
flavorOne.java.srcDir 'src/common/java'
flavorTwo.java.srcDir 'src/common/java'
flavorThree.java.srcDir 'src/common/java'
}
}
Notice the use of java.srcDir (and not srcDirs) which adds another Java source directory to the already existing default src/flavorX/java.
Now all we need to do is to drop the common Customization activity file in src/common/java to make it available to the flavors one to three. The modified version required by flavorFour would go under its own source set at src/flavorFour/java.
So, the final project structure would look something like
+ App // module
|- src
|- common // shared srcDir
|- java
|- path/to/pkg
|- CustomizationActivity.java // inherited by flavors 1, 2, 3
+ flavorOne
+ flavorTwo
+ flavorThree
+ flavorFour
|- java
|- path/to/pkg
|- CustomizationActivity.java // per-flavor activity class
|- res
|- layout
|- activity_customization.xml // overrides src/main/res/layout
|- main
+ java
|- res
|- layout
|- activity_customization.xml // inherited by flavors 1, 2, 3
I use this to override codes for 5 years, just add a piece of code to your build.gradle.
For lastest gradle plugin(>=3.4.0):
android {
......
applicationVariants.configureEach { ApplicationVariant variant ->
AndroidSourceSet flavorSourceSet = android.sourceSets.findByName(variant.productFlavors[0].name);
if (flavorSourceSet != null) {
String flavorPath = flavorSourceSet.java.srcDirs[0].path;
variant.javaCompileProvider.configure { task ->
task.exclude { FileTreeElement elem ->
!elem.isDirectory() && !elem.file.parent.startsWith(flavorPath) &&
new File(flavorPath, elem.path).exists();
}
}
}
}
For older gradle plugin:
android {
......
applicationVariants.all { ApplicationVariant variant ->
AndroidSourceSet flavorSourceSet = android.sourceSets.findByName(variant.productFlavors[0].name);
if (flavorSourceSet != null) {
variant.javaCompiler.doFirst {
String flavorPath = flavorSourceSet.java.srcDirs[0].path;
variant.javaCompiler.exclude { FileTreeElement elem ->
!elem.isDirectory() && !elem.file.parent.startsWith(flavorPath) &&
new File(flavorPath, elem.path).exists();
}
}
}
}
It'll find duplicate classes in the Main sourceset and then exclude it during compile to avoid class duplicate error.
Ravi K Thapliyal's solution did not work for me with gradle 3.5.3
Here is an alternative solution that works:
Move the code common to all your flavors into a folder such as:
src/common/java
Then copy your generic flavors code into a generic src directory:
src/generic/java
That is where your generic version of the Customization.java class should be copied into.
Then, create a copy of your flavor specific code into a specific src directory e.g.
src/specific/java
Your build.gradle script should then be updated as follows:
android {
...
productFlavors {
flavor1 {
...
}
flavor2 {
...
}
flavor3 {
...
}
flavor4 {
...
}
}
sourceSets {
flavor1.java.srcDirs('src/generic/java', 'src/common/java')
flavor2.java.srcDirs('src/generic/java', 'src/common/java')
flavor3.java.srcDirs('src/generic/java', 'src/common/java')
flavor4.java.srcDirs('src/specific/java', 'src/common/java')
}
}
Note: Your Customization.java class should be removed from src/common/java
This solution works also for activity classes
Related
Similar to flavor variants, I was expecting the ability to have source set variants for the same classname. For example,
androidTest directory
class RobotTest(val name: String = "androidTest")
main directory
class RobotTest(val name: String = "main")
test directory
class RobotTest(val name: String = "test")
Android Studio is okay when I have the same classname in just 2 of the sources:
main and test
main and androidTest
However, once I add the same class to all three, Android Studio shows an Analysis error in test and androidTest with the message:
Redeclaration: RobotTest
I ran the code in a unit test, instrumented test, and in the app and everything compiled fine (no build errors) and when logging RobotTest().name, I correctly get the 3 different values for each class context.
Does anyone have any idea how to fix this or if this isn't allowed?
When you have created a product flavor you should not place your varianted class into main source set.
Keep only common for all variants files into main
For example, you defined:
flavorDimensions("api")
productFlavors {
create("real") {
dimension = "api"
}
create("mock") {
dimension = "api"
}
}
Since that, you have exactly TWO build variants: real OR mock.
So, you have to have exactly TWO versions of your class (if it should be different):
/src/real/kotlin/com/example/SomeApi.kt
/src/main/kotlin/com/example/ (no file here)
/src/mock/kotlin/com/example/SomeApi.kt
UPD:
All above is correct only for sources! If you create 4 flavors, you must copy your Java/Kotlin class into 4 folders, you don't have a 'default' one.
For resources, the logic is different.
Gradle, at first, takes the resource file (some XML file) from the default folder 'main' and then replaces it with the overridden one from the flavor folder if the file exists there. If you create 4 flavors, you can keep a default in 'main' and you can override it only once.
I'm trying to configure a build for Android. The project has many flavors, and in one flavor, I want to replace some classes from main. The structure is like this:
main - MyClass.java
flavorA, flavorB... flavorF use main's MyClass.java, no override
flavorG needs its own MyClass.java
I don't want to copy MyClass.java into each flavor's directory & remove it from main entirely, that would not be maintainable. Is there a way to use the exclude command to accomplish this? Something like...
+ src
+ main
+ java
MyClass.java
other common files
AndroidManifest.xml
+ flavorA
+ java
other flavor-specific files
AndroidManifest.xml
+ flavorG
+ java
MyClass.java
other flavor-specific files
AndroidManifest.xml
With a gradle setup like this...
productFlavors {
flavora {
}
...
flavorg {
}
}
sourceSources {
flavora {
}
flavorb {
}
...
flavorg {
main.java {
exclude 'com/myapp/mypackage/MyClass.java'
}
}
}
The above doesn't work, am I doing something wrong? Is there another alternative?
You can use the following notation to control the JAR manifest (and specifically the 'Main-Class' attribute)
jar {
manifest {
attributes 'Main-Class': 'com.example.MyMain'
}
}
And like everything in Gradle, it's a groovy script, so you can wrap portions of it with conditions to fork the behavior according to your needs.
Assuming you're passing the flavor as a -P flag (See Gradle properties and system properties) e.g. gradle build -Pflavor=flavorg the following should work:
jar {
manifest {
if (flavor == "flavorg") {
attributes 'Main-Class': 'com.example.FlavoredMain'
} else {
attributes 'Main-Class': 'com.example.GenericMain'
}
}
}
P.S, keep in mind that the Jar closure as stated above is evaluated in configuration time, not execution timel meaning the main class condition is decided before tasks are being evaluated (See Build lifecycle).
Android
Per #Opals comment (sorry didn't notice that). To address the same issue in Android the quickest option is to have an AndroidManifest.xml per flavorg and the rest
android {
sourceSets {
main {
manifest.srcFile 'main/AndroidManifest.xml'
}
flavorg {
manifest.srcFile 'main/other/AndroidManifest.xml'
}
}
}
In the AndroidManifest.xml you have your main activity defined.
A better option would be to ensure you adhere to a proper folder structure
+ src
+ main
+ java
+ res
+ AndroidManifest.xml
+ flavorg
+ java
+ res
+ AndroidManifest.xml
The AndroidManifest.xml in flavorg should contain only the difference from the main AndroidManifest.xml (namely the main activity change) and they will be merged according to the selected flavor
I suggest you review the following Merge multiple manifest files and Gradle flavors for android with custom source sets - what should the gradle files look like?
Android - Revised question
I would instead use a factory pattern that returns the right instance of "MyClass" in accordance to the activated flavor:
productFlavors {
main {
buildConfigField "string", "APP_FLAVOR", "main"
}
flavorg {
buildConfigField "string", "APP_FLAVOR", "flavorg"
}
}
Then, in the factory you can use:
if (BuildConfig.APP_FLAVOR.Equals("flavorg") {
return new FlavorgMyClass();
}
return new MyClass();
See Gradle Tips, specifically section "Share custom fields and resource values with your app's code"
I have configured my project to have multiple product flavors. I have the following code in my module build.gradle:
android {
// Rest of the configuration omitted for clarity
buildTypes {
debug {
// ...
}
release {
// ...
}
}
productFlavors {
paid
free
}
}
If I create a file (any file: Java, resources, ...), in the paid, free, debug or release, it is recognized by Android Studio and I can use it in my project.
However, if the same file is created in paidDebug (or a similar folder) it is not recognized in Android Studio. Do I need any extra configuration for this to work? Is this not supported (yet)?
Source code files with the same name should be placed in all of the alternative source sets (but not in the 'main').
Either:
app/src/free/java/com/domain/myapp/MainActivity.java
app/src/paid/java/com/domain/myapp/MainActivity.java
Or:
app/src/freeDebug/java/com/domain/myapp/MainActivity.java
app/src/freeRelease/java/com/domain/myapp/MainActivity.java
app/src/paidDebug/java/com/domain/myapp/MainActivity.java
app/src/paidRelease/java/com/domain/myapp/MainActivity.java
Resource files with the same name can be placed in any source set, including 'main'.
app/src/main/res/layout/activity_main.xml
app/src/paidDebug/res/layout/activity_main.xml
app/src/paidRelease/res/layout/activity_main.xml
In this case, when building the 'free' flavor, the layout file from 'main' set will be used. But, during the build of the 'paid' flavor, the specific version of the layout will be used.
You can specify specific source directories in your build.gradle file. For example, to add testFreeRelease to unit test sources and testFree to android integration test sources:
android {
[...]
sourceSets {
main.java.srcDirs += 'src/main/java'
testFreeRelease.java.srcDirs += 'src/testFreeRelease/java'
androidTestFree.java.srcDirs += 'src/androidTestFree/java'
}
}
This also works for Kotlin. Just switch java with kotlin.
productFlavors {
India {
}
USA {
}
}
Lets take 2 product flavours for an example
1. India
2. USA
total number of build variants will be 4
1. IndiaDebug
2. IndiaRelease
3. USADebug
4. USARelease
Which files are common for all flavours and which files are flavour specific as well as debug and release specific ?
If localization is supported and if english is common language for India and USA then each build will have separate english file or common file ?
Product Flavour is a awesome solution to build different varieties of the same application with individual features.
Specific Files
Say like , one of your Activity will have different functionality and UI, then you can avoid keeping that Activity in common package and move to respective flavour. Each flavour can have separate java and res folder along with Manifest (which is not mandatory, Studio take care of itself). It is here your specific Activity's java file and xml file should be placed.
Example : Login Screen will have different UI and features in each flavour
Now during runtime as well as compile time, Android Studio switches between the packages and picks suitable files. This is done through Build Variant feature
Common Files
So coming to common files which is applicable is all the flavours, let it be in main/java and main/res itself.
Ideally depending on your flavour numbers, bundle.gradle will look similar to this.
productFlavors {
student {
applicationId "com.abc.student"
}
staff {
applicationId "com.abc.staff"
}
tempstaff {
applicationId "com.abc.tempstaff"
}
}
sourceSets {
tempstaff {
manifest.srcFile 'src/tempstaff/AndroidManifest.xml'
}
student{
manifest.srcFile 'src/student/AndroidManifest.xml'
}
staff {
manifest.srcFile 'src/staff/AndroidManifest.xml'
}
}
Now to conclude the answer, files which are common throughout the
application will remain in the main package. Specific files
applicable to separate flavour will go in that flavour. This means
flavours can have extra Activity/Features that are not at all a part
of others including main also
Go through this link for more information.
Our application has a free and a paid version. We also make branded-versions, which means that the application vary in two dimensions.
Four versions could be:
The App, Nike ed. free
The APP, Nike ed. paid
The App, Adidas ed. paid
The App, Adidas ed. free
My solution now is to have two build-types, paid and free:
buildTypes {
paid {
packageNameSuffix ".paid"
}
free {
packageNameSuffix ".free"
}
}
And two build flavors:
productFlavors{
nike{
packageName "com.example.theapp.nike"
}
adidas{
packageName "com.example.theapp.adidas"
}
}
Every free-version of the app make us of a content-provider, a content provider which is specific per flavor-build type combination. The problem is that I don't understand where to put a source file based on build variant. Source files put into /src/nike or /src/free will be picked up depending on build type or flavor. But how about source files that are depending on the build variant (the combination of type and flavor)?
You can create a new folder under src for every build or flavor that you have. i.e: 'free', 'paid', 'nike', 'adidas'.
The files that you put in any of these folders gets picked up when building depending on the type and build both.
According to Gradle Plugin User Guide on Android Tools Project Site:
Similar to Build Types, Product Flavors also contribute code and
resources through their own sourceSets.
and
The following rules are used when dealing with all the sourcesets used
to build a single APK:
All source code (src/*/java) are used together as multiple folders generating a single output.
Manifests are all merged together into a single manifest. This allows Product Flavors to have different components and/or
permissions, similarly to Build Types.
All resources (Android res and assets) are used using overlay priority where the Build Type overrides the Product Flavor, which
overrides the main sourceSet.
Each Build Variant generates its own R class (or other generated source code) from the resources. Nothing is shared between
variants.
meaning that your java files for the buildType "free" will overwrite the ones for your flavors "nike" if they have the same name.
But if you're adding something to a manifest, according to the second point in the list above the final manifest will be a merge of all of the manifests.
If you need more customization you can put your files in your build variant's folder "src/freeNike/".
I had similar problem with build types overriding flavors due to the overlay rules.
I ended up redirecting the build type source sets into different folders depending on which flavor was built.
android.applicationVariants.all { variant ->
switch (variant.name) {
case "FreeNike":
variant.mergeResources.doFirst {
android.sourceSets.free.setRoot("src/freeNike")
}
break;
case "FreeAdidas":
variant.mergeResources.doFirst {
android.sourceSets.free.setRoot("src/freeAdidas")
}
break;
case "PaidNike":
variant.mergeResources.doFirst {
android.sourceSets.paid.setRoot("src/paidNike")
}
break;
case "PaidAdidas":
variant.mergeResources.doFirst {
android.sourceSets.paid.setRoot("src/paidAdidas")
}
break;
}
}
You are of course free to use a different folder structure. See example here: Folder naming convention for gradle build variants
Have a look at newest Gradle plugin it now allows to have variant specific resources
http://tools.android.com/tech-docs/new-build-system
And here You have example of usage
https://android.googlesource.com/platform/tools/build/+/master/tests/overlay3/
Have you tried to put the srcDir in the sourceSets ?
Like so:
sourceSets {
main {
java {
srcDirs 'src/java'
}
}
}
That should output a javaResources with two source codes, nike and adidas.