Dagger is a great tool for dependency injection (DI). It does most of the job in compile time, making applications smaller and faster and dependency resolving safer.
What’s not great is the documentation. Dagger is not a simple tool by any means. It requires a certain level of understanding to be used properly and efficiently.
Unfortunately, Dagger’s official page provides only a simplified example and explains how to build the dependency graph. It doesn’t provide insights into what we’re building and why. That’s the reason we see so many new articles like “How to Use Dagger: The Full and Final Version” every week.
With this article, I’m going to add one more to that count. This one explains WHAT components, dependencies, modules, and scopes are, and HOW are they related.
Further Reading: Find out six ways in which Nearshore Staff Augmentation can benefit your business, and tips to make it work. Discover eight compelling benefits of Nearshoring your Software Development to Mexico. |
Components
The best way to think about components is to treat them like all other objects in your app. They look like the parts of the glue that inject something into your code and let it do the real job. In fact, however, they’re your actual code, and not a boilerplate. YOU are responsible for the component’s lifecycle in the same way you’re responsible for all other objects. Check out this typical example:
DaggerSomeServiceComponent.create().inject(this)
In this example, the subtle create() actually creates a new object.
The official documentation says that Dagger is intended to replace boring handwritten factories. Here’s how you’re creating a factory constructed by Dagger to have at your disposal.
Like any other factory, this auto-generated one can create a new object every time it’s asked to do so. It can also give you a cached one to be reused — one that’s controlled by your provider methods and scope annotations. Now you have a “factory.”
What inject(this) does is that it calls all those tedious someService = someServiceFactory.createWebService(someParameters) under the hood for you. So if you call .create().inject(…) in another place, you’ll never get the same objects injected (including singletons) since it’s a new factory that knows nothing about the old one you created just a minute ago. That’s why you’re responsible for the component’s lifecycle, and that’s why you may want to cache the object that .create() returns for future use.
Now that we’ve covered components, it’s time to talk about dependencies.
Dependencies
When you really need to use some long-living objects (e.g., a global HTTP cache) alongside short-living ones, you need to combine different scopes. That way, you can actually have the same HTTP cache for each web-service client you create at runtime and you can declare a dependency of one (short-living) component on another (long-living). Like this:
@Component(dependencies = [CoreComponent::class])
interface HighLevelComponent
And then use it as follows:
DaggerHighLevelComponent.builder()
.coreComponent(coreComponentInstance)
.build()
.inject(this)
It’s a typical code that you can find in many Android activities. Method .coreComponent(…) actually provides HighLevelComponent with the instance of CoreComponent. So if you retain the same core instance and provide it each time you build high-level components, you’ll get the same objects provided by core module. Alternatively, if you opt to create a new core component every time (like this: .coreComponent(DaggerCoreComponent.create()) ), you’ll get a brand new “core” for every new activity.
That’s why I said that YOU are responsible for the component’s lifecycle. Dagger won’t take care of the singletons if you don’t preserve the component instance. So why do we then need @Singleton and custom scope annotations?
Scopes
The main goal of scope annotations is to bind the component’s lifecycle to the lifecycles of the objects it injects. Once you define the scope for the object (via the annotation on the class itself or the provider method), Dagger will force you to set the same scope for the component. Then, it will guarantee that the component will return the same object every time it’s asked — but only if you don’t construct the object explicitly in your provider method!
There is one exception to this rule — @Reusable scope, which is something in between unscoped and scoped. Take a look at this example:
@Component(modules = [SingletonScopeModule::class])
@Singleton
interface SingletonScopedComponent {
fun inject(obj: SingletonScopedDemo)
}
@Module
abstract class SingletonScopeModule {
@Binds
abstract fun providesSingleton(obj: SingletonScoped): ISingletonScoped
}
@Component(modules = [ScopeOneModule::class])
@ScopeOne
interface SomeScopedComponent {
fun inject(obj: SomeScopedDemo)
}
@Module
abstract class ScopeOneModule {
@Binds
abstract fun provideScopeOne(obj: SomeScoped): ISomeScoped
}
Generated Dagger code will be exactly the same for both components. In addition, you can add unscoped dependencies to any of these components because unscoped objects are not “controlled” by Dagger. There are no restrictions on them, and they’re always recreated when asked because generated Dagger code simply calls the constructor every time.
That’s all for scopes. Why, then, do we need modules?
Modules
Finally, we come to the actual glue: modules. Modules are just pieces of code that describe HOW you want to build your dependencies. For example, if you need to create a new object every time, the provider will look like this:
@Provides
fun provideUnscoped() : IUnscoped = Unscoped()
A provider can contain any arbitrary code, including network (please don’t) and factory method calls. However, if the constructor is public and all parameters can be provided by Dagger, you can annotate the constructor with @Inject like this:
class UnscopedBound @Inject constructor() : IUnscopedBound
…and then let Dagger invoke it for you. Like this:
@Provides
fun provideUnscopedBound(binding: UnscopedBound) : IUnscopedBound = binding
Or even like this:
@Binds
abstract fun provideUnscopedBound(binding: UnscopedBound) : IUnscopedBound
The type of the parameter here tells Dagger which class to create once the corresponding interface is requested. @Binds is just a shortcut in case you don’t need any custom code in your provider, so you can skip it and let Dagger generate it for you. (Note that binding is only allowed if all the module methods are either static or abstract).
Finally, if you want the object to be created only once and reused everywhere, annotate the class or the provider (or even both) with a scope annotation:
@Provides
@Singleton
fun provideSingletonBound(binding: SingletonBound) : ISingletonBound = binding
Summary
There are many other things you can learn about Dagger, but the topics outlined in this article should be enough to help you understand the main ideas. The full example code is available on Github.
If you would like to learn more, there’s plenty of information on the subject. Other aspects of Dagger are well-described by the community. In particular, I can recommend this series covering some basics as well as some nice pro features and some gotchas. In addition, this article is a great summary of Dagger best practices.
Let your dagger shine!
At Distillery, our developers are passionate about sharing insight, how-to guides, and best practices. Want to learn more about our team? Let us know!