Photo by Hack Capital on Unsplash

Last week I was working on a feature that syncs data between phone and backend. That sync mechanism is triggered under certain conditions and we needed a way to bypass those conditions and force the sync when enabling some preference in debug mode. Leaving aside the details, the resulting code was something like the following:

Pretty simple. I relied on the BuildConfig.DEBUG constant generated by Android Studio.

The problem

Everything seemed fine to me until I received a comment in the pull request:

I would not want to ship Debug functionality in our Production APK/Bundle code.

I agree that we shouldn’t ship debug functionality in the published artifact, and there are many reasons for that:

⛔ It’s definitely not a good practice.

🔒 It can introduce security issues: what if someone decompiles the generated artifact and found that you are doing some tricks to avoid a validation? What if someone manages to replicate that behavior? Edit: If you are using Proguard/R8 this isn’t a real problem for you, because the compiler optimizations will hide the real condition. Anyway: remember that this tools aren’t enabled by default and you need to take are about it.

If you are using Proguard/R8 this a real problem for you, because the compiler optimizations will hide the real condition. Anyway: remember that this tools aren’t enabled by default and you need to take are about it. 🐞 And what if there is a bug in the generation of the BuildConfig class? Maybe you didn’t even consider it, but let me tell you that some years ago there was an unresolved bug that caused BuildConfig.DEBUG was always true, even in the generated .apk file.

There are better alternatives…

… but the correct one depends on what you are trying to do.

I published a tweet with a poll and thanks to the responses I learned a lot about the way developers are using this constant, and when and why we are doing things in a, probably, wrong way.

Including debug functionality

This is my own scenario, but I received some responses from developers facing a similar one, for example adding Interceptors to the HTTP requests in the app in order to log requests and responses. We should avoid this and my recommendation is to use the power of flavors in order to separate debugging logic from the production logic, including the same classes in the release and the debug flavor. In my case, the result is included in the following snippets:

The original syncData function should be as simple as:

✨ Magic. No conditionals. Now we are delegating the condition to a function included in an object called SyncCoordinator , let’s see how to implement it:

Note that these objects have the same name and they are, obviously, in two different files. The file path is the key: the first one is under the src/release directory and the second one is in src/debug . Build system will include just one of these implementations based on the active build variant. Be careful: it’s not possible to have a main class and try to override it in flavors. You can find more information about it in this StackOverflow question.

You will note also that this approach allow us to write separate unit test classes for each flavor, and maintain debug tests isolated from the release tests, which is so much cleaner. These classes should be included in a path like the following: MyProject/src/testDebug/java/com/facundomr/example/util/SyncCoordinatorTest.kt and MyProject/src/testRelease/java/com/facundomr/example/util/SyncCoordinatorTest.kt .

Now, let’s move forward and evaluate other situations:

Changing a value/constant depending on the flavor

If you just want to get a different value in a constant, then you don’t need to write any extra code. Defining the same buildConfigField in every flavor should be enough. Follow the official documentation for more information about this capability.

Including libraries needed only when in debug mode

If you are doing this, then you are not just including debug functionality: you are probably increasing significantly the size and method count of the released artifact. In order to avoid this you should read this article about No-op versions for dev tools.

Avoiding code execution in production

The difference can be subtle: it’s not the same to include debug features as to avoid some code execution in production: because maybe you should’t be scared of security issues if someone discovers that you are, for example, disabling logs based on a condition as simple as relying on BuildConfig.DEBUG . But there are better and more elegant way to do this: use Proguard to remove that lines when exporting your artifact. I recommend this article from Craig Russell about Stripping log statements using Proguard.

Warning: very careful when you write modules in your app

If you are writing an app and the code is structured in modules (for example: app and ui , and you are depending on the constant in the ui module to do some debug logic, then be careful: BuildConfig.DEBUG in app is not the same as BuildConfig.DEBUG in the ui module: you can be executing debug code even in the released artifact. There is a safer way to check globally if a build is in debug mode or not: it’s called ApplicationInfo.FLAG_DEBUGGABLE . You can read this article to learn about it.

Conclusion: Should we always avoid using BuildConfig.DEBUG ?

Definitely not. I’m not trying to say that. There can be situations in which you shouldn’t be concerned about it and it’s perfectly fine to rely on the constant, for example disabling Crashlytics on debug. When you use an if statement for this:

You still need to include the library in production.

You are not shipping a debug feature.

It’s not dangerous if someone discovers that you are disabling crash reports when debugging the app.

My only conclusion is: we should consider all these options before writing code that depends on the BuildConfig.DEBUG constant. Maybe there is a better, safer and more elegant way to achieve the same goal, and it’s always nice to learn new things.

Happy coding! 😊