If you’re reading this, you’ve probably heard something about the new Gradle based Android build system. If not, I would suggest this fine read. The new build system is designed to replace both the build system inside ADT and Ant. This is great news since the old build system was broken. I will not focus on what was wrong with the old system. If you want more context, here’s another great read.
We’ve been playing around with the new build system during the last couple of weeks and I’m ready to write a post about a specific problem that we faced and couldn’t solve with the new build system cleanly. I won’t go too much into all the bells and whistles of the build system. This post is just about a hack we did that demonstrates some of the grooviness of the new, Gradle based, Android build system.
As you can see from the image above, we have a 28MB apk. And it’s a simple app, I promise. What went wrong?
That’s a lot of drawables… And we’re supporting devices with screen densities from mdpi all the way to xxhdpi. Especially painful demonstration of this problem would be if someone with mdpi screen device wants to run our app. These devices are commonly lower-end, and have less storage space as well. As a result, we’ll force our poor user to waste a considerable amount of his device’s scarce resource (storage) to run our app with utilization rate of 10%. Can we do better?
Here’s what we’re trying to accomplish. We want to build these 3 apks, upload them to Google Play and define which devices should be served which apk. Seems easy enough… We even have the <compatible-screens /> filter we can put into the AndroidManifest.xml to help Google Play discriminate devices by dpi. Another thing we mustn’t forget is that if we plan to deploy a multi-apk distribution of an app to the Google Play, each apk needs to have a different versionCode. The versionName can stay the same.
How would we do this with the old build system? Couple of solutions come to mind:
- Let’s be stupid. Every time I want to deploy a new version, I will manually remove all the drawables I don’t need for a specific apk, change the manifest and manually deploy each apk. I don’t think I have to stress how painful and error-prone this process can be
- Let’s create an Android library project. In it, we will put all the code and resources that are shared among all the apks. Let’s create three Android application projects, one for each apk. All three will depend on the library project and add their resources and manifest. When we want to build a new version, all we need to do is open each of these three projects and build each one individually.
- I don’t know… Use the version control? I don’t know what else we could do
As it turns out, the second solution is not half bad! If only we could do it simpler… Maybe the new build system can help?
Solution with the new build system
The new build system provides a much cleaner way to build customized versions of an app than Android library projects. This is done with the brand new feature of the build system, product flavors. You can read more about them on their official plugin page. You can even group and combine flavors! Read more about this awesome feature here.
To solve our problem, we can define a “dpi” flavor group that will contain “normal“, “xlarge” and “xxlarge” flavors.
As you can see, each of these will produce an apk with a different versionCode. As mentioned before, this is required for multi-apk distributions. There are guidelines defined by Google on how the versionCode should be calculated
As you can see from above, versionCode is a function of minSdk, dpi and versionName
Ok, now that we have three flavors defined for each dpi, we can divide the resources into four directories: we put all the stuff shared between all the flavors in the main directory, hdpi and mdpi drawables go to the normal directory, xhdpi to xlarge and xxhdpi to xxlarge.
The last thing is to modify <compatible-screens /> in the manifest for each flavor. This is where problems start. During the build of a flavored app, the build system is merging AndroidManifest.xml of the flavor with the AndroidManifest.xml in the main directory. Each tag in the manifest has a rule defined on how it should be merged. You can find all the rules in the javadoc for the ManifestMerger class. Here’s the link to its source. If you browse there, you will find that the <compatible-screens /> tag won’t merge, and will throw an error if it has different content in the library and the main project. The reasoning behind this was explained by the Android SDK Tech Lead, Xavier Ducrohet, in this forum response.
What can we do to get around this limitation? Gradle to the rescue! Gradle is a convention-over-configuration build system. Since the conventions of the Android plugin won’t do the trick we need, let’s define some conventions of our own:
- For each flavor of group “dpi“, we will read the content of the file CompatibleScreens.xml in that flavor’s source directory and replace the line in the merged and processed AndroidManifest.xml that contains an empty self-closing <compatible-screens /> tag.
- The content of CompatibleScreens.xml won’t go trough any further inspection. Whatever is contained there will directly be copied into the manifest that will end up in the apk. There is no room for error
Now we can write the code to alter the build process and apply a post-process task that will do this injection for us
Here, you can see how we can traverse all the application variants, apply some post-processing to the processManifest task and point the build system to our hacked AndroidManifest.xml. First of, application variant is a combination of one or more flavors and a build type. Application variant will have exactly n flavors, where n is the number of flavorGroups. If you don’t understand what I just said, please read this link again.
What we’re doing in the above piece of code is preparing the variables for the post-processing. Other than that, we’re adding our CompatibleScreens.xml file to the list of files that processManifest task depends on. This way, every time we change something in that file, result of the last run of processManifest will be invalidated in the next build. In the end, we change the reference to AndroidManifest.xml of the variant to our hacked version of the manifest.
In this code, I’m calling a function that gets the source directory for a flavor of dpi group. Here’s the implementation of that function
Now, all that’s missing is the post-process itself…
The process is quite simple. We copy the processed manifest to our hacked manifest directory and apply a filter that will replace the <compatible-screens /> tag with whatever is found in the CompatibleScreens.xml.
No doubt the new Android build system is a huge step forward for Android. It changes the development cycle significantly with its dependency management support, flavors, customizable build types and flexible build scripts. Most importantly, it unifies the build process in IDE and command line/CI servers. The system is still in development, and it will take some time to replace the old ADT/Ant combination.
I tried to demonstrate some of the grooviness of the new build system. Sure, it’s demonstrated by a hack, but I still think it’s cool to know how easily you can plug into the build process and customize the build logic when you absolutely need to.