Loading...
Home
  • Tech Blogs
  • Videos
  • Conferences
    • Droidcon News
    • Upcoming Conferences
    • Become a Partner
    • Past Events
    • Keep Me Informed
    • Diversity Scholarships
  • Community
    • droidcon Team
    • How to Hold a Droidcon
  • Android Careers
Sign In

Global CSS

 

Getting Rid of Koin, or: How I Never Learnt To Stop Worrying And Love The Dependency Injection Framework

 

 
Rob Pridham
September 14, 2020
Tweet
Share
 
 

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.

 

Image for post

 

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.

 

Image for post

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.

 

Image for post

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.

 

Image for post

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.

 

Image for post

Example process — and if the app had already bootstrapped, the callback would be invoked immediately

 

The ‘wait for bootstrap’ function can be a central part of the application — again, whenever you have a Context, which is most of the time, you can get the Application object, and you can do application-aware stuff with that, like waiting for bootstrap.

If you get this right, then you inherently start dividing your objects into two families: things you create centrally and which stick around, and things that serve a specific purpose (like to back a UI) which typically consume objects from the first set.

Our viewmodel factory depends on our core services, which are injected into viewmodels it creates. The UI can only get hold of a viewmodel via this one route that involves the factory. The factory is only available after bootstrap. Therefore getting a viewmodel requires something that waits for bootstrap. The protection cascades out to all UIs without them having to think about it beyond using this defined mechanism.

No shortcuts means you must comply with the architecture, and architecturally-compliant things are (at least if you got it right) smarter, more coherent, more maintainable and plenty more besides. You deal with this whole problem once, centrally, rather than in every one of your components.

 

It’s Too Late For Me, I Made This Mistake Already

So you’re already using Koin or similar all over the place, and only just experiencing the pain?

Well, in January 2020, we had this:

Provisioning

  • 38 classes provided by Koin’sfactory
  • 38 classes provided by Koin’ssingle
  • Total: 76 classes provisioned under Koin

Access

  • 88 accesses via Koin’s inject across ~55 classes
  • 48 accesses via Koin’s get across ~33 classes
  • Total: 136 instances of accesses through Koin

The extent of its use looked insurmountable and historical attempts to address it had run pretty quickly into ‘this is too much to handle’.

 

Enabling Migration

First, we did a bunch of things to get the conditions right for a migration out of Koin. We got the core lifecycle stuff in place — got the ‘bootstrap manager’ and the whole initialisation sequence into fighting shape, sorted out the viewmodel factory, and created an empty ‘SportService’ marker interface to represent a responsibility: services that things like viewmodels would depend on.

That’s really helpful if everything is already based on centrally-instantiated responsibilities like viewmodels in some shape or form, and that’s where we want to get to, but unsurprisingly we don’t have that luxury yet, which is probably why Koin was introduced in the first place. What about things for which there is no direct dependency flow, e.g. things that are too deeply stuck behind horrible singletons?

Because we recognised this, we also created something we call a ServiceRegistry — basically a glorified hashmap of service instances. The service initialisation process can put stuff in here, and provided you can get hold of the registry (by getting hold of the Application object), you can pull stuff out of it again at the point of use.

Why a map rather than a stronger structure? We use project modularisation and not all accessors necessarily know about all service types. There are things we could do to firm this up but it’s a compromise. We made this accessible via a ‘wait for bootstrap’ mechanism but if you’re confident that it’s safe to do so because of the nature of where it’s used, it can also be directly interrogated for its services.

Do these characteristics sound familiar? Let’s be clear here: this registry is fundamentally the same mechanism as Koin. It doesn’t protect against initialisation order, and in fact, just like Koin, if we forget to provision things, it compiles. Same problems.

This leaves us roughly here:

 

Image for post

 

If it’s just as risky as before in some ways, why bother? Because we own it.

And because we own it:

  • it’s one mechanism, and it’s ours, and we can trace the set of usages far better than in Koin. We give ourselves one method to put things in, one to take things out, not whatever set of operations the framework offers.
  • we at least conceptually limit the type of things — ‘services’ — that go into it, rather than it being a mechanism for any old random stuff. Just this alone makes us think more carefully about what we’re provisioning and why, rather than ‘because we can’.
  • we can deprecate its use, and we have! It’s got health warnings placed around it from the very beginning, and our engineers are strongly encouraged at a code-level to find other solutions
  • we can make incremental improvements to how it works

It’s not the answer but it’s an interim step. When we migrate things out of Koin, we can aim for direct dependency passing, and if that’s too big an ask for that scenario at this point, we can fall back to using this registry mechanism as a way to proceed.

 

Planning And Organising

In the very beginning, we looked for all the stuff we provisioned under Koin, and we looked for all the things that featured at least one access, and we made a very big, very boring Trello board out of it.

 

Image for post

Early in our migration phase

 

We then figured out the set of things that were the ‘leaf nodes’ in the dependency tree — things that were provisioned under Koin but didn’t themselves depend on accessing more things through Koin. These were inherently the most movable and so we dealt with these first. This in turn could unblock working on things that used them.

The lax nature of Koin is actually a strength when you are trying to rid yourself of it. It’s a simple declaration-and-usage pair relationship without much more baggage around it, so once you’ve figured out the above, it’s easy to replace it on the same piece by piece basis.

It wasn’t always a one-to-one mapping but we tended to create Jira tickets for migrating each provisioned thing, in a roughly just-in-time fashion based on the Trello board. With a bit of variation in each case, they tended to follow a basic recipe:

 

Image for post

Example Jira description

 

This can be a little unambitious at times — it solves a very specific issue and may mean reorganising what we can see to be junk dependencies without tackling the junk element of them right now. However it’s a difficult overall task and it’s important to stay focused. It also empowered junior engineers to get stuck into this work in a tightly scoped way, and once they got experienced with some implementation, they could also get into the ticket authoring part too — the ‘figuring out how’ bit.

And so, about 46 Jira tickets later, we had got rid of Koin.

 

What’s The Outcome?

We talked about the service registry and how this is an interim step, but we also talked about how we should be able to just plain get rid of a lot of this ‘disconnected magic’ too. So where have we ended up?

Reminder — in January:

  • 76 classes provisioned in Koin
  • 136 accesses using Koin across ~85 classes

Now, at the end of August, having done this migration work amongst many other things:

  • Koin is banished
  • We made 45 things a SportService
  • 20 of them (-74%) are provisioned into the service registry, and only 17 of these have unprotected accessors. The remainder not in the registry are passed and used directly.
  • 50 unprotected accesses (-63%) to the service registry across 35 classes (-59%)

That big reduction in depth and breadth is primarily because we found various different ways to pass dependencies directly to their users, often within strongly defined and consistent patterns — exactly what we wanted. This also tells you something about how Koin was being used in ways that could have been relatively easily avoided. When you create a mechanism, people will use it.

We also rationalised some code, sometimes combining multiple dependencies into one more coherent thing, or sometimes passing finer or coarser grained dependencies to their users. As mentioned earlier, it’s easier to make these decisions once the sum total of our dependencies are in much clearer view.

The remainder in the registry is still significant, and these mostly represent situations where the accessors need architectural improvement. However their being in the registry is itself a step forward for us, for reasons discussed above.

We have further to go on this journey, and ultimately we want to completely eliminate the service registry too. We will do this through strategies such as applying an MVVM-like pattern to many of our legacy UIs, meaning that dependencies are into viewmodels which come from a central route, and therefore a lot of these registry usages will evaporate in the process of further refactoring. It’s easy to see how to get rid of about a third of our registry accesses in single stage refactors.

 

Conclusions

  • Own your architecture and the mechanics of provisioning things, because even if it’s boring, this is your job
  • Although NIH can be a bad habit, be extremely cautious about outsourcing it to a framework — of any kind, no matter how much easier it seems to make your life or how shiny and alluring it appears
  • Koin alone is not the problem but it (and things like it) very much enables you to create the problem
  • Better not to make this mistake in the first place, but if you have, we’ve demonstrated that you can claw your way back out of it again
  • An incremental, tightly focused migration with interim measures is really helpful, even if it feels admin-heavy and overly restrained at times
  • What we have achieved is a success but not the destination — we’ve removed our biggest obstacle to good architecture, not actually brought about universally good architecture yet

 

 

Tags: Android, Dependency Injection, Koin, AndroidDev, Android App Development

 

View original article at: 


 

Originally published: September 04, 2020

Android News
Our Engineering Roadmap
Our Engineering Roadmap

By Mark Ng

We just completed our engineering road map for our Android apps at Australia Post. Each year we get together and try to decide on what we are going to do from an engineering perspective for the next 12 months. Each team gets to decide on what should be done now, what they want to complete by the end of the year and whats on the horizon for next year.

By ProAndroidDev -
Android News
Android Activity Lifecycle considered harmful
Android Activity Lifecycle considered harmful

By Eric Silverberg

The Android activity lifecycle is one of the first architectural concepts learned by a new Android developer. Ask any Android dev and they’ll likely all have seen this handy diagram from Google: 

By ProAndroidDev -
Android News
Our Safe Approach to Android Jetpack Navigation in a Multi-Modular App
Our Safe Approach to Android Jetpack Navigation in a Multi-Modular App

By Alejandro Weichandt

It has been a year since we started working on the Android version of the mobile app at Sync. During that year, we faced more than once that moment when we had to choose which path to follow on an Architectural decision. This story is about Navigation.

By ProAndroidDev -
Android News
Custom KotlinX Serializers
Custom KotlinX Serializers

By Jobin Lawrance

Let’s say we have a third-party class that we are using as a type in one of our data class that we want to be serialized, then we have to write a custom serializable for @Serializable to work.

 

By ProAndroidDev -
droidcon News

Tech Showcases,

Developer Resources &

Partners

/portal/rest/jcr/repository/collaboration/Groups/spaces/droidcon_hq/Documents/public/home-details/EmployerBrandingHeader
EmployerBrandingHeader
https://jobs.droidcon.com/
/portal/rest/jcr/repository/collaboration/Groups/spaces/droidcon_hq/Documents/public/employerbranding/jobs-droidcon/jobs.droidcon.com
jobs.droidcon.com

Latest Android Jobs

http://www.kotlinweekly.net/
/portal/rest/jcr/repository/collaboration/Groups/spaces/droidcon_hq/Documents/public/employerbranding/kotlin-weekly/Kotlin Weekly
Kotlin Weekly

Your weekly dose of Kotlin

https://proandroiddev.com/
/portal/rest/jcr/repository/collaboration/Groups/spaces/droidcon_hq/Documents/public/employerbranding/pad/ProAndroidDev
ProAndroidDev

Android Tech Blogs, Case Studies and Step-by-Step Coding

/detail?content-id=/repository/collaboration/Groups/spaces/droidcon_hq/Documents/public/employerbranding/Zalando/Zalando
/portal/rest/jcr/repository/collaboration/Groups/spaces/droidcon_hq/Documents/public/employerbranding/Zalando/Zalando
Zalando

Meet one of Berlin's top employers

/detail?content-id=/repository/collaboration/Groups/spaces/droidcon_hq/Documents/public/employerbranding/Academy for App Success/Academy for App Success
/portal/rest/jcr/repository/collaboration/Groups/spaces/droidcon_hq/Documents/public/employerbranding/Academy for App Success/Academy for App Success
Academy for App Success

Google Play resources tailored for the global droidcon community

Follow us

Team droidcon

Get in touch with us

Write us an Email

 

 

Quicklinks

> Code of Conduct

> Terms and Conditions

> How to hold a conference

> FAQs

> Imprint

Droidcon is a registered trademark of Mobile Seasons GmbH Copyright © 2020. All rights reserved.

powered by Breakpoint One