Monday, May 2, 2016

Android: Remove a dependency from a build variation

Android's build variations (flavor and buildType) feature is an awesome way of maintaining several cuts from the same source code. I've used it both for white-label branding solutions and for Free vs. Pro versions. However, sometimes you have different dependencies between the versions where you will usually have to live with the fact, that all variations of your app gets the superset of dependencies. There are ways around this however, and I used it to shave over 400Kb off from a Free variation to a Pro variation.

The problem

Java is a static language, so you can't just go ahead and rely on late-bound linking and have Proguard slice and dice. It's unclear to me, whether the Android variations mechanism extends to Java source code, but I don't think it does. It's also unclear to me, whether an SPI approach could be used, letting the build class-path inject different implementations of some API for use at compile-time. What IS clear to me however, is that we can avoid pulling in a dependency for a flavor its not needed for, which means we will have to use reflection for the flavor that actually needs the stuff.

Removing AdMob from the Pro variation

It doesn't really matter what this app does, the important aspect is that there's a Pro flavor which does not contain the Google Ads from the Free version. So the first thing we're going to need, is to tell Gradle that the AdMob dependency is only for the free flavor. With Gradle, this is as easy as prepending the flavor name in front of the scope, such that "compile" becomes "freeCompile":



Now the dependency is not included for the Pro version. The next thing we need to do, is fix the problem of the static linking with the Ads dependency. The culprit code looks like the following:

 
 
protected void installAd(){ final AdView adView = (AdView)findViewById(R.id.adView); // Only the "free" versions of layouts has an AdView if(adView != null){ final AdRequest adRequest = new AdRequest.Builder().build(); adView.loadAd(adRequest); } }

The above works great in the Free version, but since we modified the Gradle build script, the Pro version no longer compiles! The second thing we then need, is a way to only use the Google Ads for the Free flavor, and do so through reflection in order to not require compile-time linking.

 
 
protected void installAd(){ final View adViewInstance = findViewById(R.id.adView); // Only the "free" versions of layouts has an AdView if(adViewInstance != null){ try { final Class builderClass = Class.forName("com.google.android.gms.ads.AdRequest$Builder"); final Object builderInstance = builderClass.newInstance(); final Method buildMethod = builderClass.getDeclaredMethod ("build"); final Object adRequest = buildMethod.invoke(builderInstance); final Class adViewClass = Class.forName("com.google.android.gms.ads.AdView"); final Method loadAdMethod = adViewClass.getDeclaredMethod("loadAd", adRequest.getClass()); loadAdMethod.invoke(adViewInstance, adRequest); } catch (Exception e) { Log.w(TAG, "Ads could not be loaded due to: " + e.getMessage()); } } }


Note that in the code examples above there's actually no build flavor check to be seen. For this, I rely on the flavor mechanism again. Only the layout from the Free flavor has the R.id.adView view. If this view can not be found, as is the case for the Pro flavor, I skip over doing anything.



The third and final step is easy to forget. The powerful post-processing tool Proguard will remove all unused classes in an attempt to trim down the size of our shippable production build. However, we now rely on reflection in the code, which Proguard's static analysis of the source code won't be able to tell. In stead, we need to instruct Proguard NOT to optimize away the specific code that we rely on. If we don't do this, the reflection magic will have no effect and in my case that means no ads will load but it's also not uncommon to see the app simply crash! Edit your proguard-rules.pro and include the appropriate directives:

 
 
-keepclassmembers class com.google.android.gms.ads.* { 
   public *; 
} 
 

And while I'm in there, I remove all that logging which serves no purpose in a production build anyway. I think it's safe to assume that no end user is sitting with adb and logcat!

 
 
-assumenosideeffects class android.util.Log { 
    public static boolean isLoggable(java.lang.String, int); 
    public static int v(...); 
    public static int i(...); 
    public static int w(...); 
    public static int d(...); 
    public static int e(...); 
} 


There's a fourth and somewhat optional step to mention. The code that you removed, may also let you remove permissions from your application manifest. This is the case for me with the Google Ads, which requires "android.permission.INTERNET" and "android.permission.ACCESS_NETWORK_STATE". My Pro flavor no longer needs these, and no app should request more permissions than it needs, so lets remove this. Once again, the power of the Android flavor mechanism comes to the rescue. Previously I had just one AndroidManifest.xml located in /src/main folder. We can leave this one as the "master" and remove the AdMob specific stuff, which is not applicable to the Pro flavor. Then add a new AndroidManifest.xml under /src/free/ and include the stuff specific to the Free version, namely the requires permissions which will be merged into one.

<?xml version="1.0" encoding="utf-8"?> 
<manifest xmlns:android="http://schemas.android.com/apk/res/android" 
    package="com.bangbits.rejsekortscanner"> 
 
    <!-- This is needed for Google AdMob --> 
    <uses-permission android:name="android.permission.INTERNET" /> 
    <uses-permission android:name="android.permission.ACCESS_NETWORK_STATE"/> 
 
</manifest> 


That's it! The Pro flavor is now trimmed down to about 400Kb lighter. This results in a smaller download, speedier load-time and lower memory pressure. Do you know of a better way then please let me know though the comments!





Post a Comment