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.

Leave a rating/review
Download materials
Save for later
Share
You are currently viewing page 2 of 4 of this article. Click here to view the first page.

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:

  1. A @Component whose lifecycle is bound to the feature.
  2. 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:

  1. You defined FeatureComponentProvider as a typealias for Provider<FeatureComponent>. This will be useful when you need to access FeatureComponent from Fragments.
  2. MainActivity now implements the FeatureComponentProvider.
  3. You created lateinit property, which stores the reference to FeatureComponent.
  4. You need the reference to AppComponent. You get it from InitApp using casting.
  5. Dagger created the DaggerFeatureComponent for you with the Builder that defines appComponent()/code>. This lets you pass AppComponent's reference.
  6. The MainActivity implements the FeatureComponentProvider. You have to override get() to provide the FeatureComponent.

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! :]

RwNews App

Functional RwNews App

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.