Advanced Android Flavors Part 4 — A New Version

Itai Hanski
ProAndroidDev
Published in
4 min readJan 26, 2018

--

This is the fourth article in my ‘Advanced Android Flavors’ series.
The first one is here.
The second one is here.
The third one is here.
The fifth one is here.

Not too long ago, Gradle release version 3.0.0 of its plugin and with it came some cool but breaking changes. I’ll try to summarize the things we need to do according to our ongoing example.

First things first

Flavor dimensions are now required for every flavor. We already have dimensions for our flavors in our example (see part 2). If your app has a dimensionless flavor — you have to add a dimension to it or you’ll get a compilation error.

Why the new requirement? It allows Gradle to automatically match between our app and our library flavors. And you know what that means? It’s time to delete some code 😌.

No more manual library configuration

In part 3 we mapped our app flavors to the appropriate Common library flavors by hand. Let me remind you. It looked like this:

dependencies {
client1DevCompile project(
path: ':common',
configuration: 'app1DevRelease'
)
client1StagingCompile project(
path: ':common',
configuration: 'app1StagingRelease'
)
client1ProductionCompile project(
path: ':common',
configuration: 'app1ProductionRelease'
)
client2DevCompile project(
path: ':common',
configuration: 'app1DevRelease'
)
client2StagingCompile project(
path: ':common',
configuration: 'app1StagingRelease'
)
client2ProductionCompile project(
path: ':common',
configuration: 'app1ProductionRelease'
)
// other dependencies}

Well, now the mapping is done automatically according to the dimensions. All we need to do is add the library to our dependency:

dependencies {
implementation project(':common')
// other dependencies}

That’s it. Well almost 😅. We have a tiny bit of configuration still.
Gradle goes over each of our dimensions and tries to match it in our library, using following this logic:

  1. The app has a dimension that the library doesn’t: nothing to do 💪.
  2. The app and library share a dimension: Select the same flavor in the library as the one that’s selected in the app 👉👈.
  3. The library has a dimension that the app doesn’t: we need to chose a fallback / default 🤷️.

There are two edge cases we need to account for. The obvious one is case number 3. But there’s a hidden one in case 2: what if the app’s dimension contains a flavor that the library doesn’t?

Let’s handle the second case first. Our app and library share a dimension, but the library doesn’t have the app’s specific flavor. For that, I’ll add a new flavor to our app under server dimension:

android {

//...

flavorDimensions "client", "server"
productFlavors {
client1 {
dimension "client"
applicationIdSuffix ".client1"
}
client2 {
dimension "client"
applicationIdSuffix ".client2"
}
testing {
dimension "server"
}

dev {
dimension "server"
}
staging {
dimension "server"
}
production {
dimension "server"
}
}

Our Common library does share the server dimension, but it doesn’t have a testing flavor. Let’s default back to dev in that case. We need to explicitly declare our fallback like so:

android {

//...

flavorDimensions "client", "server"
productFlavors {
client1 {
dimension "client"
applicationIdSuffix ".client1"
}
client2 {
dimension "client"
applicationIdSuffix ".client2"
}
testing {
dimension "server"
matchingFallbacks = ['dev']
}
dev {
dimension "server"
}
staging {
dimension "server"
}
production {
dimension "server"
}
}

Notice that matchingFallbacks is an array and can take a list of options if need be.

Now let’s handle the case where the and library has a dimension that the app doesn’t have. It’s pretty much the same idea: explicitly instruct Gradle which flavor to choose. Here’s a reminder of the Common library’s Gradle config:

android {

//...

flavorDimensions "app", "server"
productFlavors {
app1 {
dimension "app"
}
app2 {
dimension "app"
}
app3 {
dimension "app"
}
dev {
dimension "server"
}
staging {
dimension "server"
}
production {
dimension "server"
}
}

Notice we have the app dimension which doesn’t exist in our app. We want to choose the right configuration for our app which, in our case, is app1. To achieve that we’ll add the following:

android {

//...
defaultConfig{
missingDimensionStrategy 'app', 'app1'
}
flavorDimensions "client", "server"
productFlavors {
client1 {
dimension "client"
applicationIdSuffix ".client1"
}
client2 {
dimension "client"
applicationIdSuffix ".client2"
}
testing {
dimension "server"
matchingFallbacks = ['dev']
}
dev {
dimension "server"
}
staging {
dimension "server"
}
production {
dimension "server"
}
}

The first string given to the missingDimensionStrategy field is the dimension name. The following comma separated strings are the fallback values to be used in order.

Now, our automatic mapping works. We added 3 lines, and removed 24 🎉.

Goodbye Compile. Hello Implementation & Api.

The dependency configuration names have also changed in this version . The most relevant change for us is the splitting of ‘compile’ into the ‘implementation’ and ‘api’. This split helps us expose our library’s dependencies, or keep them private.

That means we can choose to have our library’s dependencies accessible to the apps using our library. Almost like we’re exposing an (wait for it…) API. On the other hand, we can keep our dependencies internal. Meaning, we only need them for our (wait for it…) implementation.

Are we done? Oh hell no.

This series was originally planned as a trilogy. But now I think we can take it even further. Another trilogy? Perhaps.

--

--