Developing and Debugging on Android with DevFun

Fun with Developer Functions

DevFun is an annotation based library aimed at Android developers to facilitate the separation, isolation, and invocation of developer and debug related code. It does this via annotation processing and Java’s service loader mechanism.

The main DevFun developer interface The main DevFun developer interface

Some of the things DevFun can do:

  • Invoke functions in your app at any time from anywhere
  • Invoke functions with values entered at run-time
  • Provide a handy debug interface by only adding a dependency

For those of you in the Angular world by sure to check out our sister library DevMod.

This article gives an overview of some of the traps and pitfalls found with common debugging techniques, and how DevFun helps you avoid them. This is followed by discussing how DevFun leverages code generation and service loaders to isolate itself from your business logic, leaving you with clean and “untainted” code.

The animation below shows DevFun in action. Other than adding the dependencies, the annotations shown is all that is used to generate the demonstrated behaviour.

All this with just some dependencies and annotations! DevFun is design to have a minimal footprint on your non-debug code, typically requiring nothing more than a function annotation. The floating cog and debug menu are provided by the devfun-menu module

Debug Code — Traps and Pitfalls

Before my leap into Java/Kotlin and Android, I came from the world of hobbyist C++, C#, and Visual Studio — I lived in a naive world where everything lived in one place, far too many things were public/static, and testing was… What I was “already doing” while I was writing it…? And I think it’s safe to assume that the beginnings of many developers were somewhat similar. Let’s have a look at some of these naive approaches to debugging, and their downsides.

Inline Test Code

So you’ve added some new feature, but it’s misbehaving — you should be unit testing but you don’t know how to do that yet or you are already pushing some deadline. So instead you just temporarily force the value to something, run, debug, and step through the code.

It’s just for a quick bug fix…

But of course you forgot to remove your debug code, so the next day you’re going insane trying to figure out why your @mention isn’t working anymore. Or worse yet, you’d accidentally committed it to the humiliation of your collaborators.

Fix parsing of hash tags Alex Waters 2018-10-23 02:59 AM
Remove debug code Mike Collaborator 2018-10-23 09:01 AM

Your shameful late-night mistake

The DEBUG Flag

The life of a developer can be surmised as; write code, run, test, repeat until working. However when your current task sits behind a login screen, we very quickly become sick of entering the test credentials. Thankfully we are a little more seasoned now, and the process of adding debug code is a little more elaborate. We now leverage the power of the debug flag!

A little nicer

Except now every time you want to log in as someone else you need to change the code. You also probably can’t commit this when working with others as they’ll potentially have their own test credentials. And oh dear — some smart-ass decided to decompile your app and now has access to your (hopefully not admin) test account.

Dependency Injection

With more experience comes better methodologies, techniques, and tooling, with the big ones typically being dependency injection and debug source trees. An old favourite was Guice, where adding debug-only overrides was relatively trivial. But these days it’s all compile-time code-gen with Dagger, which unfortunately does not lend itself to just quickly and easily overriding something — a simplicity lost for compile-time safety and run-time efficiency.

The power of dependency injection!

This is a little bit better — at least now you haven’t got your test credentials in your release builds, and issues with sharing and committing might not be a problem now since you can just not commit that one changed file (and/or untrack it or whatever).

However this approach is not without its issues and frustrations:

  • You are still polluting your business logic and adding unnecessary complexity to your login flow — simply for development and debugging purposes.
  • It requires that you create build-type specific implementations — which can be a real nightmare if you have more than just the standard debug/release variants.
  • If you need to change its structure you’ll need to adjust it for all your build types potentially breaking collaborator builds in the process (since you’re changing an untracked file or what have you).
  • It requires you to be constantly aware of the login default’s state — adjusting and rebuilding as you switch features, or when you need to test another user, etc.

Enter: DevFun

DevFun was created to make development and debugging simpler; to remove the need for those “one time” debug code snippets, to provide future developers (frequently yourself) access to your handy debug functions, and to just simply make life easier— all the while keeping your production code clean and explicit.

And the best part is, it does this with only annotations — so it doesn’t matter if you forgot to remove your debug snippets later as your business logic is never touched!

Of note is that all function annotations are SOURCE retained — this means that your compiled code will not contain any references to them. Throw in some ProGuard and your unreferenced debug functions can be stripped out entirely.

Fun with Annotation Processing

For those unfamiliar with annotation processing — it is a step in the compile process that allows code to be generated during the compile phase.

DevFun leverages annotation processing by “tagging” functions of interest. A reference is generated at compile time that describes where the function is, how to invoke it, and records any other information as described by the annotation.

Our previous examples using DevFun:

No DEBUG flags or additional DI framework needed — just SOURCE-retained annotations

Which DevFun turns into: Fun with debug functions Fun with debug functions

A couple of things to note:

  • The login options are only available on the login screen. DevFun tries its best to be “context aware” — in that fragment and activity related items should only be available when they can be used.
  • The handleWord function takes a String as a parameter. When DevFun can’t inject a type (more on that later), it prompts for manual entry for types it understands (which by default includes primitives, strings, and enums).
  • DevFun will put annotated items into a “category” based on the class name MyClass > “My Class” with the name based on the function’s name in the same manner. These can be renamed, grouped, or sorted using optional annotation fields or with other optional annotations and are completely arbitrary.

Leveraging the Service Loader

To further isolate your production sources from your debug code, DevFun leverages Java’s service loader mechanism — the generated code is an implementation of DevFunGenerated. This means your code never needs to know about it or tell DevFun where it is or how to load it.

