Developing and Debugging on Android with DevFun
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.
For those of you in the Angular world by sure to check out our sister library DevMod.
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
- AND SO MUCH MORE
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-menu
moduleDebug 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.
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.
Your shameful late-night mistake:
Fix parsing of hash tags Alex Waters 2018-10-23 02:59 AM
Remove debug code Mike Collaborator 2018-10-23 09:01 AM
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!
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.
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:
DEBUG
flags or additional DI framework needed — just SOURCE-retained annotationsWhich DevFun turns into:
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 aString
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/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.
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).
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 (left). 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).
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 (`
ordown-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 Glidedevfun-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-enddevfun-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
Originally published at www.nextfaze.com.