4

I'm trying to configure a build for Android. The project has many flavors, and in one flavor, I want to replace some classes from main. The structure is like this:

main - MyClass.java

flavorA, flavorB... flavorF use main's MyClass.java, no override

flavorG needs its own MyClass.java

I don't want to copy MyClass.java into each flavor's directory & remove it from main entirely, that would not be maintainable. Is there a way to use the exclude command to accomplish this? Something like...

 + src
   + main
     + java
       MyClass.java
       other common files
     AndroidManifest.xml
   + flavorA
     + java
       other flavor-specific files
     AndroidManifest.xml
   + flavorG
     + java
        MyClass.java
        other flavor-specific files
     AndroidManifest.xml

With a gradle setup like this...

productFlavors {
    flavora {
    }
    ...
    flavorg {
    }
}

sourceSources {
    flavora {
    }
    flavorb {
    }
    ...
    flavorg {
       main.java {
          exclude 'com/myapp/mypackage/MyClass.java'
       }
    }
}

The above doesn't work, am I doing something wrong? Is there another alternative?

Boaz
  • 4,549
  • 2
  • 27
  • 40
Deeko
  • 1,514
  • 9
  • 29

1 Answers1

3

You can use the following notation to control the JAR manifest (and specifically the 'Main-Class' attribute)

jar {
    manifest {
        attributes 'Main-Class': 'com.example.MyMain'
    }
}

And like everything in Gradle, it's a groovy script, so you can wrap portions of it with conditions to fork the behavior according to your needs.

Assuming you're passing the flavor as a -P flag (See Gradle properties and system properties) e.g. gradle build -Pflavor=flavorg the following should work:

jar {
    manifest {
        if (flavor == "flavorg") {
             attributes 'Main-Class': 'com.example.FlavoredMain'
        } else {
             attributes 'Main-Class': 'com.example.GenericMain'
        }
    }
}

P.S, keep in mind that the Jar closure as stated above is evaluated in configuration time, not execution timel meaning the main class condition is decided before tasks are being evaluated (See Build lifecycle).


Android

Per @Opals comment (sorry didn't notice that). To address the same issue in Android the quickest option is to have an AndroidManifest.xml per flavorg and the rest

android {
    sourceSets {
        main {
            manifest.srcFile 'main/AndroidManifest.xml'
        }
        flavorg {
            manifest.srcFile 'main/other/AndroidManifest.xml'
        }
    }
}

In the AndroidManifest.xml you have your main activity defined.

A better option would be to ensure you adhere to a proper folder structure

+ src
    + main
        + java 
        + res
        + AndroidManifest.xml
    + flavorg
        + java
        + res
        + AndroidManifest.xml

The AndroidManifest.xml in flavorg should contain only the difference from the main AndroidManifest.xml (namely the main activity change) and they will be merged according to the selected flavor

I suggest you review the following Merge multiple manifest files and Gradle flavors for android with custom source sets - what should the gradle files look like?


Android - Revised question

I would instead use a factory pattern that returns the right instance of "MyClass" in accordance to the activated flavor:

productFlavors {
    main {
       buildConfigField "string", "APP_FLAVOR", "main"
    }
    flavorg {
       buildConfigField "string", "APP_FLAVOR", "flavorg"
    }
}

Then, in the factory you can use:

if (BuildConfig.APP_FLAVOR.Equals("flavorg") {
   return new FlavorgMyClass();
} 
return new MyClass();

See Gradle Tips, specifically section "Share custom fields and resource values with your app's code"

Boaz
  • 4,549
  • 2
  • 27
  • 40
  • But OP asks about android application. Is it built to jar? – Opal Oct 30 '18 at 05:51
  • Thanks for the response. I'm not overriding an Activity, but rather, a regular Java class - specifically one used as a Dagger module, so it needs to be in the same package at compile-time across flavors. I've edited the question with the existing folder structure; for clarity, I'm already using the standard Android folder layout for flavors. What that does not provide for is the ability to override a Java class in main from within a flavor, which is what I'm trying to accomplish with 'exclude'. Does that make sense? – Deeko Oct 30 '18 at 12:22
  • Well, it really depends on what you're trying to acheive, you can probably find the right way to do that via your build, my question is "why? why solve it via build?" you have enough mechanisms in Java to solve this issue within code: you can "postpone" the decision on which class to load (and probably it would be better not to have the same class name in the same package) to the point of creation with a proper factory that reacts to the flavor with which you've built your code - https://stackoverflow.com/questions/32813776/android-how-to-get-flavors-applicationid – Boaz Oct 30 '18 at 18:49
  • What I'm switching out is a Dagger module, which has a reference to a class type as a method parameter, but you never actually instantiate the class, otherwise yes, I could easily use a factory of sorts. But Dagger does all that for you. According to several questions like https://stackoverflow.com/questions/34046289/share-a-dagger-2-component-between-two-different-flavors the solution is to move the component to each flavor, but as I said at the top, I have lots of flavors, most of which share these. Trying to keep the complexity down, hence trying to exclude from the rogue flavor. – Deeko Oct 30 '18 at 19:04
  • I believe the factory solution can still work, why can't you have a factory injected instead of the instance you want, and just get it from the factory? I don't know your exact use case and if you can even instantiate this class on your own (though I hope you do, for testability sake) – Boaz Oct 31 '18 at 14:54