Chapters

Hide chapters

Dagger by Tutorials

First Edition · Android 11 · Kotlin 1.4 · AS 4.1

10. Understanding Components
Written by Massimo Carli

Heads up... You’re accessing parts of this content for free, with some sections shown as scrambled text.

Heads up... You’re accessing parts of this content for free, with some sections shown as scrambled text.

Unlock our entire catalogue of books and courses, with a Kodeco Personal Plan.

Unlock now

In the previous chapters, you learned how to deal with @Modules in Dagger. You learned how a @Module helps you structure the code of your app and how you can use them to control the way Dagger creates instances of the objects in the dependency graph.

You also had the opportunity to meet the most important concept in Dagger: the @Component. You learned that a @Component defines the factory methods for the objects in your app’s dependency graph. When you get a reference to an object using a @Component’s factory method, you’re confident that Dagger has resolved all its dependencies. You saw this in both the simple Server-Repository example and in the more complex RaySequence app.

In this chapter, you’ll go back to working on the Busso App. You’ll learn how to:

  • Migrate the existing ServiceLocators and Injectors to Dagger’s equivalent @Modules and @Components.
  • Provide existing objects with a customized Builder for the @Component using @Component.Builder.
  • Use @Component.Factory as a valid alternative to @Component.Builder.

These are fundamental concepts you must understand to master Dagger. They’re also a prerequisite for the next chapter, where you’ll learn all about scopes. So get ready to dive in!

Migrating Busso to Dagger

As mentioned earlier, @Components are one of the most important concepts in Dagger. As you saw in the previous examples, a @Component:

  • Is the factory for all the objects in the dependency graph.
  • Allows you to implement the object you referred to as an Injector in the first section of the book.

Don’t worry if you don’t remember everything. With Busso’s help, you’ll review all the concepts over the course of this chapter. In particular, you’ll see how to:

  • Remove the Injector implementations, delegating the actual injection of the dependent objects to the code Dagger generates for you from a @Component.
  • Replace the ServiceLocator implementations with @Modules.

To start, use Android Studio to open the Busso App from the starter folder in the downloaded materials for this chapter. As you see in the code, the app uses the model view presenter and has ServiceLocator and Injector implementations for all the Activity and Fragment definitions.

In Figure 10.1, you can see the project structure of the di package, which you’ll switch over to Dagger first.

Figure 10.1 — Starting project structure of the Busso app
Figure 10.1 — Starting project structure of the Busso app

It’s always nice to delete code in a project and make it simpler. So let the fun begin!

Installing Dagger

The first thing you need to do is to install the dependencies Dagger needs for the project. Open build.gradle from the app module and apply the following changes:

plugins {
  id 'com.android.application'
  id 'kotlin-android'
  id 'kotlin-android-extensions'
  id 'kotlin-kapt' // 1
}
apply from: '../versions.gradle'

// ...

dependencies {
  // ...
  // 2
  // Dagger dependencies
  implementation "com.google.dagger:dagger:$dagger_version"
  kapt "com.google.dagger:dagger-compiler:$dagger_version"
}
Figure 10.2 — Sync Gradle file icon
Zaqoye 32.1 — Ggkm Dpovte gedi okic

Studying the dependency graph

In the previous examples, you learned that a:

Figure 10.3 — SplashActivity dependency graph
Nenaba 30.7 — BnfojnExnavuzv toxayjezhq lzutg

Removing the injectors

Start by opening SplashActivityInjector.kt from di.injectors and looking at the current implementation:

object SplashActivityInjector : Injector<SplashActivity> {
  // 1
  override fun inject(target: SplashActivity) {
    // 2
    val activityServiceLocator =
        target.lookUp<ServiceLocatorFactory<AppCompatActivity>>(ACTIVITY_LOCATOR_FACTORY)
            .invoke(target)
    // 3        
    target.splashPresenter = activityServiceLocator.lookUp(SPLASH_PRESENTER)
    target.splashViewBinder = activityServiceLocator.lookUp(SPLASH_VIEWBINDER)
  }
}

Creating the @Module

Create a new file named AppModule.kt in the di package and add the following code:

// 1
@Module(includes = [AppModule.Bindings::class])
class AppModule {
  // 2
  @Module
  interface Bindings {
    // 3
    @Binds
    fun bindSplashPresenter(impl: SplashPresenterImpl): SplashPresenter
    // 4
    @Binds
    fun bindSplashViewBinder(impl: SplashViewBinderImpl): SplashViewBinder
  }
}

Creating SplashPresenter and SplashViewBinde

Now, Dagger knows which classes to use when you need an object of type SplashPresenter or SplashViewBinder. It doesn’t know how to create them, though. To start solving that problem, open SplashPresenterImpl.kt from ui.splash and look at its header:

class SplashPresenterImpl constructor( // HERE
    private val locationObservable: Observable<LocationEvent>
) : BasePresenter<SplashActivity, SplashViewBinder>(), SplashPresenter {
  // ...
}
class SplashPresenterImpl @Inject constructor( // HERE
    private val locationObservable: Observable<LocationEvent>
) : BasePresenter<SplashActivity, SplashViewBinder>(), SplashPresenter {
@Module(includes = [AppModule.Bindings::class])
class AppModule(
	// 1
    private val activity: Activity
) {
  // ...
  // 2
  @Provides
  fun provideLocationObservable(): Observable<LocationEvent> {
    // 3
    val locationManager = activity.getSystemService(Context.LOCATION_SERVICE) as LocationManager
    // 4
    val geoLocationPermissionChecker = GeoLocationPermissionCheckerImpl(activity)
    // 5
    return provideRxLocationObservable(locationManager, geoLocationPermissionChecker)
  }
}

Handling SplashViewBinderImpl

Open SplashViewBinderImpl.kt from ui.splash and look at the class’ header:

class SplashViewBinderImpl( // HERE
    private val navigator: Navigator
) : SplashViewBinder {
  // ...
}
class SplashViewBinderImpl @Inject constructor( // HERE
    private val navigator: Navigator
) : SplashViewBinder {
  // ...
}
@Module(includes = [AppModule.Bindings::class])
class AppModule(
    private val activity: Activity
) {
  // ...
  @Provides
  fun provideNavigator(): Navigator = NavigatorImpl(activity) // HERE
}

Creating & using the @Component

Now that you’ve created AppModule, Dagger knows everything it needs to bind the objects for the SplashActivity implementation. Now, you need a way to access all those objects — which means it’s time to implement the @Component.

// 1
@Component(modules = [AppModule::class])
interface AppComponent {
  // 2
  fun inject(activity: SplashActivity)
}
class SplashActivity : AppCompatActivity() {

  @Inject // 1
  lateinit var splashViewBinder: SplashViewBinder

  @Inject // 2
  lateinit var splashPresenter: SplashPresenter

  override fun onCreate(savedInstanceState: Bundle?) {
    super.onCreate(savedInstanceState)
    makeFullScreen()
    setContentView(R.layout.activity_splash)
    DaggerAppComponent.builder() // 3
        .appModule(AppModule(this)) // 4
        .build() // 5
        .inject(this)  // 6
    splashViewBinder.init(this)
  }
  // ...
}
Figure 10.4 — The Busso App
Subodi 00.3 — Khi Pirwo Akj

Completing the migration

If you’re reading this, you decided to complete Busso’s migration to Dagger. Great choice! Now that SplashActivity is done, it’s time to continue the migration for the other components. You’ll notice this is really simple.

Migrating MainActivity

Open MainActivityInjector.kt from di.injectors and look at the following code:

object MainActivityInjector : Injector<MainActivity> {
  override fun inject(target: MainActivity) {
    val activityServiceLocator =
        target.lookUp<ServiceLocatorFactory<AppCompatActivity>>(ACTIVITY_LOCATOR_FACTORY)
            .invoke(target)
    target.mainPresenter = activityServiceLocator.lookUp(MAIN_PRESENTER) // HERE
  }
}
Figure 10.5 — MainActivity dependency graph
Nofafa 73.5 — NuocAphulehv xacahhikcn vmupt

@Module(includes = [AppModule.Bindings::class])
class AppModule(
    private val activity: Activity
) {

  @Module
  interface Bindings {
    // ...
    @Binds
    fun bindMainPresenter(impl: MainPresenterImpl): MainPresenter // HERE
  }
  // ...
}
class MainPresenterImpl @Inject constructor( // HERE
    private val navigator: Navigator
) : MainPresenter {
  override fun goToBusStopList() {
    navigator.navigateTo(FragmentDestination(BusStopFragment(), R.id.anchor_point))
  }
}
@Component(modules = [AppModule::class])
interface AppComponent {
  // ...
  fun inject(activity: MainActivity) // HERE
}
class MainActivity : AppCompatActivity() {

  @Inject // 1
  lateinit var mainPresenter: MainPresenter

  override fun onCreate(savedInstanceState: Bundle?) {
    super.onCreate(savedInstanceState)
    setContentView(R.layout.activity_main)
    // 2
    DaggerAppComponent
        .builder()
        .appModule(AppModule(this))
        .build()
        .inject(this) // 3
    if (savedInstanceState == null) {
      mainPresenter.goToBusStopList()
    }
  }
}

Migrating Busso’s fragments

Migrating BusStopFragment and BusArrivalFragment to Dagger is easy now. There’s just one small thing to consider: They both extend Fragment but they need access to the AppComponent implementation you created in MainActivity. That’s because they use classes that depend on:

Exposing AppComponent to the fragments

Here, you need to make the AppComponent available to the app’s Fragments. You’ll learn how Dagger solves this problem in the following chapters. At the moment, the easiest way is to add a simple utility method.

class MainActivity : AppCompatActivity() {
  // ...
  lateinit var comp: AppComponent // 1

  override fun onCreate(savedInstanceState: Bundle?) {
    super.onCreate(savedInstanceState)
    setContentView(R.layout.activity_main)
    // 2
    comp = DaggerAppComponent
        .builder()
        .appModule(AppModule(this))
        .build().apply {
          inject(this@MainActivity)
        }
    if (savedInstanceState == null) {
      mainPresenter.goToBusStopList()
    }
  }
}
// 3
val Context.comp: AppComponent?
  get() = if (this is MainActivity) comp else null

Adding the NetworkModule

You’re now going to create a @Module to tell Dagger how to get an implementation of BussoEndpoint to add to the dependency graph. Create a new file named NetworkModule.kt in the network package for the app and add this content:

private val CACHE_SIZE = 100 * 1024L // 100K

@Module
class NetworkModule(val context: Context) { // HERE

  @Provides
  fun provideBussoEndPoint(): BussoEndpoint {
    val cache = Cache(context.cacheDir, CACHE_SIZE)
    val okHttpClient = OkHttpClient.Builder()
        .cache(cache)
        .build()
    val retrofit: Retrofit = Retrofit.Builder()
        .baseUrl(BUSSO_SERVER_BASE_URL)
        .addCallAdapterFactory(RxJava2CallAdapterFactory.create())
        .addConverterFactory(
            GsonConverterFactory.create(
                GsonBuilder().setDateFormat("yyyy-MM-dd'T'HH:mm:ssZ").create()
            )
        )
        .client(okHttpClient)
        .build()
    return retrofit.create(BussoEndpoint::class.java)
  }
}
fun provideBussoEndPoint(context: Context): BussoEndpoint {
  // ...
}
@Module
class NetworkModule(val context: Context) { // HERE

  @Provides
  fun provideBussoEndPoint(): BussoEndpoint {
    // ...
  }
}
@Component(modules = [AppModule::class, NetworkModule::class]) // HERE
interface AppComponent {
  // ...
}

Creating the NetworkModule instance

Next, open MainActivity and add the following:

class MainActivity : AppCompatActivity() {
  // ...
  override fun onCreate(savedInstanceState: Bundle?) {
    super.onCreate(savedInstanceState)
    setContentView(R.layout.activity_main)
    comp = DaggerAppComponent
        .builder()
        .appModule(AppModule(this))
        .networkModule(NetworkModule(this)) // HERE
        .build().apply {
          inject(this@MainActivity)
        }
    if (savedInstanceState == null) {
      mainPresenter.goToBusStopList()
    }
  }
}
// ...
class SplashActivity : AppCompatActivity() {
  // ...
  override fun onCreate(savedInstanceState: Bundle?) {
    super.onCreate(savedInstanceState)
    makeFullScreen()
    setContentView(R.layout.activity_splash)
    DaggerAppComponent.builder()
        .appModule(AppModule(this))
        .networkModule(NetworkModule(this)) // HERE
        .build()
        .inject(this)
    splashViewBinder.init(this)
  }
  // ...
}

Handling deprecated @Modules

Before proceeding, it’s worth mentioning that the editor might display something like you see in Figure 10.6:

Figure 10.6 — A deprecated @Module
Leqahe 77.4 — A lacgovucag @Pimoza

Migrating BusStopFragment

Your next step is to quickly migrate the BusStopFragment following the same process you used above. Open AppModule.kt and add the following bindings:

@Module(includes = [AppModule.Bindings::class])
class AppModule(
    private val activity: Activity
) {

  @Module
  interface Bindings {
    // ...
    @Binds
    fun bindBusStopListViewBinder(impl: BusStopListViewBinderImpl): BusStopListViewBinder

    @Binds
    fun bindBusStopListPresenter(impl: BusStopListPresenterImpl): BusStopListPresenter

    @Binds
    fun bindBusStopListViewBinderListener(impl: BusStopListPresenterImpl): BusStopListViewBinder.BusStopItemSelectedListener
  }
  // ...
}
@Singleton // 1
class BusStopListPresenterImpl @Inject constructor( // 2
    private val navigator: Navigator,
    private val locationObservable: Observable<LocationEvent>,
    private val bussoEndpoint: BussoEndpoint
) : BasePresenter<View, BusStopListViewBinder>(),
    BusStopListPresenter {
  // ...
}
class BusStopListViewBinderImpl @Inject constructor( // HERE
    private val busStopItemSelectedListener: BusStopListViewBinder.BusStopItemSelectedListener
) : BusStopListViewBinder {
  // ...
}

Using AppComponent in BusStopFragment

Your last step is to use AppComponent in BusStopFragment. First, open AppComponent.kt and add the following definitions:

@Component(modules = [AppModule::class, NetworkModule::class])
@Singleton // 1
interface AppComponent {
  // ...
  fun inject(fragment: BusStopFragment) // 2
}
class BusStopFragment : Fragment() {

  @Inject // 1
  lateinit var busStopListViewBinder: BusStopListViewBinder

  @Inject // 1
  lateinit var busStopListPresenter: BusStopListPresenter

  override fun onAttach(context: Context) {
    context.comp?.inject(this) // 2
    super.onAttach(context)
  }
  // ...
}

Migrating BusArrivalFragment

You’re very close to completing the migration of Busso to Dagger. Open AppModule.kt and add the following bindings:

@Module(includes = [AppModule.Bindings::class])
class AppModule(
    private val activity: Activity
) {

  @Module
  interface Bindings {
    // ...
    @Binds
    fun bindBusArrivalPresenter(impl: BusArrivalPresenterImpl): BusArrivalPresenter

    @Binds
    fun bindBusArrivalViewBinder(impl: BusArrivalViewBinderImpl): BusArrivalViewBinder
  }
  // ...
}
class BusArrivalPresenterImpl @Inject constructor( // HERE
    private val bussoEndpoint: BussoEndpoint
) : BasePresenter<View, BusArrivalViewBinder>(),
    BusArrivalPresenter {
  // ...
}
class BusArrivalViewBinderImpl @Inject constructor() : BusArrivalViewBinder { // HERE
  // ...
}
@Component(modules = [AppModule::class, NetworkModule::class])
interface AppComponent {
  // ...
  fun inject(fragment: BusArrivalFragment) // HERE
}
class BusArrivalFragment : Fragment() {
  // ...
  @Inject // 1
  lateinit var busArrivalViewBinder: BusArrivalViewBinder

  @Inject // 1
  lateinit var busArrivalPresenter: BusArrivalPresenter

  override fun onAttach(context: Context) {
    context.comp?.inject(this) // 2
    super.onAttach(context)
  }
  // ...
}

Cleaning up the code

The errors you see after building the app are due to the existing ServiceLocator and Injector implementations. To fix them, you just have to delete the:

Figure 10.7 — File structure after Dagger migration
Yoziwa 43.4 — Runi wpnuqdolo ubsop Rexvuc gulqezoix

Customizing @Component creation

As you saw in the previous code, providing the reference to existing objects like Context or Activity isn’t uncommon. In your case, you just provided an activity as a parameter for the primary constructor of the @Module, as in AppModule.kt:

@Module(includes = [AppModule.Bindings::class])
class AppModule(
    private val activity: Activity // HERE
) {
  // ...
}
class SplashActivity : AppCompatActivity() {
  // ...
  override fun onCreate(savedInstanceState: Bundle?) {
    super.onCreate(savedInstanceState)
    makeFullScreen()
    setContentView(R.layout.activity_splash)
    DaggerAppComponent.builder()
        .appModule(AppModule(this)) // HERE
        .networkModule(NetworkModule(this)) // HERE
        .build()
        .inject(this)
    splashViewBinder.init(this)
  }
  // ...
}

Using @Component.Builder

Open AppComponent.kt and add the following code:

@Component(modules = [AppModule::class, NetworkModule::class])
interface AppComponent {
  // ...
  // 1
  @Component.Builder
  interface Builder {

    @BindsInstance // 2
    fun activity(activity: Activity): Builder

    fun build(): AppComponent // 3
  }
}
@Component(modules = [AppModule::class, NetworkModule::class])
interface AppComponent {
  // ...
  @Component.Builder
  interface Builder {

    fun appModule(appModule: AppModule): Builder

    fun networkModule(networkModule: NetworkModule): Builder

    fun build(): AppComponent
  }
}
@Module(includes = [AppModule.Bindings::class])
class AppModule { // 1
  // ...
  @Provides // 2
  fun provideNavigator(activity: Activity): Navigator = NavigatorImpl(activity)

  @Provides // 3
  fun provideLocationObservable(activity: Activity): Observable<LocationEvent> {
    val locationManager = activity.getSystemService(Context.LOCATION_SERVICE) as LocationManager
    val geoLocationPermissionChecker = GeoLocationPermissionCheckerImpl(activity)
    return provideRxLocationObservable(locationManager, geoLocationPermissionChecker)
  }
}
@Module
class NetworkModule { // 1

  @Provides // 2
  fun provideBussoEndPoint(activity: Activity): BussoEndpoint {
    val cache = Cache(activity.cacheDir, CACHE_SIZE)
    // ...
  }
}
class MainActivity : AppCompatActivity() {
  // ...
  override fun onCreate(savedInstanceState: Bundle?) {
    super.onCreate(savedInstanceState)
    setContentView(R.layout.activity_main)
    comp = DaggerAppComponent
        .builder()
        .activity(this) // HERE
        .build().apply {
          inject(this@MainActivity)
        }
    if (savedInstanceState == null) {
      mainPresenter.goToBusStopList()
    }
  }
}
// ...
class SplashActivity : AppCompatActivity() {
  // ...
  override fun onCreate(savedInstanceState: Bundle?) {
    super.onCreate(savedInstanceState)
    makeFullScreen()
    setContentView(R.layout.activity_splash)
    DaggerAppComponent.builder()
        .activity(this) // HERE
        .build()
        .inject(this)
    splashViewBinder.init(this)
  }
  // ...
}

Using @Component.Factory

Using @Component.Builder, you learned how to customize the Builder implementation for the @Component Dagger creates for you. Usually, you get the reference to the Builder implementation, then you invoke some setter methods that pass the parameter you need and finally, you invoke build() to create the final object.

@Component(modules = [AppModule::class, NetworkModule::class])
interface AppComponent {
  // ...
  @Component.Factory // 1
  interface Factory {
    // 2
    fun create(@BindsInstance activity: Activity): AppComponent
  }
}

class SplashActivity : AppCompatActivity() {
  // ...
  override fun onCreate(savedInstanceState: Bundle?) {
    super.onCreate(savedInstanceState)
    makeFullScreen()
    setContentView(R.layout.activity_splash)
    DaggerAppComponent
        .factory() // 1
        .create(this) // 2
        .inject(this) // 3
    splashViewBinder.init(this)
  }
  // ...
}
class MainActivity : AppCompatActivity() {
  // ...
  override fun onCreate(savedInstanceState: Bundle?) {
    super.onCreate(savedInstanceState)
    setContentView(R.layout.activity_main)
    comp = DaggerAppComponent
        .factory() // HERE
        .create(this).apply {
          inject(this@MainActivity)
        }
    if (savedInstanceState == null) {
      mainPresenter.goToBusStopList()
    }
  }
}
// ...

Key points

  • The most important concept to understand in Dagger is the @Component, which works as the factory for the objects in the dependency graph.
  • You migrated Injectors to Dagger @Components and ServiceLocators to Dagger @Modules.
  • A dependency diagram helps you migrate an existing app to Dagger.
  • @Component.Builder lets you customize the Builder implementation that Dagger creates for your @Component.
  • With @Component.Factory, you ask Dagger to create a factory method to create your @Component implementation.
Have a technical question? Want to report a bug? You can ask questions and report bugs to the book authors in our official book forum here.
© 2024 Kodeco Inc.

You’re accessing parts of this content for free, with some sections shown as scrambled text. Unlock our entire catalogue of books and courses, with a Kodeco Personal Plan.

Unlock now