Dependency Injection: the pattern without the framework
It’s actually good!
Originally published on: blog.kotlin-academy.com by Jean-Michel Fayard
Dependency Injection is a pattern, not any particular framework. You could even implement it manually, but that would be too much trouble .
Wait.. Why? How do we know that?
Update: added a paragraph on the distinction between Dependency Injection (a term I’ve been using loosely) and Service Locator (the alternative pattern I came to usually prefer).
Dependency Injection is a big topic in the Android world. Neglect it and you will soon feel the painful experience that all your code is closely tied to the android framework, and you cannot really test your app in a meaningful and actually useful way.
Dagger, a dependency injection framework originally from Square then remixed by Google, is the big framework of the android world (though not limited to it). Familiarity with dagger is assumed in this article.
Good resources on the topic include a presentation by Jake Wharton, a fragmented podcast episode, and that missing guide: how to use dagger2. I will steal examples found in the latter article since I’ve found it practical and on the point.
What the discussions on the topic have usually in common is that they insist that dependency injection is a general programming pattern, not any particular framework. You can do DI yourself… well, except you don’t because that would not be practical at all.
I wondered: is that really so? I thought I would give it a try, curious to see at which point I would hit a wall.
That didn’t happen.
Want to see how to do dependency injection with a couple of conventions, a few tricks, and otherwise plain normal languages constructs?
Let’s get started.
Business Logic / Component / Configuration modules
We will keep the basic architecture of dagger, described in the schema above.
Because we have a similar architecture, we sill start the exploration by showing how to migrate away from dagger.
Let’s assume we have dagger configuration modules set-up correctly. Dagger can provide us an implementation of the AppComponent and that’s all that we care about for now. This allows us to tackle directly the business logic. But first, we will refactor the Component.
Note : if you want to try out the code snippets above without having to configure a complete android project for that, use the following snippet that defines the needed types https://gist.github.com/jmfayard/5313e2fa8afdbba84bb94ea5a0a52792
Starting point
// easy access to the component implementation
fun app() = AppComponent.instance
@Component([AppModule::class])
interface AppComponent {
// here we can do either
// constructor injection
// or
// fields injection
companion object {
lateinit var instance: AppComponent
}
}
class CustomApplication : Application() {
override fun onCreate() {
super.onCreate()
AppComponent.instance = DaggerAppComponent.Builder()
//..
.build()
}
}
We start with a standard dagger project, except that I make the component easily accessible via a top-level function. The convention will be explained later, bear with me for now.
fun app(): AppComponent = ...
Constructor injection, not fields injection
@Component(modules = [CatModule::class])
interface AppComponent {
/** Fields injection **/
// fun inject(myGodActivity: Activity)
// fun inject(myGodFragment: Fragment)
/** Types availables for constructor injection **/
fun context(): Context
fun retrofit(): Retrofit
fun okHttpClient(): OkHttpClient
fun catRepository(): CatRepository
fun catService(): CatService
@Named("MAIN") fun mainThreadScheduler(): Scheduler
@Named("BACKGROUND") fun backgroundThreadScheduler(): Scheduler
}
There are two styles of DI you can do at this point. I’m firmly on the side that prefers constructor injection. Fields injection in my mind plays too well with what Israel Ferrer Camacho calls the “ Burrito Design Pattern ”, where you put too much stuff in god objects that belong to the framework. Constructor injection is clean and simple.
A Component of properties
Before we move on to the business logic, we will apply an important trick: using properties inside of getter methods in the component interface!
IntelliJ/Android Studio has just the right refactoring tool: convert function to property
Let’s apply it to every getter method.
@Component(modules = [CatModule::class])
interface AppComponent {
val context: Context
val retrofit: Retrofit
val okHttpClient: OkHttpClient
val catRepository: CatRepository
val catService: CatService
@Named("MAIN")
val mainThreadScheduler: Scheduler
@Named("BACKGROUND")
val backgroundThreadScheduler: Scheduler
}
view rawAppComponen
What happened here? For us, it will be important, but from the perspective of dagger_:_ almost nothing. It sees your val property as a valid getter method. Compile the project again and it will continue to work like a charm.
Business Logic: constructor injection + default values
So we have a list of properties that can be injected. Here is how we can typically use them with the dagger approach of code generation:
class CatController @Inject constructor(
val catService: CatService,
val catRepository: CatRepository,
@Named("background")
val backgroundThread: Scheduler,
@Named("MAIN")
val mainThread: Scheduler
)
interface AppComponent {
val catController: CatController
// ... other existing properties
}
fun usage() {
val catController = app().catController
}
So we pass the properties in the constructor, marked it with the inject
annotation, add a new getter method (or actually, a property) in the Component. To get a CatController, we ask the Component to provide it.
Can we achieve the same thing without relying on dagger?
Yes, we can with a simple pattern: we declare the required properties in the constructor like before, and we declare a default value coming from the same property from the component.
class CatController(
// a normal property
val catId : Int,
// injected properties
val catService: CatService = app().catService,
val catRepository: CatRepository = app().catRepository,
val backgroundThread: Scheduler = app().backgroundThreadScheduler,
val mainThread: Scheduler = app().mainThreadScheduler
)
fun usage() {
// Just a normal call to the constructor
// with the convention that we don’t pass any values for the optional arguments
val controller = CatController(catId = 42)
}
I’ve been using this approach for months, alongside with dagger for building the AppComponent itself, and there are a lot of things I like about it:
- the usage side is super straightforward. It’s just a standard call to the constructor, with the convention that you don’t pass any values for the optional arguments.
- You don’t have to add a new property to the component
- You have two possibilities to enable testing: either you change the dependency graph globally by creating a
TestComponent
implementing AppComponent - … or you pass directly any tests doubles you need directly in the constructor.
- notice how you can mix freely injected fields (
catService
) and normal constructor parameters (catId
) - injecting two properties of a different type is a non-brainer (
mainThread
vsbackgroundThread
). No need to use an additional annotation likeNamed
- since we are using plain language constructs and not some extra-language annotation processing magic, your IDE(a) can help you in all the ways it usually helps you: autocompletion, jump to the definition of
app()
, see every implementation of a property, …
On the other hand, you will have to type a few more characters than we rely on Inject
. I am fine with the kind of boilerplate that make things explicit and that works well with auto-completion, so it’s a valid tradeoff for me.
I’ve been using this pattern for months and won’t look back.
Recap
Here is how each three parts of our dependency injection currently compares to dagger
- We keep dagger’s
Component
abstraction with small but important tweaks: we use constructor injection, kotlin properties, and a top-level function to access it easily. - In the business logic, we roll our own convention for the constructor injection part
- We have dagger implements the
Component
interface.
At that point, dagger is only an implementation detail. It implements the Component interface, and it does it well. But we could as well implement the Component interface directly.
There is a strong case to do just that when we start a new project.
Going From Zero To One
What if instead of starting with a project already using dagger, I am starting a fresh new project?
I have had my share of writing an app and then noticing too late that I can’t actually test it in a meaningful way. Never again! I want to have dependency injection from day 1. On the other hand, dagger is initially painful to setup. What if we could set up dagger later, but still use dependency injection?
Let say we are building the networking part. The most natural way to get started is something like that
interface ApiComponent // initially empty
object ApiModule : ApiComponent {
val loggingInterceptor = HttpLoggingInterceptor()
.setLevel(HttpLoggingInterceptor.Level.BODY)
val okHttpClient = OkHttpClient.Builder()
.addNetworkInterceptor(loggingInterceptor)
.build()
val moshi = Moshi.Builder().build()
val retrofit = Retrofit.Builder()
.client(okHttpClient)
.addCallAdapterFactory(RxJava2CallAdapterFactory.create())
.addConverterFactory(MoshiConverterFactory.create(moshi))
.baseUrl("http://httpbin.org")
.build()
val catApi = retrofit.create(CatApi::class.java)
}
Next we just need to make our interface ApiComponent
useful. Let’s use the refactoring tool Pull Members Up
.
Also, like before, we make the component accessible via a top-level function.
fun api() : ApiComponent = ApiModule
interface ApiComponent {
val okHttpClient: OkHttpClient
val moshi: Moshi
val catApi: CatApi
}
object ApiModule : ApiComponent {
val loggingInterceptor = HttpLoggingInterceptor()
.setLevel(HttpLoggingInterceptor.Level.BODY)
override val okHttpClient = OkHttpClient.Builder()
.addNetworkInterceptor(loggingInterceptor)
.build()
override val moshi = Moshi.Builder().build()
val retrofit = Retrofit.Builder()
.client(okHttpClient)
.addCallAdapterFactory(RxJava2CallAdapterFactory.create())
.addConverterFactory(MoshiConverterFactory.create(moshi))
.baseUrl("http://httpbin.org")
.build()
override val catApi = retrofit.create(CatApi::class.java)
}
The IDE did almost all of the work here! Being lazy, this is something I enjoy a lot. We are ready to write testable production code.
Kotlin properties
Let’s dive deeper after this first success. How hard would it be to duplicate all the clever work that dagger is doing with its code generation in pure kotlin? The answer is that kotlin properties are very powerful
Singleton Provides fun retrofit(okHttpClient: OkHttpClient, moshi: Moshi)
This is how we provided retrofit in the ApiModule example. There is actually a lot going on here. In dagger terms, our ApiModule is an ObjectGraph that provides the type Retrofit, as a singleton, based on other methods that providesMoshi
and OkHttpClient
. The compiler and the IDE work hard to ensure we have no null pointer exceptions and don’t mess up the order in which we initialize the properties.
the compiler working hard for us
With kotlin properties, as one Perl motto says, _simple things are easy, complicated things are possible.
class AndroidModule(val app: Context) : AndroidComponent {
private val preferences: SharedPreferences =
app.getSharedPreferences(app.packageName, Context.MODE_PRIVATE)
// Our properties are singleton by default
// but we are by no mean limited to it
// we can simply implement the property with a getter method
override val now: Instant
get() = Instant.now()
// Need to delay an expensive initialization? No problem
override val catApi by lazy { retrofit.create(CatApi::class.java) }
// and if that was not enough, we can use any delegated properties
// https://kotlinlang.org/docs/reference/delegated-properties.html
// For example with kotlin-jetpack we can trivially bind a property
// to a shared preference or an android resource
override val userId: String by preferences.bindPreference(key = "userId", default = "")
override val primaryTextColor: Int by app.bindResource(R.color.primaryText)
}
At this point, we are stuck with a first world problem: should we use dagger to implement the Component or implement it ourselves?
To that question, my answer is clear: YES
Both methods work. Depending on the context, we can choose a different tradeoff. If the team knows dagger well, dagger is fine. If the team knows kotlin well, the direct approach is fine. Also, it’s a great learning experience.
I have happily used dagger for months. Recently I switched to implementing it directly. Switching is not hard. Same concepts, same direct acyclic graph of dependencies, just a different syntax.
The tipping point for me was that I modularized my app and was able to relegate all code generation tools except dagger to smaller modules. Removing kapt
from the main module was a big win for having reliably fast incremental builds, build caches, network cache, … Also the superior IDE integration.
Bonus: what about java?
Having a project not 100% kotlin? No problem, the approach of relying on default argument works too. Just declare two constructors, one with all dependencies, and one where you pass the Component.
package di;
public class CatController {
private final int catId;
private final CatApi catApi;
private final Scheduler backgroundThread;
private final Scheduler mainThread;
public CatController(int catId, CatApi catApi, Scheduler backgroundThread, Scheduler mainThread) {
this.catId = catId;
this.catApi = catApi;
this.backgroundThread = backgroundThread;
this.mainThread = mainThread;
}
public CatController(int catId, AppComponent component) {
this(catId, component.getCatApi(), component.getMainThread(), component.getMainThread());
}
}
fun usage() {
// in the production code we use the constructor that takes the AppComponent
val controller = CatController(42, app())
}
Bonus: going multi-modules
As I said, I was in the process of splitting my app into multiple smaller gradle modules. As it turns out, it’s pretty simple to do with this pattern.
The function fun app(): AppComponent
is called so because it provides the properties that the :app
gradle module needs to be injected.
If we have multiple modules like :app, :common
we just need to repeat this pattern:
// Inside the gradle module :app
fun app() = AppComponent.instance
interface AppComponent {
val moshi: Moshi
val context: Context
companion object {
lateinit var instance: AppComponent
}
}
// Inside the gradle module :common
fun common() = CommonComponent.instance
interface CommonComponent {
val moshi: Moshi
// other properties
companion object {
lateinit var instance: CommonComponent
}
}
// Implement all the components at once
class ComponentsImplementation(
override val moshi: Moshi,
override val context: Context
) : CommonComponent, AppComponent
class CustomApplication: Application() {
override fun onCreate() {
// component is both a CommonComponent and a AppComponent
val component = ComponentsImplementation(
moshi = Moshi.Builder().build(),
context = this
)
// take care of those lateinit var instance
AppComponent.instance = component
CommonComponent.instance = component
}
}
Bonus: Injecting Activities/Fragments
Until now, I have assumed that I deal with plain normal classes where I can use constructor injection. But what about the activities and fragments that belong to the framework? We have often at least (and should have often at most) 0 fragments and 1 activity.
There are actually quite a few different ways to simulate fields injection you may find useful
class CatActivity: AppCompatActivity() {
// plain normal injection
val catRepository: CatRepository =
api().catRepository
// lazy injection
val catApi : CatApi by lazy {
api().catApi
}
// an alternative simpler lazy injection
// for when the component property itself is a singleton
val moshi: Moshi
get() = app().moshi
fun usage() {
// or directly inside an activity method
val repository: CatRepository = app().catRepository
}
}
Conclusion
If nothing else, trying to do manual Dependency Injection would have been a great learning experience. The first times I integrated dagger in my app, it felt overwhelming. Grasping at the same time the concepts and the syntax was hard. I have found it very helpful to dive into the core dependency injection pattern, which is actually simple despite being so useful. Once you get the pattern and have your graph of dependencies written down, setting up dagger is just a matter of syntax.
After months of playing with manual DI, I feel confident to say that it’s actually a fine alternative in many contexts.
It’s a superior experience when you start a new project and want to have DI as soon as possible. It‘s straightforward once you are used to it. The writing experience is superior, because all of the IDE integration that you are used to also works here, you don’t rely on features outside of the scope of the language. It plays nicely with build caches. It was a blessing for my build time because code generation is now confined to smaller modules.
Also, in the end, the concepts we are using are actually and intentionally not far from those from dagger. If at some point in the life of the project, it makes more sense to use the efficient and mature dagger framework, switching is not hard. Just a different syntax.
Update (27.02) : Dependency Injection vs Service Locator
I have been using loosely the term dependency injection in this article.
Astute readers have pointed out that the correct term for what I’m describing is a Service Locator. This is because my classes like CatController are reaching out externally (in my case via the default argument) to find their dependencies.
The minimum change I would have to make to move toward dependency injection would be this one below where the dependency to the locator itself is removed. From this contrived example one can begin to see the tradeoff. The CatController now knows nothing from the outside world. On the other hand the Component grows larger. There is a recursive aspect to it. Everything that merely *uses* an injected property should probably be produced by the component (DI framework) and scoped in a clever way (Our “scopes” were limited to: deliver a singleton or new instance)
/** Trying to figure out where the DI and Service Locator patterns differ
** compare this gist to https://gist.github.com/jmfayard/184f7389ec851d6b448789ca9fd91fdb
** where default values are declared in the constructor of CatController
**/
interface AppComponent {
// new property, directly implemented in the interface
val catController: CatController
get() = CatController(catService, catRepository, mainThread, backgroundThread)
// Existing injected properties
val catService: CatService
val catRepository: CatRepository
//...
}
class CatController(
val catService: CatService,
val catRepository: CatRepository
) {
...
}
fun main() {
val controller = app().catController
}
I had heard of this distinction before, but could not quite wrap my head around why it matters or wether I should care.
Before the discussion with my astute readers, I had a unclear pre-conception that the Service Locator pattern was somewhat inferior to the Dependency Injection pattern. In fact it’s quite similar, just has different thread-offs. Martin Fowler’s own take on this is that The choice between Dependency Injection and its Service Locator Alternative is less important than the principle of separating configuration from use.
The fundamental choice is between Service Locator and Dependency Injection. The first point is that both implementations provide the fundamental decoupling that’s missing in the naive example — in both cases application code is independent of the concrete implementation of the service interface. The important difference between the two patterns is about how that implementation is provided to the application class. With service locator the application class asks for it explicitly by a message to the locator. With injection there is no explicit request, the service appears in the application class — hence the inversion of control.
Inversion of control is a common feature of frameworks, but it’s something that comes at a price. It tends to be hard to understand and leads to problems when you are trying to debug. So on the whole I prefer to avoid it unless I need it. This isn’t to say it’s a bad thing, just that I think it needs to justify itself over the more straightforward alternative.
The key difference is that with a Service Locator every user of a service has a dependency to the locator. The locator can hide dependencies to other implementations, but you do need to see the locator. So the decision between locator and injector depends on whether that dependency is a problem (…)
Inversion of Control Containers and the Dependency Injection pattern
Taking a step back, what was I looking for? I wanted to decouple the core of my app from its boundaries (network, shared preferences, databases, android framework calls, …). I wanted to enable testing by easily replacing real service implementations with fakes one. I wanted to do this in a type-safe way.
The Service Locator pattern correctly implemented provides just that. It’s more straightforward because it still feels like regular code. The DI pattern on the other hand provides *more* than what I really needed. I can sense the beauty of it and how powerful it can be in some contextes, but it comes at a price. So my final realisation is that I would usually prefer a simpler Service Locator.
Links
If you like it, remember to clap. Note that if you hold the clap button, you can leave more claps!
To be up-to-date with great news on Kotlin Academy, subscribe to the newsletter, observe Twitter and follow.