Image credit: Martijn Baudoin on Unsplash
Background
Hello. I’m Rob and as of January 2020, I work on the Android version of BBC Sport. Our codebase started its life in 2013 and it’s fair to say we have amassed some tech debt over that time — we are now trying to sort out our fundamentals.
Obligatory disclaimer: views expressed are my own, not the BBC’s or the team’s.
One of the biggest pain points before I joined this project was simply described as ‘Koin’. This is a dependency injection (DI) framework along the lines of Dagger. Actually, Koin isn’t exactly the problem, but we’ll come back to that. It is inevitably experienced as the problem, because it’s the name on the police report when anything DI-related manifests in failure.
For those of you not too familiar with DI frameworks, they’re a mechanism whereby somewhere centrally you can say, ‘I’m provisioning this object’, and somewhere else — untroubled by any obvious connection between the two — you can say, ‘summon my object’. Sometimes this is driven by annotations, like Dagger or Spring, and sometimes it’s more driven by functional definitions, as Koin is.
Now, Koin works in this respect, and it would be unfair to say otherwise — we are not suffering from implementation bugs or faults in its defined behaviours. I will say it does have a big deficiency compared to Dagger, in that it has no compile safety so you can completely forget to do the provisioning part and it will only blow up at runtime. However, this is not really the problem.
The far bigger problem — and Koin is not alone in this — is that it allows you to gloss over all the stuff you should have thought about, like the small matter of software architecture.
Quick Primer: How Koin Works In Practice
Imagine we have this classic relationship.
To create ThingThatTakesDependency, we have to give it an instance of DependencyThing. But what if we don’t have one to hand? What if it’s created somewhere else distant from this usage and we want to grab it without all the legwork of passing it in? Well, we can use Koin.
In the application, we configure a Koin module that provides a single instance of DependencyThing. In ThingThatTakesDependency, we summon it, and Koin provides it.
This saves us some boring work, and if it comes with the same kind of testing support, it probably makes mocking in unit tests easier too, at least in the short term.
Slight problem here that I mentioned earlier - if we bin the entire provisioning element, this still compiles:
For that matter, of course, so does this:
Oh well.
Like I said earlier, this isn’t the biggest problem.
The Genie
Let’s get away from code and the tangible for a moment.
Imagine that you have to rewire your home but you don’t know anything about electrics.
You call an electrician and he has a look and he sucks through his teeth and says it’s going to cost you £5,000, and something about having to replaster and redecorate afterwards.
The next day, while you’re thinking about whether you’re too old to learn a new trade, or maybe which of your internal organs you could list on eBay, you discover a dusty old lamp. You rub the lamp and, in a flash of particularly tired narrative, a genie emerges.
This sort of thing
The genie says to you, ‘Hey there, I’m an unexpectedly specific NICEIC-certified genie and it looks like you’re thinking about rewiring your house’. You feebly reply in the affirmative. ‘I’m able to grant you precisely one wish, as long as it’s rewiring your house’, says the genie. Great, you say, and in a big blue flash and a puff of burnt plastic magic smoke, the deed is done — everything you wanted is complete, and the genie is gone.
A couple of years later and you find yourself thinking it would be really nice if you could have some of those ‘smart home’ light switches so that botnets can turn your lights on and off for you at random. You still don’t know anything about electrics but you reckon you can probably change a switch.
You unscrew the old one and… there’s no wires attached to it. It did work, but there’s no wires. I mean, cool, kind of, but this isn’t right. You have a look at another one, and a few sockets, and they’re the same. No wiring anywhere. And now because you’ve meddled with them, they don’t work any more either. The more you look, you realise the more trouble you’re in, because it’s literally impossible to change anything. And rubbing that old lamp does nothing now.
Maybe you should have hired some old fashioned type who, you know, went about their trade without relying on the supernatural?
What’s Your Stupid Genie Bit Got To Do With Software? Software’s Not Wiring
Software is mostly wiring, or plumbing if that’s your preferred visualisation. How we get information from one place to another is really important, even if sometimes it’s really tedious too. Like an electrician, it’s fundamentally the job.
When we think about internal architecture, how and when we create something and how we get it to its users is the big deal. If we surrender fundamental parts of that, we stop doing a fundamental part of our jobs.
Under traditional dependency injection, where we pass objects into constructors ourselves, you can’t pass around something you haven’t got — an instance either exists or it doesn’t.
Certainly you can complicate this picture with things like:
- lazy instantiation
- language features like Kotlin’s ‘lateinit’
- lambda providers
- complex state, e.g. requiring an ‘initialise’ call before a constructed object can be legitimately used
…but this is often implementation detail, not a wholesale pattern. If you can create a trap for yourself where it really looks like you can use something, when actually you can’t, that’s probably architecture, or at the very least corrosive to whatever ideas about architecture you do have.
But Spring
Some software frameworks and indeed whole design methodologies use DI frameworks. Enterprise stuff like Spring is a classic example (caveat: I am not, thanks to my charmed life, a Java EE dev) where it’s entirely normal to have everything injected via some sort of annotation incantation. Does this make it OK? And does it make it right for Android?
In some situations, you don’t have a coherent core context — what are the central objects of an enterprise web app? In some situations you don’t have a really clearly defined lifecycle — do you fully own the entry point(s) to your application, and is there a clear expectation of lifetime, or are these things largely invoked by or delegated to the enclosing framework? In other words, if you yourself are summoned by magic, perhaps it’s sensible to use more magic to go about your business rather than trying to go against the flow of the environment to add overarching constancy.
And On Android…
In Android, for better or worse, we do have a core context and we do have a lifecycle. We have an Application object, and it’s accessible from anything that has a Context — views, activities, fragments (usually), services. We have a coherent entry point both to the app and to individual components.
In other words, we have something central and omnipresent we could use as a building block. And, because we have a clear lifecycle, we often design around that — one example of this is ‘bootstrap’, a process used in almost every BBC app, whereby on app start we fetch a remote configuration file, use that to configure our app components and only then show the UI.
The basic elements of the ‘bootstrap’ process
Bootstrap is really important to the business because it lets us remotely reconfigure the app without redeploying it. Bootstrap is also really important to the architecture because it introduces the possibility of non-linear instantiation. Whilst the app is starting, as hooked into Application.onCreate(), we may go off and do an async web fetch, and only when that is done, we’ll create our core services. However in the meantime onCreate() completes and our app components — like a broadcast receiver invocation or a UI resume that recreates the fragments outside of the normal entry sequence — are potentially then free to try and access dependencies that aren’t ready yet.
When we don’t structurally protect the startup order, we run into this and it’s normally a crash
When we rely heavily on DI frameworks, this is a big problem, because there is typically no protection against the uninitialised state. If you’re forced to provision everything up front (Dagger) then you find ways to bodge delayed provisioning. If you’re not forced to do so (Koin) then nothing stops you accessing things that aren’t ready yet. This is where most of our historical crashes have come from.
What Should I Do Instead?
You are pretty well served by the rule, ‘if you can’t get it, you can’t use it’, and in turn this rule is pretty well served by the old low-tech approach: constructor injection and passing objects around directly.
The first way this manifests is really simple — the linear instantiation process, basically the big list of services and core dependencies that you create. You probably can’t create Car without first creating Engine. If your implementation of Engine requires the circular dependency of Car then in the first instance this doesn’t have a compilable solution, and most of the time, nor should it. Stop and sort out your architecture.
When it comes to the threading complexities of something like bootstrap, you can put the vast majority of your major components behind a ‘wait for bootstrap’ process that returns a container with them in, and make that the only way to access those things.
On Android there are a small number of platform invocations — like widgets; remember widgets? From 2008? — where you need to respond synchronously, but more often you are able to do the work on a new thread.