11.
Components & Scopes
Written by Massimo Carli
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
BussoEndpointevery time you need a reference to an object of typeBusStopListPresenteror aBusArrivalPresenterthat depends on it. Instead, you should have just one instance of the endpoint, which lives as long as the app does. - Use the
@Singletonannotation to solve the problem of multiple instances ofBusStopListPresenterImplthat are bound to theBusStopListPresenterandBusStopListViewBinder.BusStopItemSelectedListenerabstractions. However,@Singletonisn’t always a good solution, and you should understand when to use it and when not to. - Get the reference to
LocationManagerfrom anActivity, but it should have a broader lifecycle, like the app does, and it should also depend on the appContext.
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 propertypropof typeTif it has two methods —getProp(): TandsetProp(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:
- The container is responsible for the lifecycle of the components it contains.
- There is always a way to describe the component to the container.
- 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:
ActivityServiceContentProviderBroadcastReceiver
In this case, the following applies to the components:
- The Android environment is the container that manages the lifecycle of standard components according to the available system resources and user actions.
- You describe these components to the Android Environment using AndroidManifest.xml.
- 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
Fragmenta 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 theActivitythat 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.
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:
You can see that this structure has:
- A single
@Componentin AppComponent.kt under the di package. - Two different
@Modules, one in AppModule.kt in that di package with the@Componentand the other in NetworkModule.kt in the network package.
Open AppComponent.kt and look at the current implementation:
@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
}
}
The AppComponent interface is responsible for the entire app’s dependency graph. You have inject() overloads for all the Fragment and Activity components and you’re using all the @Modules. That means all the app’s objects are part of the same graph. This has important implications for scope.
So far, you learned that:
- By default, Dagger creates a new instance of the class that’s bound to a specific abstraction type every time an injection of that type occurs.
- Using
@Singleton, you can only bind the lifecycle of an instance to the lifecycle of the@Componentyou use to get its reference.
In other words, a @Singleton IS NOT a Singleton!
Using @Singleton doesn’t give you a unique instance of a class across the entire app. It just means that each instance of a @Component always returns the same instance for an object of a given type. Different @Component instances will return different instances for the same type, even if you use @Singleton.
As you’ll see later, if you want an instance of an object that lives as long as the app does, you need a @Component that matches the app’s lifespan.
Note: Singleton is a foundational Gang Of Four Design Pattern that describes a way to have one and only one instance of a class, which you can access from any part of the app’s code. Many developers consider it an anti-pattern because of the concurrency issues that can arise, especially in a distributed environment.
To better understand this concept, your next step will be to fix the BussoEndpoint in the Busso app.
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:
Those are:
BusStopListPresenterImplBusArrivalPresenterImpl
Open BusStopListPresenterImpl.kt in ui.view.busstop and add the following init block:
@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")
}
// ...
}
Now, open BusArrivalPresenterImpl.kt in ui.view.busarrival and do the same thing:
class BusArrivalPresenterImpl @Inject constructor(
private val bussoEndpoint: BussoEndpoint
) : BasePresenter<View, BusArrivalViewBinder>(),
BusArrivalPresenter {
// HERE
init {
Log.d("BUSSOENDPOINT", "Arrival: $bussoEndpoint")
}
// ...
}
That just added some logs that print information about the specific BussoEndpoint instance. Build and run, then navigate back and forth a few times in the arrivals fragment, and you’ll get a log like this:
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
Now, you can clearly see that the BusStopListPresenterImpl and BusArrivalPresenterImpl objects are all using different instances for the BussoEndpoint type. It’s important to note that all the Fragments are using the same AppComponent instance through the comp extended property you defined in MainActivity.kt. You already know what to do to ensure that @Component always returns the same instance for the BussoEndpoint: Use @Singleton.
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.
However, using Dagger gives you a way to get around this problem. Open NetworkModule.kt in the network and apply the following change:
@Module
class NetworkModule {
@Provides
@Singleton // HERE
fun provideBussoEndPoint(activity: Activity): BussoEndpoint {
// ...
}
}
Using @Singleton in provideBussoEndPoint() achieves the same goal. You’re asking Dagger to create a single instance of the BussoEndpoint implementation. If you repeat the previous experiment with this new code, you’ll get the following output:
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
Now, the instance for the BussoEndpoint type is always the same. Everything seems to work, but this isn’t a good solution for the reason mentioned above. Using @Singleton didn’t give you a single instance for the BussoEndpoint implementation for the entire app.
With this configuration, you’re creating an instance for BussoEndpoint for each instance of AppComponent — but you have one AppComponent per Activity. In the app, you have two of them.
To create a single BussoEndpoint instance across the entire app, you need to define a @Component with the same lifespan. This is called an application scope. You’ll learn how to define and use application scopes next.
Defining an application scope
When objects live as long as the app does, they have an application scope.
You already know what to do to make Dagger provide a single instance for a given type across the entire app. You need to define a:
-
@Modulethat encapsulates the information about how to create the objects. -
@Componentas a factory for those objects. - Way to make the
@Componentlive as long the app, and make it accessible from any other point in the code.
Look at Busso’s code and you’ll see that there are three types of objects that have an application scope:
BussoEndpointLocationManagerGeoLocationPermissionChecker
You’ll start by defining the @Module.
Note: As you know, the
Observable<LocationEvent>depends on theLocationManagerandGeoLocationPermissionChecker, but it’s a good practice to create a new instance every time. You could create a single instance ofObservable<LocationEvent>to keep alive as long as the app, but you’d need to create ashareableversion of it. To learn more aboutRxJavaorRxKotlin, check out the book, Reactive Programming with Kotlin.
Note: As you improve the Dagger configuration, you won’t be able to successfully build and run after each step — you need to complete the refactoring for the app to run properly. But don’t worry, just code along and everything will be fine. :]
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.
You don’t want to have all the definitions in a single place, so your first step is to improve some of the existing files. Open NetworkModule.kt in the network and add the following:
@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)
}
}
This code gives you a better definition of the dependencies. In particular, you define:
-
provideCache(), which returns the reference to theCacheimplementation. It’s important to note that it has the same scope as the@Componentannotated with the@Singletonthat uses it. Another fundamental change is the input parameter forproviitsdeCache(): It’s now Application. -
provideHttpClient(), which returns theOkHttpClientthat receives theCacheimplementation as its parameter. You use@Singletonagain here. -
provideBussoEndPoint(), which returns the RetrofitBussoEndpointimplementation from theOkHttpClientit receives as a parameter. You want this to be a@Singletonas well.
Note: You might wonder if
CacheandOkHttpClientactually need to be@Singletons or not. In the app, you’re just consuming theBussoEndpointobject, which is the only one that needs to be@Singleton, so you could avoid the annotation on its dependencies. On the other hand, you should only have oneCacheand oneOkHttpClientin the app. Therefore, it’s preferable to use@Singletonon them as well.
Now, create a new file named LocationModule.kt in di and add the following code:
@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)
}
This refactoring is similar to the one you did before, but it’s a bit more important. In this code, you define:
-
provideLocationManager(), which returns theLocationManagerfrom theApplicationyou pass as a parameter. You only need a singleLocationManager, so you use@Singleton. -
providePermissionChecker(), which returns the implementation forGeoLocationPermissionCheckerusing theApplicationyou get as a parameter. You only need one, so you use@Singletonagain. -
provideLocationObservable(), which returns theObservable<LocationEvent>implementation using theLocationManagerandGeoLocationPermissionCheckeryou get as parameters. Because of the previous@Providesfunctions, Dagger will create a newObservable<LocationEvent>implementation from the sameLocationManagerandGeoLocationPermissionCheckerobjects it gets as input parameters.
Note that in LocationManager’s case, you don’t actually know if you need @Singleton or not. That’s because the LocationManager you get from the Context using getSystemService() might already be a Singleton in the Design Pattern sense.
Finally, you’ll merge the two modules into one as a convenience. Create a new file named ApplicationModule.kt in di and enter the following code:
@Module(includes = [
LocationModule::class,
NetworkModule::class
])
object ApplicationModule
This lets you keep all the @Modules for the application scope objects in the same place.
Note: Before implementing the
@Component, remember to delete theprovideLocationObservable()definition from AppModule.kt in di.
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.
You already know how to do that. Create a new file named ApplicationComponent.kt in di and enter the following code:
@Component(modules = [ApplicationModule::class]) // 1
@Singleton // 2
interface ApplicationComponent {
@Component.Factory
interface Builder {
fun create(@BindsInstance application: Application): ApplicationComponent // 3
}
}
This is a very simple but important definition. Here, you:
-
Require the information from
ApplicationModule. -
Use
@Singletonbecause you have bindings that should have the same lifespan as theApplicationComponent. -
Define a
@Component.Factorythat generates a factory method, which you’ll use to create theApplicationComponentinstance from the existingApplicationobject.
You don’t have any inject() functions in this code because you won’t use the ApplicationComponent directly. As you’ll see shortly, you just need a way to make these objects available to other @Components with different @Scopes.
For instance, both SplashActivity and BusStopListPresenterImpl need Observable<LocationEvent>, and they definitely don’t have application scope.
Note: You’ll learn all about @Component dependency in the next chapter. Here, you’ll use the dependencies attribute of the
@Componentannotation, which is also the approach originally implemented in Dagger.
To get the reference to an object in the dependency graph from a @Component, Dagger requires you to be explicit. Using inject()s, you ask Dagger to create the code to inject dependencies into a target object — but you don’t have direct access to the objects it injects.
To do that, Dagger asks you to provide factory methods for the objects you want to expose. To access Observable<LocationEvent> and BussoEndpoint directly from the ApplicationComponent, you need to add the following code to the @Component definition in ApplicationComponent.kt.
@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
}
}
Once you get the reference to ApplicationComponent’s instance, you just need to invoke:
-
locationObservable()to get a reference to theObservable<LocationEvent>implementation. -
bussoEndpoint()to get the reference to theBussoEndpoint.
These functions also make the Observable<LocationEvent> and BussoEndpoint available to other dependent @Components, as you’ll see shortly.
Now, you need to create a unique instance of ApplicationComponent and make it available across the entire app. In later chapters, you’ll learn another way for sharing objects between different @Components with different @Scopes called @Subcomponents. For now, you’ll use Main.
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:
- Create a class that extends Android’s
Application. - Create the
ApplicationComponentinstance and make it available from any point in the Busso app’s code. - Register this class in AndroidManifest.xml.
Start by creating a new file named Main.kt in the main package of the app with the following code:
// 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
Here, you:
- Create
Main, which extends the AndroidApplication. - Define
lateinit appComponent, which will contain a reference toApplicationComponent. - Invoke
create()on the Factory you get from the static factory method,factory(). This passes the reference to theApplicationitself and saves theApplicationComponentinstance in theappComponentproperty. - Define the
appCompextension property for theContexttype. If you try to access this property from aContextthat’s not usingMainas itsApplicationContext, you’ll get a crash. This is good because it lets you catch errors early in development.
To access DaggerApplicationComponent, you need to build the app. Don’t worry if the build fails, Dagger will be able to generate the code for the ApplicationComponent, anyway.
Now, open AndroidManifest.xml and apply the following change, adding the android:name attribute for the application element:
<?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>
You’ve now set up everything you need to manage objects with application scope. But how can you actually use them?
Check out SplashActivity and you see that it needs the reference to the SplashPresenter and SplashViewBinder implementations. SplashPresenterImpl needs the reference to Observable<LocationEvent>, but SplashViewBinderImpl needs a Navigator that depends on the Activity — and that’s not available at the Application level. That means you need another scope.
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.
You also realized that there are other objects, like Navigator, that need an Activity, instead. You need to bind the lifecycle of these objects to the lifecycle of the Activity they depend upon. Based on what you’ve learned so far, you should just follow these steps:
- Create the
@Modules for the objects that depend on theActivity. - Define a
@Componentthat uses those@Modules and has a@Component.Builderor@Component.Factorythat accepts the reference to theActivitythey need. - Use
@Singletonfor the objects you want to bind to theActivitylifecycle. - Create the instance of this
@Componentin theonCreate()of the specificActivityimplementation, as you did for the existingAppComponent.
This is exactly what you’re going to do, but with a small but very important difference: You’re going to create and use a new @Scope named @ActivityScope.
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.
To do this, create a new file named ActivityScope.kt in a new di.scopes package and add the following code:
@Scope // 1
@MustBeDocumented // 2
@Retention(RUNTIME) // 3
annotation class ActivityScope // 4
You already defined a custom annotation when you learned about custom qualifiers. This isn’t much different. Here, you:
- Use
@Scopeto mark this annotation as a way to define a scope. Like@Singleton,@Scopeis part of Java Specification Request 330. In@Singleton’s source code, you can see it has the same@Scopeannotation. - Use
@MustBeDocumentedto mark that a Java doc should be generated for this annotation. - Apply
RUNTIMEas a retention value. This means that you store the annotation in binary output where it’s visible for reflection. - Create a simple annotation class named
ActivityScope.
Now, you’re ready to use the @ActivityScope in the same way you used @Singleton. The only difference is the name.
Creating the ActivityModule
Now, you need to define a @Module for the objects with an @ActivityScope. In this case, those objects are implementation for:
NavigatorSplashPresenterSplashViewBinderMainPresenter
To do this, create a new file named ActivityModule.kt in di and enter the following code:
@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)
}
It’s important to note that you use @ActivityScope to tell Dagger it should always return the same instance of the Navigator implementation if you get the reference through a @Component that has @ActivityScope as one of the scopes it supports. At the moment, you don’t have any @Components like that — so it’s time to create one.
Remember to delete the same definitions from the existing AppModule.kt file.
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
}
}
This code should be familiar. Here, you:
- Create a
@Componentwith a reference to the@Modules it needs to create the objects for its dependency graph. - Use
@ActivityScopeto tell Dagger that the objects that use the same annotation have a lifecycle bound to anActivityComponent. - Define
inject()forSplashActivityandMainActivity. - Define
navigator()as the factory method forNavigator. This is also necessary to makeNavigatorvisible to its dependent@Component. - Use
@Component.Factoryto ask Dagger to generate a factory method that creates anActivityComponentfrom anActivity.
Now, take a quick look at the dependencies in Figure 11.4:
In this class diagram, you can see that SplashActivity depends on the SplashPresenter abstraction whose implementation is SplashPresenterImpl. This class depends on Observable<LocationEvent>. The problem is that SplashActivity has an activity scope while Observable<LocationEvent> has an application scope.
You need a way for ActivityComponent to access the object of the ApplicationComponent graph. You’ll tackle that problem next.
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:
- Tell Dagger that you need the objects from another
@Component. - Create a
@Component.Builderor@Component.Factorythat allows you to pass the reference of the dependent@Component.
Open ActivityComponent.kt, which you just created in di, and make the following changes:
@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
}
}
In this code, you:
- Use the
dependencies’@Componentattribute to tell Dagger which dependent@Components it needs to build its dependency graph. - Add a new
applicationComponentparameter of typeApplicationComponentto the static factory method for theActivityComponent. Note that you’re not using@BindsInstance. As you recall, you can accomplish this by using@Modules as parameters.
This is going to change the code Dagger generates for you. To see how, you’ll use this component in SplashActivity and MainActivity.
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.
Start by opening SplashActivity.kt and applying the following changes:
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)
}
// ...
}
In this code, you:
- Use
factory()to get the Factory implementation that Dagger created for you. - Invoke
create(), passing the reference to the currentActivityas the first parameter. As the second parameter, you pass the reference to theApplicationComponentthat you get by using theappCompextended property you defined in Main.kt. It’s fundamental to understand that you only have one instance ofappCompacross the entire app. - Inject the dependent objects using
inject()as usual.
Now, you can do the same for MainActivity. Open MainActivity.kt in the ui.view.main and apply the following changes:
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
In this code, you can see that:
- The
@Componenttype you want to retain isActivityComponent. - You create the instance of
DaggerActivityComponentas you did before — by savingActivityComponent’s reference in thecompproperty. - You implement the
activityCompextension property to access theActivityComponentfrom theFragmenttheMainActivitywill contain.
At this point, you’re using:
-
@Singletonin a@Componentfor the objects with application scope. -
@ActivityScopein a@Componentfor the objects with activity scope.
What about the Fragments? Well, at this point, you should know exactly what to do — create another custom scope.
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.
Create a new file named FragmentScope.kt in di.scopes with the following code:
@Scope
@MustBeDocumented
@Retention(RUNTIME)
annotation class FragmentScope
This defines the new @FragmentScope. The only way this code differs from @Singleton and @ActivityScope is in its name.
Now, create a new file named FragmentModule.kt in the di folder and add the following code:
@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
}
This is nothing new — you just define the bindings for the components you need in the Fragments of the Busso App. Note that this code contains the remaining definitions you had in AppModule.kt, so delete that file now.
Then, create a new file named FragmentComponent.kt in di with the following code:
@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
}
}
This code just contains the:
- Definition of the dependencies on the
ActivityComponentandApplicationComponent@Components. It’s important to note here that these dependencies are not transitive, so the dependency onApplicationComponentmust be explicit. -
@FragmentScopeannotation, because some bindings need to have a lifecycle bound to this@Component. - Definition of the
inject()methods for the actual injection of the dependencies inBusStopFragmentandBusArrivalFragment. - The
@Component.Factoryinterface that asks Dagger to generate a factory method that creates aFragmentComponentfrom the existingApplicationComponentandActivityComponent.
This allows you to finally delete AppComponent.kt in the di package.
Now, it’s time to use the FragmentComponent in Busso’s Fragments.
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 {
// ...
}
This binds the lifecycle of BusStopListPresenterImpl to FragmentComponent with the scope @FragmentScope — which means you have to replace @Singleton with it. Otherwise, you’d prevent Dagger from generating DaggerFragmentComponent, which you’ll use in your Fragments.
Next, open BusStopFragment.kt in ui.view.busstop and apply the following change:
class BusStopFragment : Fragment() {
// ...
override fun onAttach(context: Context) {
with(context) {
DaggerFragmentComponent.factory()
.create(applicationContext.appComp, activityComp) // HERE
.inject(this@BusStopFragment)
}
super.onAttach(context)
}
// ...
}
The only thing to mention here is that you use the references to ApplicationComponent and ActivityComponent as parameters of the factory method for the FragmentComponent.
Now, open BusStopFragment.kt in ui.view.busstop and apply the same change:
class BusArrivalFragment : Fragment() {
// ...
override fun onAttach(context: Context) {
with(context) {
DaggerFragmentComponent.factory()
.create(applicationContext.appComp, activityComp) // HERE
.inject(this@BusArrivalFragment)
}
super.onAttach(context)
}
// ...
}
Now, you can finally build and run Busso and check that everything works as expected.
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. -
@Singletonis a@Scopelike any other. - A
@Singletonis not a Singleton. - You can implement
@Componentdependencies by using the dependencies@Componentattribute and providing explicit factory methods for the object you want to export to other@Components.
Great job! This was another important chapter, and you managed to implement different @Scopes for Busso’s components. But you’re far from done! Dagger is evolving and there are many other important concepts to learn. See you in the next chapter.