For those unaware, a “service” is an implementation of some interface. The fully qualified name of the implementation is a line in a “services file” (plain-text) in the jar/aar META-INF/services/ (or use AutoService to create it for you). These implementations can then be found by the service loader at run-time:

Another benefit to using the service loader is that it becomes trivial to allow libraries to provide generic and frequently used utility functions — an example of this is the devfun-util-glide library, which provides functions to view and clear Glide’s image caches.

Utility functions can be provided by libraries Utility functions can be provided by libraries

The library itself is an object with @DeveloperFunction annotated functions. Arguments to functions are injected automatically at run-time (more on that next).

The service loader is also used for “modules” DevFunModule. These extend the functionality of DevFun and are loaded and initialised alongside it (which itself is initialised automatically on app start using a ContentProvider). They aren’t required for simple utility libraries like the above, but can be used for example, to spin up a local HTTP server providing a means of invoking functions from the browser (devfun-httpd and devfun-httpd-frontend) — these are quite experimental and mostly proofs of concept. In the future it might be fun to experiment with a kotlin-react app or something (not going to lie — it’s pretty gross what’s there at the moment).

Web front-end provided by a DevFun module allows the invocation of functions from your desktop Web front-end provided by a DevFun module allows the invocation of functions from your desktop

Function Invocation and Dependency Injection

Most modern development practices involve some sort of dependency injection. In this article we’re assuming you’re using Dagger 2.x (natively supported by DevFun) — though support for other frameworks wouldn’t be much of an problem.

One of the many issues surrounding traditional debug & developer code, is that it requires you to modify your production code — if only to reference a NOP implementation — to incorporate these developer-only enhancements. Though potentially trivial changes, it still adds unnecessary noise and is just “not fun” to look back over it in the future. Not to mention future developers that encounter it; “What’s this LoginDefaults — maybe that’s causing bug #123 by giving the wrong values… Oh, it’s just for developers… Well that was a waste of time.”

DevFun itself does not use nor require the use of Dagger. Instead it uses a simple InstanceProvider system (below). At the time this was because (among other reasons) the underlying code generated by Dagger would fail if the consumer used a different version (though these days it’s pretty stable).

Instance providers handle DevFun’s “dependency injection”

At compile-time DevFun records what it needs to know in order to invoke a function; upon what (the “receiver”), the function to be invoked, and its parameter types. When invoking, it loops through its providers checking the most recently added first. This system makes it trivial to support other DI frameworks — for example, the module devfun-inject-dagger2 adds an instance provider that traverses your Dagger graph.

The dynamic nature of the instance provider system means that as long as a type is provided by Dagger or whatever, then you can effectively create and annotate any function without giving it much thought.

If you happen to be using Dagger 1.x, Guice, or some other DI framework, feel free to submit an issue or submit a PR with support.

Source-sets, Source-retention, and Modularity

Though you could leave your “main” source tree pristine and only have debug code in your debug tree, it’s quite common for objects to have some function that you just wish you could call; something to reset its state, or something to log some data, or something that triggers a bunch of logic that’s just a bit of a PITA to trigger by normal means.

Unfortunately there’s no way to avoid some debug-ish code in your main sources to reference these elusive functions — but we can minimise the impact. The devfun-annotations module contains only annotations, interface definitions, and some inline utility functions (i.e. minimal method count). Additionally all function annotations are SOURCE retained, so their contents and presence are completely absent from your byte code.

Furthermore, since DevFun’s functionality is provided via service-loader modules, the rest of it can exist solely in your debug class-path, and does not require you to reference it at all from your any of your sources. Some of the modules provided by DevFun:

  • A “menu” module devfun-menu provides a floating debug-cog and hotkeys (` or down-down-up-down on your volume buttons) that shows a menu of functions for invocation.
  • A DI module devfun-inject-dagger2 for Dagger 2.x allows DevFun to use your components for instance resolution and injection.
  • Modules are available for LeakCanary devfun-util-leakcanary and Glide devfun-util-glide that provide some utility functions (show leak activity, show and clear Glide image caches etc).
  • Experimental modules provide a built-in web server devfun-httpd (NanoHTTPD) and a front-end devfun-httpd-frontend (bootstrap) allowing for remote invocation of functions from a browser.
  • And an experimental module devfun-stetho allows calling functions from Chrome’s JS console via Stetho.

Disclaimer: the experimental modules are mostly proofs of concept and are not as stable or feature complete as the menu. Also, their code is… Of questionable quality. But they do generally work and are kind of fun if only for the novelty.

Designed by developers, for developers

DevFun was design with developing and developers in mind, it’s intended to “just work” with minimal effort (auto-loading & initialisation and built-in Dagger 2.x DI support etc). The majority of its features and functionality are intended to be customised with most options leveraging Kotlin’s default values. Furthermore, a majority of its API uses interfaces, which in combination with its instance provider system, allows for easily providing your own implementations over the standard ones, and even intercepting core functionality on the fly.

So if at any point you find something annoying, or you want the means to change or configure something, feel free to create an issue or submit a PR — more than likely it’ll be accepted. DevFun is meant for development, not as some critical component to your app’s functionality. i.e. if it breaks or crashes it really doesn’t matter because it just for us developers! (though obviously efforts are made to avoid that and to provide decent backwards compatibility — but you get the point).

Thanks for reading!

Some quick links and extra information:

  • For installation and usage see the README over at the GitHub repository
  • For a higher-level overview see the “wiki” (should rename that)
  • For extensive documentation see the Dokka generated GitHub pages
  • For some awesome app development needs see NextFaze (shameless plug)
  • And again for those in the Angular world go check out DevMod