Chapters

Hide chapters

Dagger by Tutorials

First Edition · Android 11 · Kotlin 1.4 · AS 4.1

11. Components & Scopes
Written by Massimo Carli

Heads up... You're reading this book for free, with parts of this chapter shown beyond this point as scrambled text.

In the previous chapter, you migrated the Busso App from a homemade framework with ServiceLocators and Injectors to Dagger. You converted Injectors into @Components and ServiceLocators into @Modules, according to their responsibilities. You learned how to manage existing objects with types like Context or Activity by using a @Component.Builder or @Component.Factory.

However, the migration from the previous chapter isn’t optimal — there are still some fundamental aspects you need to improve. For instance, in the current code, you:

  • Create a new instance of BussoEndpoint every time you need a reference to an object of type BusStopListPresenter or a BusArrivalPresenter that depends on it. Instead, you should have just one instance of the endpoint, which lives as long as the app does.
  • Use the @Singleton annotation to solve the problem of multiple instances of BusStopListPresenterImpl that are bound to the BusStopListPresenter and BusStopListViewBinder.BusStopItemSelectedListener abstractions. However, @Singleton isn’t always a good solution, and you should understand when to use it and when not to.
  • Get the reference to LocationManager from an Activity, but it should have a broader lifecycle, like the app does, and it should also depend on the app Context.

These are just some of the problems you’ll fix in this chapter. You’ll also learn:

  • The definition of a component and how it relates to containers.
  • What a lifecycle is, why it’s important and what its relationship to scope is.
  • More about @Singletons.
  • What a @Scope is and how it improves your app’s performance.

It’s going to be a very interesting and important chapter, so get ready to dive in!

Components and Containers

It’s important to understand the concept behind components. When you ask what a component is in an interview, you usually get different answers. In the Java context, one of the most common answers is, “A component is a class with getters and setters.” However, even though a component’s implementations in Java might have getters and setters, the answer is incorrect.

Note: The getter and setter thing is probably a consequence of the JavaBean specification that Sun Microsystems released way back in 1997. A JavaBean is a Java component that’s reusable and that a visual IDE can edit. The last property is the important one. An IDE that wants to edit a JavaBean needs to know what the component’s properties, events and methods are. In other words, the component needs to describe itself to the container, either explicitly — by using BeanInfo — or implicitly. To use the implicit method, you need to follow some conventions, one of which is that a component has the property prop of type T if it has two methods — getProp(): T and setProp(T). Because of this, a JavaBean can have getters and setters — but even when a class has getters and setters, it’s not necessarily a JavaBean.

The truth is that there’s no component without a container. In the relationship between the components and their container:

  1. The container is responsible for the lifecycle of the components it contains.
  2. There is always a way to describe the component to the container.
  3. Implementing a component means defining what do to when its state changes according to its lifecycle.

A related interview question in Android is, “What are the standard components of the Android platform?” The correct answer is:

  • Activity
  • Service
  • ContentProvider
  • BroadcastReceiver

In this case, the following applies to the components:

  1. The Android environment is the container that manages the lifecycle of standard components according to the available system resources and user actions.
  2. You describe these components to the Android Environment using AndroidManifest.xml.
  3. When you define an Android component, you provide implementations for some callback functions, including onCreate(), onStart(), onStop() and so on. The container invokes these to send a notification that there’s a transition to a different state in the lifecycle.

Note: Is a Fragment a standard component? In theory, no, because the Android Environment doesn’t know about it. It’s a component that has a lifecycle bound to the lifecycle of the Activity that contains it. From this perspective, it’s just a class with a lifecycle, like any other class. But because it’s an important part of any Android app, as you’ll see, it has the dignity of a specific scope.

Figure 11.1 — The Android Environment as Container
Figure 11.1 — The Android Environment as Container

But why, then, do you need to delegate the lifecycle of those components to the container — in this case, the Android environment? Because it’s the system’s responsibility to know which resources are available and to decide what can live and what should die. This is true for all the Android standard components and Fragments. But all these components have dependencies.

In the Busso App, you’ve seen how an Activity or Fragment depends on other objects, which have a presenter, model or viewBinder role. If a component has a lifecycle, its dependencies do, too. Some objects need to live as long as the app and others only need to exist when a specific Fragment or Activity is visible.

As you can see, you still have work to do to improve the Busso App.

Fixing the Busso App

Open the Busso project from the starter folder in this chapter’s materials. The file structure for the project that is of interest right now is the one in Figure 11.2:

