77

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:

  1. I looked at flavor dimensions, but turns out they are not applicable in this case.
  2. 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.

kenny
  • 1,095
  • 1
  • 12
  • 14
  • 1
    Off the cuff: have `CustomizationBase` in `main` with the common stuff. Have `Customziation` that just inherits from `CustomizationBase` with nothing else, duplicated in three flavors. Have `Customization` that overrides `CustomizationBase` as needed in the fourth flavor. – CommonsWare Feb 17 '15 at 14:30
  • @CommonsWare Sorry, my mistake, I didn't explain it well. This Customization class is actually an Activity, which also has a same layout for the 3 of them and a different one for the last one. Do you think I could apply something like that in this case too? – kenny Feb 17 '15 at 14:37
  • OK, just to make sure that I understand this: the one file in common between three flavors is a layout resource? – CommonsWare Feb 17 '15 at 14:40
  • @CommonsWare Both the Activity and the layout file. So for example flavor 1, 2 and 3 have the same UserRegistrationActivity.java and the same layout, but flavor 4 has a modified version of both. – kenny Feb 17 '15 at 14:45
  • 6
    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/`. 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. – CommonsWare Feb 17 '15 at 14:48
  • @CommonsWare Ah, thanks for the idea, I'll try using an extra source path and report how it goes! – kenny Feb 17 '15 at 15:18
  • @CommonsWare Thanks, I ended up doing it the way you suggested, I put a custom src folder and added it in sourcesets in gradle. Would you like to add it as an answer so I can accept? – kenny Feb 18 '15 at 20:48
  • 1
    Actually, it'll probably be better if you write up the answer, as you're the one with the working solution. I haven't tried using multiple directories this way before, though I recall seeing others use it elsewhere. – CommonsWare Feb 20 '15 at 23:38

3 Answers3

166

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
starball
  • 20,030
  • 7
  • 43
  • 238
Ravi K Thapliyal
  • 51,095
  • 9
  • 76
  • 89
  • This is really powerful and when combined with inheritance to provide hook methods even more! Thanks for sharing! – narko Jul 28 '17 at 09:16
  • I don't know why but i am not able to resolve Duplicate Class issue :/ followed your approach replaces `main` to `v1` and new flavor `v2` sharing code base of `v1`. any solution? – Sumeet Gohil Sep 27 '18 at 14:05
  • It doesn't work for me. If I create class with the same name in another's flavor package I still get "Redeclaration". – Den Jun 10 '20 at 09:45
  • 1
    just to add, common resources can also be added in similar way . We can have a common folder for app and resources and add to the sourceSet like this: sourceSets { flavour1 { manifest.srcFile 'src/flavour1/AndroidManifest.xml' java.srcDirs = ['src/flavour1/java', 'src/common/java'] res.srcDirs = ['src/flavour1/res', 'src/common/res'] } – Sonika Sep 29 '20 at 07:14
  • Not sure this solution works anymore. It didn't for me using Gradle 4.0.1. Maybe try multiple paths in srcDirs as explained here: https://stackoverflow.com/questions/29783594/tell-gradle-to-check-two-directories-for-main-java-source-code – AlanKley Nov 09 '20 at 18:31
  • Although helpful, this solution does not work for me with gradle 3.5.3. Here is an alternative solution: https://stackoverflow.com/a/66141092/2068732 – matdev Feb 10 '21 at 16:42
  • If I have 8 ~ 10 flavor,and only one flavor is different, this still a nightmare – peerless2012 Apr 14 '22 at 07:46
5

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

matdev
  • 4,115
  • 6
  • 35
  • 56
4

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.

reker
  • 2,063
  • 13
  • 12
  • That looks interesting but I can't get it to work gradle fails to sync. ApplicationVariant is highlighted in red. – Slion Apr 02 '20 at 13:15
  • 2
    @Slion Import com.android.build.gradle.api.ApplicationVariant or just remove it. Type of variable is not necessary in Groovy. – reker Apr 03 '20 at 02:47
  • Removing was the first thing I tried but it would still not take. Anyway somehow I'm now getting the merging behaviour I wanted without it. – Slion Apr 03 '20 at 04:29
  • @reker This is a solution for Java project only, right? Is there any similar way for Kotlin? – Francis Jun 08 '20 at 07:03
  • @Francis I don't know, it exclude source path from `variant.javaCompiler`. Kotlin may use other compiler, and I think it's similar, you can try yourself. – reker Jun 09 '20 at 09:03
  • This works... partially. While it compiles without errors, android studio will show lint error message. Any idea how to make this go away? – chitgoks Feb 10 '22 at 00:24
  • @chitgoks Turn off lint – reker Feb 10 '22 at 10:21