Dagger 2 Tutorial for Android: Advanced – Part 2
In this tutorial, you’ll learn how to implement advanced features of Dagger 2 by using subcomponents, custom scopes and multibinding. By Massimo Carli.
Sign up/Sign in
With a free Kodeco account you can download source code, track your progress, bookmark, personalise your learner profile and more!
Create accountAlready a member of Kodeco? Sign in
Sign up/Sign in
With a free Kodeco account you can download source code, track your progress, bookmark, personalise your learner profile and more!
Create accountAlready a member of Kodeco? Sign in
Sign up/Sign in
With a free Kodeco account you can download source code, track your progress, bookmark, personalise your learner profile and more!
Create accountAlready a member of Kodeco? Sign in
Contents
Dagger 2 Tutorial for Android: Advanced – Part 2
30 mins
- Getting Started
- Managing Multiple @Scopes
- Exploring @Singleton
- Using @Singleton
- Creating a Feature @Component
- Managing @Component Dependencies
- Creating Your Custom @Scope
- Dependencies Between Differently-Scoped @Components
- Injecting With a Custom @Component
- Connecting the Custom @Scope
- Component Dependencies With @Subcomponent
- Migrating to @Subcomponent
- @Component Dependencies Versus @Subcomponent
- Multibinding in Dagger
- Adding Different Implementations
- Implementing Multibinding
- Getting Dagger to Use the Multibinding Implementation
- Where to Go From Here?
Creating a Feature @Component
The NewsRepository
implementation must be a single instance shared between all the objects of the app. This isn’t true for NewsListPresenter
and NewsDetailPresenter
, which should be present only while you display the news. Those classes have a different lifecycle.
If the app had some different features, you’d want to release those instances once you’re done with the feature to avoid wasting memory. Doing this requires two things:
- A
@Component
whose lifecycle is bound to the feature. - A custom
@Scope
to bind the dependencies to their related@Component
.
Your first step is to set up the new @Component
. Create a new file named FeatureComponent.kt in the di
package like this:
@Component(modules = [FeatureModule::class])
interface FeatureComponent {
fun inject(frag: NewsListFragment)
fun inject(frag: NewsDetailFragment)
}
The FeatureComponent
will hold the dependencies for the two features of the app – the news list, and details. This code is nearly the same as AppComponent
‘s.
Next, change AppComponent
to this:
@Component(modules =[AppModule::class])
@Singleton
interface AppComponent {
}
You removed the code that’s now in the FeatureComponent
. Now, you need to move the contents of AppModule
into a new file. Create FeatureModule.kt in the di
package and add:
@Module
abstract class FeatureModule {
@Binds
abstract fun provideNewsListPresenter(newsRepository: NewsListPresenterImpl): NewsListPresenter
@Binds
abstract fun provideNewsDetailPresenter(newsRepository: NewsDetailPresenterImpl): NewsDetailPresenter
}
This contains the definition that previously was in AppModule.kt.
Now, you’ll change that definition to:
@Module
abstract class AppModule {
@Binds
abstract fun provideNewsRepository(newsRepository: MemoryNewsRepository): NewsRepository
}
This contains only the @Binds
for the NewsRepository
.
Try to build and run and you’ll get the following error:
FeatureComponent.java:7: error: [Dagger/MissingBinding] com.raywenderlich.rwnews.repository.NewsRepository cannot be provided without an @Provides-annotated method.
public abstract interface FeatureComponent {
...
This is because you haven’t told Dagger how to use the new @Component
yet, and also because Dagger doesn’t know how to manage the NewsRepository
implementation. That implementation is the responsibility of AppComponent
.
For your next step, you’ll make FeatureComponent
use objects from AppComponent
.
Managing @Component Dependencies
As you’ve seen, building the app throws an error because FeatureComponent
doesn’t know how to implement NewsRepository
. You can fix this by using @Component
‘s dependencies attribute.
Open and modify the FeatureComponent
like this:
@Component(
modules = [FeatureModule::class],
dependencies = [AppComponent::class] // HERE
)
interface FeatureComponent {
- - -
}
Here, you’re telling Dagger that the new FeatureComponent
needs objects from the dependency graph that AppComponent
manages. Build and run the app and you’ll get a different error:
FeatureComponent.java:6: error: com.raywenderlich.rwnews.di.FeatureComponent (unscoped) cannot depend on scoped components:
@dagger.Component(modules = {com.raywenderlich.rwnews.di.FeatureModule.class}, dependencies = {com.raywenderlich.rwnews.di.AppComponent.class})
Here, AppComponent
is scoped because it uses @Singleton
, but Dagger is complaining that you can only create dependencies between scoped components. That’s because Dagger doesn’t understand the relationship between objects in @FeatureComponent
and the ones in @AppComponent
.
To address this, you need a custom scope, which you’ll make next.
Creating Your Custom @Scope
Creating a custom @Scope
is simple; it’s similar to @Singleton
‘s code.
Create a file named FeatureScope.kt in the di
package and add the following code:
import javax.inject.Scope
@Scope
@MustBeDocumented
@Retention(AnnotationRetention.RUNTIME)
annotation class FeatureScope
It’s like @Singleton
with a different name and with @MustBeDocumented
in place of the deprecated @Documented
.
Now, add the following to FeatureComponent
:
@Component(
modules = [FeatureModule::class],
dependencies = [AppComponent::class]
)
@FeatureScope // HERE
interface FeatureComponent {
- - -
}
This implements the new @Scope
.
Build and run and you’ll notice that the previous error has disappeared. However, Dagger’s now complaining about NewsRepository
.
Dependencies Between Differently-Scoped @Components
You can fix the problem easily. If one @Component
wants to use objects from another using the dependencies
attribute, a function needs to explicitly expose them.
In this case, add the following definition to AppComponent
:
@Component(modules = [AppModule::class])
@Singleton
interface AppComponent {
fun repository(): NewsRepository // HERE
}
This function tells FeatureComponent
how to access NewsRepository
‘s implementation, even with a different scope.
Build and run. The app will work from Dagger’s side, but you still have to use @FeatureComponent
instead of @AppComponent
to inject dependencies.
Injecting With a Custom @Component
In the next step, you’ll tell Dagger when to use and release the new FeatureComponent
. In this case, the lifecycle of the feature is the lifecycle of MainActivity
.
You’ll create FeatureComponent
in the MainActivity
, just as you created AppComponent
in InitApp
.
You also need to pass AppComponent
‘s reference to FeatureComponent
to manage NewsRepository
‘s dependencies.
Start by going to MainActivity.kt and add the following code:
import javax.inject.Provider
// 1
typealias FeatureComponentProvider = Provider<FeatureComponent>
// 2
class MainActivity : AppCompatActivity(), FeatureComponentProvider {
// 3
lateinit var featureComp: FeatureComponent
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
if (savedInstanceState == null) {
supportFragmentManager.beginTransaction()
.replace(R.id.anchor, NewsListFragment())
.commit()
// 4
val appComp = (applicationContext as InitApp).appComp()
// 5
featureComp = DaggerFeatureComponent.builder()
.appComponent(appComp)
.build()
}
}
// 6
override fun get(): FeatureComponent = featureComp
}
You’ve done many important things with this code:
- You defined
FeatureComponentProvider
as a typealias forProvider<FeatureComponent>
. This will be useful when you need to accessFeatureComponent
fromFragment
s. -
MainActivity
now implements theFeatureComponentProvider
. - You created
lateinit
property, which stores the reference toFeatureComponent
. - You need the reference to
AppComponent
. You get it fromInitApp
using casting. - Dagger created the
DaggerFeatureComponent
for you with the Builder that definesappComponent()/code>. This lets you pass
AppComponent
's reference. - The
MainActivity
implements theFeatureComponentProvider
. You have to overrideget()
to provide theFeatureComponent
.
The last step is to use FeatureComponent
in both NewsListFragment
and NewsDetailFragment
. This is very easy – you just have to replace the current implementation of onAttach()
in both classes, like this:
override fun onAttach(context: Context) {
(context as FeatureComponentProvider).get().inject(this)
super.onAttach(context)
}
Now, build and run. The app will finally run as usual! :]
You just connected a bunch of things, but... wait, you forgot something! You'll address that next.
Connecting the Custom @Scope
In the previous code, you used @FeatureScope
only on the FeatureComponent
– there are no objects with the same annotation.
This means that you're creating new instances of NewsListPresenterImpl
and NewsDetailPresenterImpl
at every injection.
Test that by adding this log message to NewsListFragment
:
override fun onAttach(context: Context) {
(context as FeatureComponentProvider).get().inject(this)
super.onAttach(context)
Log.i(TAG, "In NewsListFragment using NewsListPresenter $newsListPresenter")
}
Or add the following to NewsDetailFragment
:
override fun onAttach(context: Context) {
(context as FeatureComponentProvider).get().inject(this)
super.onAttach(context)
Log.i(TAG, "In NewsDetailFragment using NewsDetailPresenter $newsDetailPresenter")
}
Build and run the app. You'll see a log like this:
I/AdvDagger: In NewsListFragment using NewsListPresenter .presenter.impl.NewsListPresenterImpl@ccf8169
/I/AdvDagger: In NewsDetailFragment using NewsDetailPresenter .presenter.impl.NewsDetailPresenterImpl@e66466f // DIFFERENT
I/AdvDagger: In NewsDetailFragment using NewsDetailPresenter .presenter.impl.NewsDetailPresenterImpl@27a84f // DIFFERENT
Every time you display the details, Dagger creates a new instance of the NewsDetailPresenterImpl
. That's not good, but the solution is very easy. Just annotate the NewsDetailPresenterImpl
like this using your custom scope @FeatureScope
:
@FeatureScope // HERE
class NewsDetailPresenterImpl @Inject constructor(
private val newsRepository: NewsRepository
) : BasePresenter<NewsModel, NewsDetailView>(),
NewsDetailPresenter {
- - -
}
Build and run the app again, repeating the same actions. You'll see a log like this:
I/AdvDagger: In NewsListFragment using NewsListPresenter .presenter.impl.NewsListPresenterImpl@ccf8169
I/AdvDagger: In NewsDetailFragment using NewsDetailPresenter .presenter.impl.NewsDetailPresenterImpl@e66466f // SAME
I/AdvDagger: In NewsDetailFragment using NewsDetailPresenter .presenter.impl.NewsDetailPresenterImpl@e66466f // SAME
Now, NewsDetailPresenterImpl
's instance has the same lifecycle as FeatureComponent
, which is the same lifecycle as MainActivity
.