Figure 11.2 — Busso initial project structure
Xakagi 61.0 — Nijwi isegoef rlecizw xtyussiku

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

  fun inject(activity: SplashActivity)

  fun inject(activity: MainActivity)

  fun inject(fragment: BusStopFragment)

  fun inject(fragment: BusArrivalFragment)

  @Component.Factory
  interface Factory {

    fun create(@BindsInstance activity: Activity): AppComponent
  }
}

Fixing BussoEndpoint

The BussoEndpoint implementation is a good place to start optimizing the Busso App. It’s a typical example of a component that needs to be unique across the entire app. To recap, BussoEndpoint is an interface which abstracts the endpoint for the application.

Understanding the problem

Before you dive into the fix, take a moment to prove that, at the moment, Dagger creates a new instance every time it needs to inject an object with the BussoEndpoint type. Using the Android Studio feature in Figure 11.3, you see that two classes depend on BussoEndpoint:

Figure 11.3 — Find BussoEndpoint injections
Sunize 57.9 — Wofl FapgiEymmeelk arhestuowq

@Singleton
class BusStopListPresenterImpl @Inject constructor(
    private val navigator: Navigator,
    private val locationObservable: Observable<LocationEvent>,
    private val bussoEndpoint: BussoEndpoint
) : BasePresenter<View, BusStopListViewBinder>(),
    BusStopListPresenter {
  // HERE  	
  init {
    Log.d("BUSSOENDPOINT", "StopList: $bussoEndpoint")
  }
  // ...
}
class BusArrivalPresenterImpl @Inject constructor(
    private val bussoEndpoint: BussoEndpoint
) : BasePresenter<View, BusArrivalViewBinder>(),
    BusArrivalPresenter {
  // HERE    	
  init {
    Log.d("BUSSOENDPOINT", "Arrival: $bussoEndpoint")
  }
  // ...
}
D/BUSSOENDPOINT: StopList: retrofit2.Retrofit$1@68c7c92
D/BUSSOENDPOINT: Arrival: retrofit2.Retrofit$1@cb74e1b
D/BUSSOENDPOINT: Arrival: retrofit2.Retrofit$1@542346a
D/BUSSOENDPOINT: Arrival: retrofit2.Retrofit$1@dfeaf68

Using @Singleton

As you’ve learned, using @Singleton is the first solution to the multiple instances problem. In this specific case, however, something’s different: You can’t access the code of the class that’s bound to the BussoEndpoint interface because the Retrofit framework created it for you.

@Module
class NetworkModule {

  @Provides
  @Singleton // HERE
  fun provideBussoEndPoint(activity: Activity): BussoEndpoint {
     // ...
  }
}
D/BUSSOENDPOINT: StopList: retrofit2.Retrofit$1@dc70bea
D/BUSSOENDPOINT: Arrival: retrofit2.Retrofit$1@dc70bea
D/BUSSOENDPOINT: Arrival: retrofit2.Retrofit$1@dc70bea
D/BUSSOENDPOINT: Arrival: retrofit2.Retrofit$1@dc70bea

Defining an application scope

When objects live as long as the app does, they have an application scope.

Defining ApplicationModule

This @Module tells Dagger how to create the objects it needs for the application scope. You have an opportunity to improve the structure of your code by putting your new Dagger knowledge to work.

@Module
class NetworkModule {
  // 1
  @Provides
  @Singleton
  fun provideCache(application: Application): Cache =
      Cache(application.cacheDir, 100 * 1024L)// 100K
  // 2
  @Provides
  @Singleton
  fun provideHttpClient(cache: Cache): OkHttpClient =
      Builder()
          .cache(cache)
          .build()
  // 3
  @Provides
  @Singleton
  fun provideBussoEndPoint(httpClient: OkHttpClient): BussoEndpoint {
    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(httpClient)
        .build()
    return retrofit.create(BussoEndpoint::class.java)
  }
}
@Module
class LocationModule {
  // 1
  @Singleton
  @Provides
  fun provideLocationManager(application: Application): LocationManager =
      application.getSystemService(Context.LOCATION_SERVICE) as LocationManager
  // 2
  @Singleton
  @Provides
  fun providePermissionChecker(application: Application): GeoLocationPermissionChecker =
      GeoLocationPermissionCheckerImpl(application)
  // 3
  @Provides
  fun provideLocationObservable(
      locationManager: LocationManager,
      permissionChecker: GeoLocationPermissionChecker
  ): Observable<LocationEvent> = provideRxLocationObservable(locationManager, permissionChecker)
}
@Module(includes = [
  LocationModule::class,
  NetworkModule::class
])
object ApplicationModule

Defining ApplicationComponent

Now, you need to define the @Component for the objects with application scope. The main thing to note here is that NetworkModule and LocationModule need an Application, which is an object you don’t have to create. You provide it instead.

@Component(modules = [ApplicationModule::class]) // 1
@Singleton // 2
interface ApplicationComponent {

  @Component.Factory
  interface Builder {

    fun create(@BindsInstance application: Application): ApplicationComponent // 3
  }
}
@Component(modules = [ApplicationModule::class])
@Singleton
interface ApplicationComponent {
  // 1
  fun locationObservable(): Observable<LocationEvent>
  // 2
  fun bussoEndpoint(): BussoEndpoint

  @Component.Factory
  interface Builder {

    fun create(@BindsInstance application: Application): ApplicationComponent
  }
}

The Main component

As you learned in the first section of this book, Main is the object that kicks off the creation of the dependency graph. In this case, it must be an object where you create the instance of ApplicationComponent, which keeps it alive as long as the app is. In Android, this is easy because you just need to:

// 1
class Main : Application() {
  // 2
  lateinit var appComponent: ApplicationComponent

  override fun onCreate() {
    super.onCreate()
    // 3
    appComponent = DaggerApplicationComponent
        .factory()
        .create(this)
  }
}
// 4
val Context.appComp: ApplicationComponent
  get() = (applicationContext as Main).appComponent
<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
  xmlns:tools="http://schemas.android.com/tools"
  package="com.raywenderlich.android.busso">

  <application ...
    android:name=".Main"> // HERE
    // ...
  </application>
</manifest>

Creating a custom @Scope

In the previous section, you implemented all the code you need to manage objects with application scope. These are objects that need to live as long as the app does. You managed them with a @Component that you created by using the Application you got from Main.

Creating @ActivityScope

As you read in the previous chapters, @Singleton is nothing special. It doesn’t say that an object is a Singleton, it just tells Dagger to bind the lifecycle of the object to the lifestyle of a @Component. For this reason, it’s a good practice to define a custom scope using the @Scope annotation.

@Scope // 1
@MustBeDocumented // 2
@Retention(RUNTIME) // 3
annotation class ActivityScope // 4

Creating the ActivityModule

Now, you need to define a @Module for the objects with an @ActivityScope. In this case, those objects are implementation for:

@Module(includes = [ActivityModule.Bindings::class])
class ActivityModule {

  @Module
  interface Bindings {
    @Binds
    fun bindSplashPresenter(impl: SplashPresenterImpl): SplashPresenter

    @Binds
    fun bindSplashViewBinder(impl: SplashViewBinderImpl): SplashViewBinder

    @Binds
    fun bindMainPresenter(impl: MainPresenterImpl): MainPresenter
  }

  @Provides
  @ActivityScope
  fun provideNavigator(activity: Activity): Navigator = NavigatorImpl(activity)
}

Creating the ActivityComponent

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

@Component(
    modules = [ActivityModule::class] // 1
)
@ActivityScope // 2
interface ActivityComponent {

  fun inject(activity: SplashActivity) // 3

  fun inject(activity: MainActivity) // 3

  fun navigator(): Navigator // 4

  @Component.Factory
  interface Factory {
    // 5
    fun create(@BindsInstance activity: Activity): ActivityComponent
  }
}
Figure 11.4 — SplashActivity dependencies
Vibeja 64.8 — TtxufmUzbuzeds cojurdusyaup

Managing @Component dependencies

The problem now is finding a way to share the objects in the ApplicationComponent dependency graph with the ones in ActivityComponent. Is this a problem similar to the one you saw with existing objects? What if you think of the Observable<LocationEvent> and BussoEndpoint as objects that already exist and that you can get from an ApplicationComponent? That’s exactly what you’re going to do now. Using this method, you just need to:

@Component(
    modules = [ActivityModule::class],
    dependencies = [ApplicationComponent::class] // 1
)
@ActivityScope
interface ActivityComponent {

  fun inject(activity: SplashActivity)

  fun inject(activity: MainActivity)

  fun navigator(): Navigator

  @Component.Factory
  interface Factory {
    fun create(
        @BindsInstance activity: Activity,
        applicationComponent: ApplicationComponent // 2
    ): ActivityComponent
  }
}

Using the ActivityComponent

In the previous paragraph, you changed the @Component.Factory definition for the ActivityComponent. Now, you need to change how to use it in Busso’s activities.

