Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Workaround for duplicate classes in Gradle flavors and main

Tags:

android

gradle

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???

like image 374
Sound Conception Avatar asked Jul 10 '14 12:07

Sound Conception


2 Answers

No need to hard code Activity names.

Add intent filter for respective activities to be loaded as per flavour.

Flavour A : ActivityA.java

Flavour B : ActivityB.java

Case : Main(Common) : BaseActivity with button. On button click for flavour A should navigate to ActivityA and for flavour B should navigate to ActivityB.

Manifest for flavour A :

<activity
            android:name="com.abc.ActivityA"
            android:screenOrientation="portrait"
            android:theme="@style/AppTheme.NoActionBar">
            <intent-filter>
                <action android:name="com.abc.openDetailsActivity" />
                <category android:name="android.intent.category.DEFAULT" />
            </intent-filter>
        </activity>

Manifest for Flavour B :

<activity
            android:name="com.abc.ActivityB"
            android:screenOrientation="portrait"
            android:theme="@style/AppTheme.NoActionBar">
            <intent-filter>
                <action android:name="com.abc.openDetailsActivity" />
                <category android:name="android.intent.category.DEFAULT" />
            </intent-filter>
        </activity>

Both should have the same action in the manifest.

Now from BaseActivity call following :

Intent i = new Intent();
            i.setAction("com.abc.openDetailsActivity");
            startActivity(i);

If Build Variant is A, ActivityA will open from Flavour A and if Build Variant is B, ActivityB will open from Flavour B

like image 94
Rahul Avatar answered Sep 30 '22 15:09

Rahul


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.

like image 45
gMale Avatar answered Sep 30 '22 13:09

gMale