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
text.You can unlock the rest of this book, and our entire catalogue of books and videos, with a raywenderlich.com Professional subscription.
In the previous chapter, you migrated the Busso App from a homemade framework with ServiceLocator
s and Injector
s to Dagger. You converted Injector
s into @Component
s and ServiceLocator
s into @Module
s, 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 typeBusStopListPresenter
or aBusArrivalPresenter
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 ofBusStopListPresenterImpl
that are bound to theBusStopListPresenter
andBusStopListViewBinder.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 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 propertyprop
of typeT
if it has two methods —getProp(): T
andsetProp(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:
Activity
Service
ContentProvider
BroadcastReceiver
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
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 theActivity
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.
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 Fragment
s. 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:
@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
:
@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
}
}
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.
-
Fragment
s are not standard components, but they have a lifecycle. - Android components have dependencies with lifecycles.
-
@Scope
s 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@Component
s.