Dagger 2 Tutorial For Android: Advanced

In this tutorial, you’ll learn about the advanced concepts of Dagger. You’ll learn about component lifecycles, @Binds, and component builders and factories. By Massimo Carli.

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

Hidden Power of @Binds

The true power of both @Binds and static @Provides is that Dagger doesn’t generate factory classes which wrap those functions. This is the part about generating less code and increasing performance. Not only does it increase performance at build-time, but it also increases the runtime performance because Dagger is no longer allocating extra classes for each component you create.

Finally, by using @Inject and @Binds, you’ve abstracted away the creation of the repository dependency. This means that if you change the constructor parameters in the MemoryNewsRepository, you won’t have to change the provider function. It’ll update automatically, because of the @Inject.

If you did this for all the dependencies, you could freely update the constructors by adding or removing parameters, changing their order, and you wouldn’t have to do the extra work of updating the provider/factory functions.

The @Component.Builder Interface

Look at the last implementation of the AppModule. You can see that you needed to do magic with the companion object and static methods because of the use of @Binds which required the annotated method to be abstract.

But what if you want to provide the instance for the NewsRepository type when you instantiate the AppComponent? To do this, change the implementation of the AppModule class like this:

@Module
class AppModule(
  // 1
  private val newsRepository: NewsRepository
) {

  // 2
  @Provides
  fun provideNewsListPresenter(): NewsListPresenter = NewsListPresenterImpl(newsRepository)

  // 2
  @Provides
  fun provideNewsDetailPresenter(): NewsDetailPresenter = NewsDetailPresenterImpl(newsRepository)

}

See the following new steps:

  1. The AppModule now defines a constructor with a parameter of the type NewsRepository.
  2. The same is then used to create the instances that @Provides annotated methods return. The methods don’t have any parameters now.

If you build the application now, you’ll get an error in InitApp, saying DaggerAppComponent.create() doesn’t exist.

Providing Parameterized Dependencies

If the @Module used by a @Component needs a parameter, Dagger doesn’t generate create(). It instead generates an implementation of the Builder pattern which requires you to provide the parameterized dependencies. Change the code in the InitApp like this:

class InitApp : Application() {

  lateinit var appComponent: AppComponent

  override fun onCreate() {
    super.onCreate()
    appComponent = DaggerAppComponent
      .builder()  // 1
      .appModule(AppModule(MemoryNewsRepository())) // 2
      .build() // 3
  }

  fun appComp() = appComponent
}

As you can see:

  1. The DaggerAppComponent that Dagger provides now has a builder() which returns an implementation of a Component.Builder.
  2. You create an instance of the MemoryNewsRepository you need, to create the instance of the AppModule. You then pass to the AppComponent builder the created AppModule using appModule().
  3. The last method to invoke in the Builder implementation is called build>(). It returns the AppComponent implementation.

In this case, you don’t need the @Singleton annotation anymore. This is because you’re responsible of the creation of the MemoryNewsRepository class which is then used by the AppComponent any time it needs it.

Remove @Singleton from the MemoryNewsRepository and AppComponent as in these definitions:

class MemoryNewsRepository : NewsRepository {
 - - -
}
@Component(modules = [AppModule::class])
interface AppComponent {
  - - -
}

Build and run the app. It now works as expected, reusing the same instance of MemoryNewsRepository in all the fragments.

RwNews App

User selects a generic title from a list and the story displays

RwNews App

On one hand, you own the MemoryNewsRepository, so passing it in as a parameter didn’t do much. On the other hand, this is how you pass the reference to the Context, the Resources, or any other runtime-created dependency you don’t own in Andriod.

Improving Parameterized Dependencies

In the previous code, you created the MemoryNewsRepository in the InitApp and passed it to the builder of the AppComponent encapsulating it within an instance of the AppModule.

You can do better and pass only what Dagger really needs: The instance of the MemoryNewsRepository. To do this, change the current code in the AppComponent.kt to get the following implementation:

@Component(modules = [AppModule::class])
interface AppComponent {

  fun inject(frag: NewsListFragment)

  fun inject(frag: NewsDetailFragment)

  // 1
  @Component.Builder
  interface Builder {

    // 2
    @BindsInstance
    fun repository(repo: NewsRepository): Builder

    // 3
    fun build(): AppComponent
  }
}

There are many important things to note here:

  1. You created an inner interface annotated with @Component.Builder. The name is usually Builder but it’s not important.
  2. The interface needs to define some operations similar to the ones you created for @Binds. Each of them must have a single parameter of the type of the object you need to inject and must have the same interface as return type.
    This allows for chaining of such methods. Each of these methods must be annotated with @BindsInstance.
  3. Finally, need to define a method whose name must be build which must have the component as the return type. In this case, the type is AppComponent.

As you can see, there are many musts you need to follow.

Now you open the AppModule and remove the parameter from the constructor getting the following implementation:

@Module
class AppModule {

  @Provides
  fun provideNewsListPresenter(newsRepository: NewsRepository): NewsListPresenter =
    NewsListPresenterImpl(newsRepository)

  @Provides
  fun provideNewsDetailPresenter(newsRepository: NewsRepository): NewsDetailPresenter =
    NewsDetailPresenterImpl(newsRepository)

}

You kinda went all the way back to square one. Such a déjà vu. :]

To build and run the app, you need to change the way you create the AppComponent within InitApp like this:

class InitApp : Application() {
  - - -
  override fun onCreate() {
    super.onCreate()
    appComponent = DaggerAppComponent
      .builder()
      .repository(MemoryNewsRepository()) // HERE
      .build()
  }
  - - -
}

Dagger created the repository() for the Builder which lets you pass the MemoryNewsRepository directly. Now build and run the project and the app still runs successfully! :]

RwNews App

User selects a generic title from a list and the story displays

RwNews App

Making Dagger Work for You Again

The previous implementation for the AppModule was a déjà vu because it’s very similar to one of the first implementations. But now you also know how to use @Binds! Change the AppModule class to this to re-implement @Binds:

@Module
abstract class AppModule {

  @Binds
  abstract fun provideNewsListPresenter(newsRepository: NewsListPresenterImpl): NewsListPresenter

  @Binds
  abstract fun provideNewsDetailPresenter(newsRepository: NewsDetailPresenterImpl): NewsDetailPresenter

}

This isn’t enough. While Dagger knows what classes to use as implementation of the NewsListPresenter and NewsDetailPresenter, it doesn’t know how to create an instance of NewsListPresenterImpl and NewsDetailPresenterImpl.

You already know the solution: @Inject. Add the annotation to the constructors like in the following code:

class NewsDetailPresenterImpl @Inject constructor(
  private val newsRepository: NewsRepository
) : BasePresenter<NewsModel, NewsDetailView>(),
  NewsDetailPresenter {
  - - -
}

and

class NewsListPresenterImpl @Inject constructor(
  private val newsRepository: NewsRepository
) : BasePresenter<NewsListModel, NewsListView>(),
  NewsListPresenter {
  - - -
}

Build and run the app as usual.

RwNews App

User selects a generic title from a list and the story displays

RwNews App