class SplashActivity : AppCompatActivity() {
  // ...
  override fun onCreate(savedInstanceState: Bundle?) {
    super.onCreate(savedInstanceState)
    makeFullScreen()
    setContentView(R.layout.activity_splash)
    DaggerActivityComponent.factory() // 1
        .create(this, this.application.appComp) // 2
        .inject(this) // 3
    splashViewBinder.init(this)
  }
  // ...
}
class MainActivity : AppCompatActivity() {

  @Inject
  lateinit var mainPresenter: MainPresenter

  lateinit var comp: ActivityComponent // 1

  override fun onCreate(savedInstanceState: Bundle?) {
    super.onCreate(savedInstanceState)
    setContentView(R.layout.activity_main)
    comp = DaggerActivityComponent // 2
        .factory()
        .create(this, this.application.appComp)
        .apply {
          inject(this@MainActivity)
        }
    if (savedInstanceState == null) {
      mainPresenter.goToBusStopList()
    }
  }
}

val Context.activityComp: ActivityComponent // 3
  get() = (this as MainActivity).comp

Creating the @FragmentScope

Not all of Busso’s objects will have an application or activity scope. Most of them live as long as a Fragment, so they need a new scope: the FragmentScope. Knowing that, you just repeat the same process you followed for @Singleton and @ActivityScope.

@Scope
@MustBeDocumented
@Retention(RUNTIME)
annotation class FragmentScope
@Module
interface FragmentModule {

  @Binds
  fun bindBusStopListViewBinder(impl: BusStopListViewBinderImpl): BusStopListViewBinder

  @Binds
  fun bindBusStopListPresenter(impl: BusStopListPresenterImpl): BusStopListPresenter

  @Binds
  fun bindBusStopListViewBinderListener(impl: BusStopListPresenterImpl): BusStopListViewBinder.BusStopItemSelectedListener

  @Binds
  fun bindBusArrivalPresenter(impl: BusArrivalPresenterImpl): BusArrivalPresenter

  @Binds
  fun bindBusArrivalViewBinder(impl: BusArrivalViewBinderImpl): BusArrivalViewBinder
}
@Component(
    modules = [FragmentModule::class],
    dependencies = [ActivityComponent::class, ApplicationComponent::class] // 1
)
@FragmentScope // 2
interface FragmentComponent {

  fun inject(fragment: BusStopFragment) // 3

  fun inject(fragment: BusArrivalFragment) // 3

  @Component.Factory
  interface Factory {
    // 4
    fun create(
        applicationComponent: ApplicationComponent,
        activityComponent: ActivityComponent
    ): FragmentComponent
  }
}

Using the FragmentScope

In the previous paragraph, you added a new @Component.Builder that asks Dagger to generate a custom factory method for you. You now need to create the FragmentComponent instances in BusStopFragment and BusArrivalFragment. Before doing that, it’s important to open BusStopListPresenterImpl.kt in the ui.view.busstop package and apply the following change:

@FragmentScope // HERE
class BusStopListPresenterImpl @Inject constructor(
    private val navigator: Navigator,
    private val locationObservable: Observable<LocationEvent>,
    private val bussoEndpoint: BussoEndpoint
) : BasePresenter<View, BusStopListViewBinder>(),
    BusStopListPresenter {
  // ...
}
class BusStopFragment : Fragment() {
  // ...
  override fun onAttach(context: Context) {
    with(context) {
      DaggerFragmentComponent.factory()
          .create(applicationContext.appComp, activityComp) // HERE
          .inject(this@BusStopFragment)
    }
    super.onAttach(context)
  }
  // ...
}
class BusArrivalFragment : Fragment() {
  // ...
  override fun onAttach(context: Context) {
    with(context) {
      DaggerFragmentComponent.factory()
          .create(applicationContext.appComp, activityComp) // HERE
          .inject(this@BusArrivalFragment)
    }
    super.onAttach(context)
  }
  // ...
}

Key points

  • There’s no component without a container that’s responsible for its lifecycle.
  • The Android environment is the container for the Android standard components and manages their lifecycle according to the resources available.
  • Fragments are not standard components, but they have a lifecycle.
  • Android components have dependencies with lifecycles.
  • @Scopes let you bind the lifecycle of an object to the lifecycle of a @Component.
  • @Singleton is a @Scope like any other.
  • A @Singleton is not a Singleton.
  • You can implement @Component dependencies by using the dependencies @Component attribute and providing explicit factory methods for the object you want to export to other @Components.
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 reading for free, with parts of this chapter shown as scrambled text. Unlock this book, and our entire catalogue of books and videos, with a Kodeco Personal Plan.

Unlock now