Adding a debug overlay to an Android app

A window into and onto the current state of the app

Christian Schmitz
3 min readDec 31, 2018

When developing an application, the two build-types debug and release are defined by default. Additional to these two and possibly some other custom ones, different flavors of the app can be defined with multiple dimensions. To differentiate between these, the version or application name should be annotated with a suffix or replaced completely. Furthermore, the app could change its state dynamically on runtime, like switching between staging and production environments.

In most of these cases though, all these app configurations appear alike when being run on the device. Some relevant information might be accessible within the settings or on info screens. While developing or testing the app, this information being displayed as an overlay to the layout of every single screen, and therefore even more accessible, could increase the efficiency in day-to-day work tasks.

Let’s call this a debug overlay.

Instead of adding the overlay with every Activity or Fragment implementation, this could be delegated to some ActivityLifecycleCallbacks implementation. As an additional advantage also any Activity not maintained within the project will display this overlay then.

val callbacks = object : ActivityLifecycleCallbacks {
...
override fun onActivityStarted(activity: Activity) =
activity.registerOverlay()
override fun onActivityDestroyed(activity: Activity) =
activity.unregisterOverlay()
}

Since the overlay is not actually an overlay, but rather a second content view, the Activity needs to have setContentView called already. In most cases this could be assumed after onCreate and the onActivityCreated callback might run too early. Therefore onActivityStarted is the callback on which the callback is actually registered. Additionally this skips Activities which are finished before even being visible to the user.

val activities: MutableSet<Activity> =
newSetFromMap(WeakHashMap<Activity, Boolean>())
fun Activity.registerOverlay() {
if (activities.add(this))
updateOverlay()
}
fun Activity.unregisterOverlay() {
if (activities.remove(this))
removeOverlay()
}

To handle additional state changes dynamically, the Activities can be added to a WeakHashSet to be accessible anytime. It also helps to ensure to update or remove the overlay only once for every single Activity.

fun Activity.removeOverlay() {
window.decorView.debug_overlay?.removeFromParent()
}
fun Activity.updateOverlay() {
val view = window.decorView.debug_overlay
?: layoutInflater
.inflate(R.layout.debug_overlay, null)
.debug_overlay
.removeFromParent()
.also { addContentView(it, params) }
view.setupDebugOverlay()
}

With a little help of a small utility removeFromParent and the synthetic properties of Kotlin, the actual overlay handling is as easy as inflating the layout to addContentView and somehow setupDebugOverlay the view with version information, the server environment or any other valuable data for developers.

Implementing the overlay itself with a non-focusable view, this approach will not interfere with the UX of the app and by applying a suitable coloring, the UI will be mostly undisturbed as well.

Now developers and testers are aware of the current configuration applied to the app and the integrity of the design is preserved as well. The impact on the whole code-base is as little as implementing a single class with a small layout and registering it with the Application.

--

--