10.
Understanding Components
Written by Massimo Carli
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 andInjectors to Dagger’s equivalent@Modules and@Components. - Provide existing objects with a customized Builder for the
@Componentusing@Component.Builder. - Use
@Component.Factoryas 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
Injectorin 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
Injectorimplementations, delegating the actual injection of the dependent objects to the code Dagger generates for you from a@Component. - Replace the
ServiceLocatorimplementations 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.
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"
}
As you learned in the previous chapters, you add Dagger support to the Busso App by:
- Enabling the annotation processor plugin,
kotlin-kapt. - Adding dependencies to the Dagger library. The library’s version is already included in versions.gradle in the root folder of the project.
Select File ▸ Sync Project with Gradle Files to update the Gradle configuration in Android Studio. You can also select the icon shown in Figure 10.2, which appears as soon as you update some Gradle files:
Now, you’re ready to migrate Busso to Dagger.
Studying the dependency graph
In the previous examples, you learned that a:
-
@Componentdescribes which objects in the dependency graph you can inject, along with all their dependencies. -
@Moduletells Dagger how to create the objects in the dependency graph.
With these definitions in mind, you know that you need to migrate the existing Injectors to @Components and ServiceLocators to @Modules. Start the migration from the SplashActivity using the dependency graph, as shown in Figure 10.3:
The dependency diagram in Figure 10.3 shows that:
-
SplashActivitydepends onSplashViewBinderandSplashPresenter. -
SplashViewBinderImplis the class to create when you need an object of typeSplashViewBinder. - For the
SplashPresentertype, you useSplashPresenterImpl. -
SplashViewBinderImpldepends on aNavigator. -
NavigatorImplis the class to use for theNavigatortype. -
SplashPresenterImpldepends on anObservable<LocationEvent>implementation. - In the diagram,
ObservableImpljust represents theObservable<LocationEvent>implementation you’ll get fromprovideRxLocationObservable(). -
NavigatorImpldepends on theActivity. - Also, the
Observable<LocationEvent>implementation needs anActivity.
To migrate the Busso App to Dagger, you need to describe the dependency diagram in Figure 10.3 using Dagger annotations.
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)
}
}
This code is familiar by now. Think about what it does and what the responsibilities of @Components and @Modules are. You can see that:
- The
SplashActivityInjectordefinesinject()with a parameter of typeSplashActivity, which is the target of the injection. Note that a Dagger@Componentcan do the same thing. - Now, you get a reference to the
ActivityServiceLocator, which is the object that knows how to get the instances of theSplashViewBinderandSplashPresenterimplementations. A@Modulehas this responsibility in Dagger. - Here, you assign
SplashPresenter’s andSplashViewBinder’s references to the related properties in theSplashActivity.
This tells you that to migrate the SplashActivityInjector class to Dagger, you need to:
- Define a
@Modulethat tells Dagger how to get theSplashViewBinderandSplashPresenterimplementations. - Define a
@Componentusing the previous@Module, which definesinject()forSplashActivity. - Use the
@ComponentinSplashActivity.
It’s time to put this theory into code.
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
}
}
This code should be quite familiar. In it, you:
- Create a new module named AppModule as a class. You’ll understand very soon why it’s a class. Here, you also include the
Bindingsmodule that’s defined in the same file. You’ve seen this pattern in previous chapters. - Define the
Bindingsinterface, because you need some abstract functions that aren’t possible in a concrete class. - Use
@Bindsto bindSplashPresenterImpltoSplashPresenter. - Do the same for
SplashViewBinderand its implementation,SplashViewBinderImpl.
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 {
// ...
}
SplashPresenterImpl uses constructor injection and needs a reference to an implementation of Observable<LocationEvent>. Here, you just need to use @Inject like this:
class SplashPresenterImpl @Inject constructor( // HERE
private val locationObservable: Observable<LocationEvent>
) : BasePresenter<SplashActivity, SplashViewBinder>(), SplashPresenter {
Dagger now knows that when you need an object of type SplashPresenter, it has to create an instance of SplashPresenterImpl using the primary constructor, and that constructor needs an object of type Observable<LocationEvent>. How can you tell Dagger how to get what it needs? Simple, just add that information in the @Module.
Go back to AppModule.kt and apply the following changes to tell Dagger how to get a reference to an object of type Observable<LocationEvent>:
@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)
}
}
In this code, you:
- Add a constructor parameter of type
Activity. You already learned why you need that constructor in the last chapter, where you treatedContextthe same way. - Define
provideLocationObservable(), which is a@Providesmethod responsible for providing theObservable<LocationEvent>implementation. - Use the
activityyou got from theAppModuleprimary constructor as a parameter to get the reference toLocationManager. - Use
activityagain to create an instance ofGeoLocationPermissionCheckerImpl. - Use the
LocationManagerandGeoLocationPermissionCheckerImplto invokeprovideRxLocationObservable().
Now, Dagger has all the information it needs to create an instance of SplashPresenterImpl. But you still need to deal with SplashViewBinderImpl.
Handling SplashViewBinderImpl
Open SplashViewBinderImpl.kt from ui.splash and look at the class’ header:
class SplashViewBinderImpl( // HERE
private val navigator: Navigator
) : SplashViewBinder {
// ...
}
In this case, you need to do two things:
- Annotate the primary constructor with
@Inject. - Tell Dagger how to get an object of type
Navigator.
Start by adding @Inject to the SplashViewBinderImpl primary constructor, like this:
class SplashViewBinderImpl @Inject constructor( // HERE
private val navigator: Navigator
) : SplashViewBinder {
// ...
}
Then, open AppModule.kt and add the following definition:
@Module(includes = [AppModule.Bindings::class])
class AppModule(
private val activity: Activity
) {
// ...
@Provides
fun provideNavigator(): Navigator = NavigatorImpl(activity) // HERE
}
Here, you create an instance of NavigatorImpl using the Activity you got from the primary constructor.
Great! It’s been a long journey, but Dagger now has all the information about the objects it needs to implement SplashActivity. It’s time to implement the @Component and get rid of SplashInjector.
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.
Create a new file named AppComponent.kt in the di package and add the following content:
// 1
@Component(modules = [AppModule::class])
interface AppComponent {
// 2
fun inject(activity: SplashActivity)
}
Again, this should look familiar. In this code, you define:
- An
AppComponentinterface annotated with@Component. It’s important to note that you’re usingAppModuleas a value for themodulesattribute. This tells Dagger to use what’s inAppModuleto create the objects it needs. -
inject()as the function that injects all the dependenciesSplashActivityneeds.
This @Component contains the same information you previously had in SplashActivityInjector.
Now, open SplashActivity.kt and apply the following changes:
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)
}
// ...
}
Note: Dagger generates
DaggerAppComponentwhen you build the app. This happens even if the configuration isn’t complete, unless some big mistake prevents it. If you don’t seeDaggerAppComponent, try to build the app first.
In the previous code, you:
- Use
@Injectto tell Dagger thatSplashActivityneeds a reference to an object of typeSplashViewBinderin thesplashViewBinderproperty. - Do the same for the object of type
SplashPresenterfor thesplashPresenterproperty. - Use
builder()to get the reference to the Builder Dagger has created for you, for the implementation of theAppComponentinterface. - Pass an instance of
AppModuleto theAppComponentBuilder implementation. It needs this to create theNavigatorandObservable<LocationEvent>implementations. - Create the
AppComponentimplementation instance, invokingbuild()on the Builder. - Invoke
inject()on theSplashActivityfor the actual injection.
Before continuing, you need to:
- Note that you removed the existing
inject()invocation on theonCreate()inSplashActivity. - Completely delete SplashActivityInjector.kt from the di.injectos package. You don’t need it anymore!
Congrats! You completed the first step in migrating Busso to Dagger: making SplashActivity use AppComponent to inject all its dependencies. Build and run now, and everything works as expected:
In a real app, you’d need to repeat the same process for MainActivity, BusStopFragment and BusArrivalFragment, getting rid of all the Injector and ServiceLocator implementations.
For this chapter, however, you have two options. You can code along and complete the migration of Busso to Dagger, giving you some valuable practice, or you can simply check the project in the final folder from the downloaded materials for this chapter. In that folder, all the work has been done for you.
If you want to take the second option, jump ahead to the Customizing @Component creation section. In that case, see you there. Otherwise, continue on.
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.
The classes you need to migrate are:
MainActivityBusStopFragmentBusArrivalFragment
You’re about to delete a load of code. Buckle up!
Note: It’s important to note that the following migration is not the best. You still need to manage different
@Components for different@Scopes, which isn’t ideal. Don’t worry, you’ll fix this in the next chapter.
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
}
}
Note: It’s curious how the
MainActivityhas aPresenterbut noViewBinder. All it needs to do is to display aFragment, while the navigation responsibility is something you usually assign to thePresenter.
This code tells you that MainActivity depends on the MainPresenter abstraction. This is just one thing you can see from the complete dependency graph in Figure 10.5:
The dependency diagram above contains all the information Dagger needs to define the dependency graph. This diagram tells you that:
-
MainActivitydepends on theMainPresenterabstraction. -
MainPresenterImplis the class to instantiate for theMainPresentertype. -
MainPresenterImpldepends on theNavigationabstraction. -
NavigatorImplis the class to use as theNavigatorimplementation. -
NavigatorImpldepends onActivity.
Note that Dagger knows some of this information already, like how to bind NavigatorImpl to Navigator. What Dagger doesn’t know is how to bind MainPresenterImpl to MainPresenter.
Open AppModule.kt and add the following definition to the Bindings interface:
@Module(includes = [AppModule.Bindings::class])
class AppModule(
private val activity: Activity
) {
@Module
interface Bindings {
// ...
@Binds
fun bindMainPresenter(impl: MainPresenterImpl): MainPresenter // HERE
}
// ...
}
Here, you use @Binds to bind MainPresenterImpl to MainPresenter. Now, you need to tell Dagger how to create the instance of MainPresenterImpl. Open MainPresenterImpl.kt from ui.view.main and apply the following, now obvious, change:
class MainPresenterImpl @Inject constructor( // HERE
private val navigator: Navigator
) : MainPresenter {
override fun goToBusStopList() {
navigator.navigateTo(FragmentDestination(BusStopFragment(), R.id.anchor_point))
}
}
You use @Inject to tell Dagger that it needs to invoke the primary constructor to create an instance of MainPresenterImpl. It already knows how to provide a Navigator implementation.
Now, you need to define inject() for the MainActivity in the AppComponent. Open AppComponent.kt from di and add the following definition:
@Component(modules = [AppModule::class])
interface AppComponent {
// ...
fun inject(activity: MainActivity) // HERE
}
It’s very important to note that the parameter type matters. The parameter must match the target’s type for the injections. In short, you can’t use a parameter of type Activity, which would include both MainActivity and SplashActivity. Remember that you’re giving Dagger information, you’re not writing actual code; Dagger creates the code for you. Again, inject()’s name doesn’t matter, it’s just a convention.
For your last step, open MainActivity.kt from ui.view.main and apply the following changes:
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()
}
}
}
As you did for SplashActivity, you:
- Use
@Injectto tell Dagger you want the reference to theMainPresenterimplementation in themainPresenterproperty. - Create the
AppComponentimplementation using the Builder Dagger generated for you. - Invoke
inject()to run the actual injection on theMainActivity.
Now, build and run and check that everything works. Then, delete MainActivityInjector.kt, since you don’t it need anymore.
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:
BussoEndpointObservable<LocationEvent>
These are objects you manage at the Activity level. At this point, you need to:
- Make the
AppComponentavailable to theFragments. - Fix the missing dependency by creating a
@Modulefor theBussoEndpoint. - Inject the dependencies you need into the
Fragments.
You’ll start by exposing AppComponent.
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.
Open MainActivity.kt and apply the following changes:
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
In this code, you:
- Add the
compproperty of typeAppComponent. This is the reference to theAppComponentinstance you create forMainActivity. - Create the
AppComponentimplementation using the Builder Dagger generated for you, then save its reference in thecompproperty. - Define
compas an extension property for theContexttype. If theContextreceiver IS-AMainActivity, it’s the reference to theAppComponentinstance. Otherwise, it’snull.
Now, you just need to tell Dagger how to create an implementation for the BussoEndpoint type and then migrate the two Fragments.
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)
}
}
This code is very similar to the one in BussoEndpoint.kt in the same package. In that case, the signature has Context as a parameter.
fun provideBussoEndPoint(context: Context): BussoEndpoint {
// ...
}
In NetworkModule.kt, the same function has no parameter because it uses the Context the NetworkModule receives in its primary constructor.
@Module
class NetworkModule(val context: Context) { // HERE
@Provides
fun provideBussoEndPoint(): BussoEndpoint {
// ...
}
}
You can now remove provideBussoEndPoint() from BussoEndpoint.kt. Then add NetworkModule to the values for modules @Component attribute. Open AppComponent.kt and apply the following:
@Component(modules = [AppModule::class, NetworkModule::class]) // HERE
interface AppComponent {
// ...
}
This last change has some consequences. You’re telling Dagger that the AppComponent implementation needs a NetworkModule. This means that Dagger will generate a function in the builder for it.
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()
}
}
}
// ...
Note: Remember to build the app if
networkModule()is not available. Dagger needs to generate it from the previous configuration.
Here, you create an instance of NetworkModule, passing a reference to the MainActivity that IS-A Context. The bad news is that you have to do the same in SplashActivity.kt, which should then look like this:
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)
}
// ...
}
Note: Yeah. There’s a lot of repetition here. Don’t worry, you’ll get rid of all of this very soon.
At this point, the BussoEndpoint implementation is part of the dependency graph and you have everything you need to quickly complete the migration.
Handling deprecated @Modules
Before proceeding, it’s worth mentioning that the editor might display something like you see in Figure 10.6:
Here, Android Studio marks networkModule() as deprecated. That’s because you’re not using the bindings in the NetworkModule — because you haven’t yet completed the migration for the Fragments. You can just ignore this warning for now.
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
}
// ...
}
It’s important to note that you’re not only binding BusStopListPresenterImplto the BusStopListPresenter type, but also to BusStopListViewBinder.BusStopItemSelectedListener. You have to be careful that Dagger uses the same instance for the two bindings.
Note: You had the same problem with the RaySequence app, but you solved it using
@Singleton.
Now, you need to use @Inject the primary constructor for BusStopListPresenterImpl and BusStopListViewBinderImpl .
Open BusStopListPresenterImpl.kt and add the following:
@Singleton // 1
class BusStopListPresenterImpl @Inject constructor( // 2
private val navigator: Navigator,
private val locationObservable: Observable<LocationEvent>,
private val bussoEndpoint: BussoEndpoint
) : BasePresenter<View, BusStopListViewBinder>(),
BusStopListPresenter {
// ...
}
Note that Dagger knows all about the primary constructor parameter types. Remember to use @Singleton to ensure you’re using the same instance of BusStopListPresenterImpl for BusStopListPresenter and for BusStopListViewBinder.BusStopItemSelectedListener.
Finally, do the same in BusStopListViewBinderImpl.kt:
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
}
Here, you’re:
- Using
@Singletonbecause ofBusStopListPresenterImpl. As you read earlier, you need this to bind the lifecycle ofBusStopListPresenterImpltoAppComponent’s lifecycle. - Adding
inject()for theBusStopFragment.
Then, open BusStopFragment.kt and apply the following changes:
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)
}
// ...
}
Here, you use:
-
@Injectfor thebusStopListViewBinderandbusStopListPresenterproperties. - The extended property,
comp, to access theAppComponentimplementation in theMainActivityand then to invoke theinject().
Now, repeat the same process for BusArrivalFragment.
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
}
// ...
}
Now, open BusArrivalPresenterImpl.kt and add @Inject, like this:
class BusArrivalPresenterImpl @Inject constructor( // HERE
private val bussoEndpoint: BussoEndpoint
) : BasePresenter<View, BusArrivalViewBinder>(),
BusArrivalPresenter {
// ...
}
Do the same for BusArrivalViewBinderImpl.kt, like this:
class BusArrivalViewBinderImpl @Inject constructor() : BusArrivalViewBinder { // HERE
// ...
}
Again, open AppComponent.kt and add the following definition:
@Component(modules = [AppModule::class, NetworkModule::class])
interface AppComponent {
// ...
fun inject(fragment: BusArrivalFragment) // HERE
}
Finally, open BusArrivalFragment and apply the following changes:
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)
}
// ...
}
Here, you follow the same approach you used for BusStopFragment. Build the project now. You’ll see some compilation errors, but only because you need some clean-up. :]
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:
- injectors package
- locators package
- Main.kt file in the app’s main package and its reference in AndroidManifest.xml
-
ServiceLocatorImplTestin the di.locators package in thetestbuild type
After this, the directory structure for the project will match Figure 10.7.
Now, you can finally build and run — and the app will work as expected.
Great job! You’ve completely migrated Busso to Dagger.
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
) {
// ...
}
This method lets you use the existing object — activity, in this case — in any @Provides function in the same AppModule.kt. That’s because activity acts like a normal property of AppModule.
Then, you need to explicitly create the @Module instance and use it during the building process for the @Component, as you did in SplashActivity.kt:
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)
}
// ...
}
Here, you also had to create the NetworkModule for the same purpose: to provide an implementation of Context. It would be nice if you could pass the reference to an existing object to @Component by adding it directly to the dependency graph. That would make the provided object accessible to any other objects that depend on it. Fortunately, you can actually do that by using @Component.Builder and @Component.Factory.
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
}
}
These few lines of code are very important. In the AppComponent interface, you:
- Add
Builderas an internal interface annotated with@Component.Builder. That tells Dagger you want to customize the code it’ll generate asBuilderof theAppComponentimplementation. - Define a function that accepts a unique parameter of the type you want to provide. In this case, you define
activity()with a parameter of typeActivity. This function must return the same Builder because it’s going to generate one of the methods you must invoke to pass the existing object to the@Component. To make that object available to the dependency graph, you must annotate the function with@BindsInstance. In short, here you tell Dagger that yourAppComponentneeds anActivityand that you’re providing its reference by invokingactivity()on theBuilderimplementation Dagger generates for you. - Must have a unique function with no parameters, returning a reference to the
AppComponent. The name of the function doesn’t matter.
If you don’t use the @BindsInstance annotation, you’ll get the not-so-clear message: error: @Component.Builder has setters for modules or components that aren’t required.
Build the app now and you’ll get some compilation errors. This happens because you told Dagger that you want to provide a custom Builder for the AppComponent implementation that ignores AppModule and NetworkModule.
One solution is to implement the exact same Builder Dagger would. Just replace the previous Builder implementation with the following:
@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
}
}
Now, you can successfully build and run because the custom Builder implementation you configured is exactly the same as the one Dagger would have created without the @Component.Builder. The appModule() and networkModule functions have the same name.
Note: It’s important to know that it’s not mutually exclusive to pass the reference to both a
@Moduleand an existing object. You could have both in your Builder. Also note that, in this case, you don’t need to use@BindsInstance.
Of course this isn’t what you want. Restore the previous version of Builder in AppComponent.kt and apply the following changes to AppModule.kt:
@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)
}
}
In this code, you:
- Remove the
activityconstructor parameter of theAppModuleclass. - Pass the
Activityas the parameter forprovideNavigator(). - Do the same for
provideLocationObservable().
You basically treat the Activity type as something that’s already part of the dependency graph for the @Component.
Now, open NetworkModule.kt and make the same change, like this:
@Module
class NetworkModule { // 1
@Provides // 2
fun provideBussoEndPoint(activity: Activity): BussoEndpoint {
val cache = Cache(activity.cacheDir, CACHE_SIZE)
// ...
}
}
In this code, you:
- Remove the constructor parameter of type
Context. - Use a parameter of type
ActivityinprovideBussoEndPoint().
Build the app now, and you’ll still get some compilation errors because the Builder implementation for the AppComponent has changed. To fix this, open MainActivity.kt and apply the following changes:
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()
}
}
}
// ...
In the code above, you simply use activity() to pass the reference to the existing object to the dependency graph. Because of this, NetworkModule also gets the reference to the same object.
Before you build and run, you also need to apply the same change to SplashActivity.kt, like this:
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)
}
// ...
}
Now you can finally build and run the app successfully.
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.
Dagger also allows you to implement a factory as a way to generate a factory method. You can use this to create an instance of the @Component that invokes a simple function to pass all the parameters at the same time.
Open AppComponent.kt and replace the @Component.Builder definition with the following:
@Component(modules = [AppModule::class, NetworkModule::class])
interface AppComponent {
// ...
@Component.Factory // 1
interface Factory {
// 2
fun create(@BindsInstance activity: Activity): AppComponent
}
}
In this case, you define:
- The
Factoryinterface and annotate it with@Component.Factory. - A
create()function with a parameter of typeActivity.create()can have any parameters you need, but it must return an object with the same type as the@Component. In this case, the return type isAppComponent.@BindsInstancehere has the same meaning you learned in the previous paragraph: If you use it for a parameter, the provided value becomes part of the dependency graph and is available for injection.
Build and run the project and you’ll get some compilation errors. That’s because Dagger generates different code now. To fix this, open SplashActivity.kt and apply the following changes:
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)
}
// ...
}
In this code, you invoke:
-
factory()to get the reference to the Factory for theAppComponentimplementation. -
create(), passing theActivityit needs to create theAppComponentimplementation. -
inject()for the actual injection.
Of course, you need to do the same for MainActivity as well, applying the same changes:
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()
}
}
}
// ...
Note that @Component.Builder and @Component.Factory are basically two different ways of doing the same thing. A rule of thumb about which one to use involves the number of objects you need to provide for the actual construction of the @Component implementation.
In both cases, you can pass one of these objects as optional by using @Nullable as its parameter. @Component.Factory allows you to write less code, passing all the parameters in one call, while @Component.Builder is a little bit more verbose.
Great job! In this chapter, you finally migrated the Busso App from your homemade framework to Dagger. This long, and occasionally repetitive process reduced the number of lines of code in your project. Well, at least the lines of code you wrote.
The Dagger configuration you used in this chapter is still not optimal, however. To use your resources more efficiently, some components should have different lifecycles than others. While the BussoEndpoint should live as long as the app, the Navigator lifecycle should be bound to the Activity lifecycle.
In the next chapter, you’ll make more important changes. See you there!
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 andServiceLocators to Dagger@Modules. - A dependency diagram helps you migrate an existing app to Dagger.
-
@Component.Builderlets 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@Componentimplementation.