Multi-Flavored, Multi-Moduled, Multi-Functional Gradle

Using flavors to optimise code and app size.

You want to build an app and instant app that can use the same code using Feature Modules right?

Hopefully the answer to that is: yes.

It was the exact solution we had decided to implement. To give a user the chance to try out our app in near full functionality before downloading the full version. The big thing for us, was to use the same code base for both apps. So we had this basic structure:

But what we wanted now, was something which could be used by all our feature modules but where the functionality was dependent on if it was the App, or the Instant App. What we didn’t want to do here was end up writing loads of conditional blocks with build config variables to determine whether we should do this or that. We also didn’t want the Instant App to import any of the dependency libraries that would be used only in the App. As it would therefore inflate its download size.

The best idea/solution we came up with was to build two separate modules with the same package name and have one with the same interface but that returned nothing to the calls or default null values.

Having then attempted this solution I began to realise it was simply not possible as each feature module wanted to explicitly import one of the two modules I had created.

So, what actually was the solution?

I came across this series of blog posts by Itai Hanski particularly post 3, which spoke about a library with flavors. Then post 4 explained about optimizing these flavors. It would turn out, this would be the solution. However, it wasn’t there in plain sight.

My app already had 3 flavors with their own dimension.

So this block was in the build.gradle for app, instantapp, base, featureA and featureB . So now we needed to create a library. You do this by adding a new module and selecting Android Library. Here we create two new flavors and we don’t include these original three, as we’re wanting the library to be separate from the app. These new flavors will also have a new dimension.

First thing we want to do in the gradle of our library is differentiate what we want from each one. So we create two new directories with the same paths at src level. One called ‘pro’ and the other ‘free’. You then create the operations you want to create that are identical with method names and public variables etc.

All we want to do now is have certain libraries that the pro version will use. So to do this where you would usually use api “com.facebook.fresco:fresco:1.10.0” we have proApi “com.facebook.fresco:fresco:1.10.0” and this means Fresco will only be imported when we are running the pro flavour.

So that’s the library sorted… what’s next?

So now I had to work out how in all the modules I would determine if we are in the instant app or app. Which proved difficult. I read through gradles Authoring Multi-Project Builds to try find the solution and mainly it was the filtering of projects I was trying but to no avail.

Then it hit me.

We can share the library dimension with the rest of the modules in the app. So that’s what I did and now all the modules’ gradle files looked like this:

This, as you can imagine left me with a lot of build variants. Yes, it worked. I could now have a free version of the Instant App and a Pro version of the actual app. However, I could also build the other way around, which isn’t what I want. It’s a waste when we never plan on having a pro Instant App and a free version of the actual app. So, I began work on minimising the amount of build variants I had. With the aim of only being able to build a pro version of the full app and a free version of the Instant App.

The first iteration

My first attempt taking from Hanski’s series of posts was to remove the ‘free’ flavor from the apps build gradle and do the same with the ‘pro’ flavor in the Instant Apps. This would then mean, because they are not available we need to set some fallbacks in the libraries gradle. So because I knew if the module doesn’t have the ‘pro’ flavor fallback to ‘free’ and vice versa like so:

Although this worked I still wasn’t entirely happy with it. I felt that more could be done. I was right, it could.

The second and final iteration

The reason I still didn’t like this was because I had a flavor dimension in the app and instant apps build gradles that only needed one flavor. To which I then remembered this part from the fourth post by Hanski.

Snippet from https://proandroiddev.com/advanced-android-flavors-part-4-a-new-version-fc2ad80c01bb

Part 3 “the library has a dimension that the app doesn’t”. What I had been blind to see is that my app and instant app modules don’t have or need these dimensions, but my features needed both. So, in our app and instantapp build gradles we just have to add the lines missingDimensionStrategy ‘library’, ‘pro’ and missingDimensionStrategy ‘library’, ‘free’ to the defaultConfig section respectively.

The Final Outcome

So now we have an app that for its three country flavors when you run it it will only run the pro version of our library. Then with our Instant App when you run that it will only run the free version of the library.

Before you leave…

Don’t forget to clear the instant app from your emulator or device when then going to run the full app. Otherwise, you will get an error like this: