- Part 1
- Part 2
Do you like clean architecture but are bored writing tens of interfaces and their impls? Instantiating tens of objects to just call their methods? Then you’ve come to the right place. Let’s get started!
A quick look at which libraries I’ll be using for this example. Nothing fancy, except maybe using Koin instead of Dagger for the sake of less boilerplate code.
Next, let’s assume we have to fetch the following color JSON from a remote API (you can find it here):
Modeling this as a Kotlin data class:
I strongly recommend making all your data classes immutable and keep them as clean as possible, which implies no methods at all except maybe for mapping, although I personally prefer to use extension functions for mapping data from one type to another.
We define our Retrofit interface. Yes, I know I said we will get rid of interfaces but here we’re limited by this specific library API :)
Injecting functions instead of interfaces
Now comes the interesting part. Instead of creating an interface and an implementation for the data source, we will use top-level functions, inject any needed dependencies there, and inject these functions where needed.
To achieve this we first create a simple helper function to shorten the access to Koin from top-level functions:
Now we can write the following provider functions:
As you can see each provider function actually works by returning an instance of some class we need to inject somewhere else. Given this, we can write the following Koin module:
Note that unfortunately the named() are necessary because of how Kotlin functions are implemented. The reflection system has no way to tell () -> String from () -> Retrofit, so we must name each function injection.
You can see that all injections are singletons. This is because we’re injecting a top-level function, which is immutable, so all our injections can be treated as singletons.
Mocking data source
Now what if we want to use a mocked data source? Let’s add a couple of flavors: mock and prod.
We create a couple of utility provider functions:
And of course their respective injections:
And we modify our provideGetColors() function to call the mock or production version depending on the flavor:
We didn’t even had to touch the dependency injection definition for provideGetColors() ;)
Avoiding unnecessary instantiation
However in the previous implementation we have an important drawback: some instances are always created when the function is called. For example the provideRetrofit() function will always instantiate a new Retrofit object. In some cases this might be overkill and impact performance. Fortunately this has a very simple solution: injecting the object instantiation instead, then we can control its creation (singleton or factory):
Repository and use case layers
As you can easily see, the same design can be applied to both the repository and the use cases: get rid of the interfaces and their implementations and use injected top-level functions. This will help you think without state, and if in any case you need to keep internal state, you can either inject it as well inside the functions or just use the good ol’ interface and implementations where needed if you feel more comfortable with it.
Unit testing is very straightforward: we don’t need mocks because well, there are no objects. We just need to inject the right functions for our test cases.
So what is your opinion? Is using top-level function injection worth ditching interfaces? What are the pros and cons you can see from using this approach? I’m really eager to read your opinions!
If you found this interesting, don’t forget to check the second part ;)