Workaround for duplicate classes in Gradle flavors and main

android, gradle

Solution

So there are two issues here 1) the coding bug in your main workaround 2) the broader problem you are trying to solve.

I can help more with the first issue than the second. All you need to do is initialize your variable. You asked "How can this be overcome???" I believe this will do the trick:

Class activityX = null;  //remove 'final' and initialize this to null, then null-check it later
if (BuildConfig.CUSTOM_ACTIVITY_X) {
    try {
        activityX = Class.forName("CustomActivityX");
    } catch (ClassNotFoundException e) {
        e.printStackTrace();
    }
} else {
    activityX = DefaultActivityX.class;
}

final Intent intent = new Intent();
...
if(activityX != null) {
    intent.setClass(ThisActivity.this, activityX); 
    startActivity(intent);
}

Now, regarding the broader issue you are solving, it's pretty clear that the code above has some code smells, which signal that there might be a better way. I've used flavor-specific classes without having to copy them to all the other flavors. In those cases, other flavors did not execute code that relied on those classes. For example, imagine a "paid" version where the "free" version simply never loads some of the classes available for paid users.

So I think the issue only arises if all flavors attempt to load the class in question. It's hard to suggest an alternative without understanding your overall codebase. However, I would suggest that you try using inheritance, abstract classes, or interfaces to solve your problem.

The first thing I would investigate: is the Activity really the smallest unit of code/behavior that you need to override? I suspect it isn't. Perhaps you can create a BaseActivity that has all the boilerplate code and then isolate the flavor-specific code into the exact components that require it.

For example, a frequent pattern I've used is to handle this kind of thing via XML files. That way, activities & fragments can always have the same behavior across flavors but they load different layouts. Those layouts contain custom view components (who extend from a common interface or abstract parent class, which allows the activity to code to that interface). Now flavors that don't need certain classes will never attempt to load them because they don't exist in the layout that is loaded by that particular flavor.

Anyway, I hope that helps in some way. It's very difficult to address your broader issue without understanding the nuances of your codebase. My advise would be to fundamentally rethink things and try to keep the class loader away from ever needing to load your flavor-specific classes.

Problem

The Problem: I imagine my problem is quite common. I have a fairly large gradle code base, from which I produce customised versions using product flavors. These product flavors will often require a customised version of one or more classes from `src\main\java`. I have read the Gradle documentation and have also come across the following questions looking at the same issue: Using Build Flavors - Structuring source folders and build.gradle correctly Build flavors for different version of same class I understand why you can't define the same class in `src\main\java` and also in your flavors, however the solution of moving the class from `src\main\java` into your product flavor has a fairly major drawback. When you move a class from `src\main\java` into your latest flavor to customise it, you also need to move a copy of the original uncustomised version of that class into every other previous product flavor or they won't build anymore. You may only need to tweak one or two different classes from the originals each time (and then have to re-distrubute those classes to the flavor directories), but over time the number of classes moved will build and the number remaining in `src\main\java` will decrease every time you have to do this. Eventually most of the classes will be in the flavors (even though the majority will be copies of the originals) and `src\main\java` will be almost empty, kind of defeating the purpose of the whole Gradle build structure. Additionally you'll need to keep a "default" flavor that you can clone each time you start a new flavor, so you know you're starting with all classes as per your original code base. My Initial Workaround: Use fields in the BuildConfig to define if custom class should be used or not: ``` buildConfigField 'boolean', 'CUSTOM_ACTIVITY_X', 'true' ``` You can then use code such as: ``` final Intent intent = new Intent(); ... if (BuildConfig.CUSTOM_ACTIVITY_X) { intent.setClass(ThisActivity.this, CustomActivityX.class); } else { intent.setClass(ThisActivity.this, DefaultActivityX.class); } startActivity(intent); ``` Every flavor will still need a copy CustomActivityX, but it can just be a dummy empty class in flavors where you know it will not be used. This means your default versions of the classes are always retained in `src\main\java`. An Improved Workaround: While trying to get rid of the need for a dummy CustomActivityX in every other flavor, I've looked at using `Class.forName()`. For example: ``` final Class activityX; if (BuildConfig.CUSTOM_ACTIVITY_X) { try { activityX = Class.forName("CustomActivityX"); } catch (ClassNotFoundException e) { e.printStackTrace(); } } else { activityX = DefaultActivityX.class; } final Intent intent = new Intent(); ... intent.setClass(ThisActivity.this, activityX); startActivity(intent); ``` However this obviously results in "activityX may not have been initialized" when you try to use it, because of the `try/catch` block. How can this be overcome???

Original source

Related problems