11

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

Community
  • 1
  • 1
Sound Conception
  • 5,263
  • 5
  • 30
  • 47

3 Answers3

5

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.

gMale
  • 17,147
  • 17
  • 91
  • 116
  • Thanks for your response gmale. A final variable can't be assigned more than once. Though I could make activityX a non-final class-level variable (err yes sorry for the terminology there), so it can still be used in inner classes, which overcomes that issue. – Sound Conception Jul 11 '14 at 09:19
  • Regarding the codebase, unfortunately it's a large codebase that I've "inherited" to work on. It's structured along similar lines to the approach you have described in that all the classes were initially contained in main, and the flavors only really contained XML files for "skinning" the app to customise it. The problem is that more and more I am required to produce flavors in which some of the core classes have to behave in a new or different way than initially designed. Hence the desire to create custom versions in the flavors. – Sound Conception Jul 11 '14 at 09:36
  • @gmale I am trying to use inheritance to solve this problem (stub out fns in a superclass, and implement them in a subclass). I want gradle to substitute the subclass in for the superclass in one specific product flavor without gating with build config variables. Is this possible? – Daniel Smith Jul 16 '14 at 19:22
  • @DanielSmith the android gradle plugin doesn't really do complex resource resolution for JAVA files. Meaning, classes in "main" are not replaced by those in the flavor folders. So the options are to either put one class in every flavor (excluding 'main') OR to simplify the differences in behavior to the point where they can be represented in resource files. Often, the latter is the better approach: set it up so the main differences across flavors are resource values. For "complex" cases, layout files are great because you can use them to load entirely different custom view objects. – gMale Nov 10 '14 at 16:12
1

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

Rahul
  • 352
  • 1
  • 4
  • 17
0

I believe that extending your activities (and other classes) is a cleaner solution.

For example if you have a HelpActivity in the main flavor, you make that an abstract class, and in the flavor you create a FlavorHelpActivity that extends HelpActivity. Here you call super and add all the things that are unique to this flavor.

You need to update the manifest in each flavor to point to the correct flavor name of the activity (FlavorHelpActivity), and also your menu items must point correctly, so in the extended activities you must override the onOptionsItemSelected.

I'm going to try this solution, so perhaps later I can tell if there are drawbacks.

-- Update --

I've been trying my suggested approach, and it's not ideal:

  • Programming in activity that extends other activity is a bit weird. Half your logic is not in the class you are looking at.
  • You have quite some overhead on getting everything right in the manifest and how activities are linked together with intents.

It's do-able way of working, but I'll probably go back to the situation as described by the person who asked the question.

Julius
  • 159
  • 1
  • 1
  • 13