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.
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
.
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???
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
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.
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With