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
Some of the things DevFun can do:
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.
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
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.
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 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.
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:
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.
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
A couple of things to note:
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).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.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/com.my.service.InterfaceName
(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
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
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.
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:
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.devfun-inject-dagger2
for Dagger 2.x allows DevFun to use your
components for instance resolution and injection.devfun-util-leakcanary
and Glide
devfun-util-glide
that provide some utility functions (show leak activity,
show and clear Glide image caches etc).devfun-httpd
(NanoHTTPD) and a front-end
devfun-httpd-frontend
(bootstrap) allowing for
remote invocation of functions from a browser.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.
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: