4
Dependency Injection & Scopes
Written by Massimo Carli
In the previous chapter, you learned what dependency injection is and how to use it to improve the architecture of the Busso App. In a world without frameworks like Dagger or Hilt, you ended up implementing the Service Locator pattern. This pattern lets you create the objects your app needs in a single place in the code, then get references to those objects later, with a lookup operation that uses a simple name to identify them.
You then learned what dependency lookup is. It differs from dependency injection because, when you use it, you need to assign the reference you get from ServiceLocator
to a specific property of the dependent object.
Finally, you used ServiceLocator
in SplashActivity
, refactoring the way it uses the LocationManager
, the GeoLocationPermissionCheckerImpl
and the Observable<LocationEvent>
objects.
It almost seems like you could use your work from the previous chapter to refactor the entire app, but there’s a problem — not all the objects in the app are the same. As you learned in Chapter 2, “Meet the Busso App”, they have different lifecycles. Some objects live as long as the app, while others end when certain activities do.
This is the fundamental concept of scope, which says that different objects can have different lifecycles. You’ll see this many times throughout this book.
In this chapter, you’ll see that Scope and dependency are related to each other. You’ll start by refactoring how SplashActivity
uses Navigator
. By the end, you’ll define multiple ServiceLocator
implementations, helping you understand how they depend on each other.
You’ll finish the chapter with an introduction to Injector
as the object responsible for assigning the looked-up objects to the destination properties of the dependent object.
Now that you know where you’re heading, it’s time to get started!
Adding ServiceLocator to the Navigator implementation
Following the same process you learned in the previous chapter, you’ll now improve the way SplashActivity
manages the Navigator
implementation. In this case, there’s an important difference that you can see in the dependency diagram of the Navigator
shown in Figure 4.1:
In the dependency diagram, you see that NavigatorImpl
depends on Activity
, which IS-A Context
but also an abstraction of AppCompatActivity
. This is shown in the class diagram in Figure 4.2:
In this class diagram, note that:
-
Activity
extends theContext
abstract class. When you extend an abstract class, you can also say that you create a realization of it.Activity
is, therefore, a realization ofContext
. -
AppCompactActivity
extendsActivity
. -
SplashActivity
IS-AAppCompactActivity
and so IS-AActivity
. Thus, it also IS-AContext
. -
Application
IS-AContext
. -
Main
IS-AApplication
and so IS-AContext
. - Busso depends on the Android framework.
Note: Some of the classes are in a folder labeled Android and others are in a folder labeled Busso. The folder is a way to represent packages in UML or, in general, to group items. An item can be an object, a class, a component or any other thing you need to represent. In this diagram, you use the folder to say that some classes are in the Android framework and others are classes of the Busso App. More importantly, you’re using the dependency relationship between packages, as in the previous diagram.
The class diagram also explicitly says that Main
IS-NOT-A Activity
.
You can see the same in NavigatorImpl.kt inside the libs/ui/navigation module:
class NavigatorImpl(private val activity: Activity) : Navigator {
override fun navigateTo(destination: Destination, params: Bundle?) {
// ...
}
}
From Main
, you don’t have access to the Activity
. The Main
class IS-A Application
that IS-A Context
, but it’s not an Activity
. The lifecycle of an Application
is different from the Activity
’s.
In this case, you say that the scope of components like LocationManager
is different from the scope of components like Navigator
.
But how can you manage the injection of objects with different scopes?
Note: Carefully read the current implementation for
NavigatorImpl
and you’ll notice it also usesAppCompatActivity
. That means it depends onAppCompatActivity
, as well. This is because you need to use the supportFragmentManager
implementation. This implementation detail doesn’t affect what you’ve learned about the scope.
Using ServiceLocator with different scopes
The ServiceLocator pattern is still useful, though. In ServiceLocator.kt in the di package, add the following definition, just after the ServiceLocator
interface:
typealias ServiceLocatorFactory<A> = (A) -> ServiceLocator
// 1
const val NAVIGATOR = "Navigator"
// 2
val activityServiceLocatorFactory: ServiceLocatorFactory<AppCompatActivity> =
{ activity: AppCompatActivity -> ActivityServiceLocator(activity) }
class ActivityServiceLocator(
// 3
val activity: AppCompatActivity
) : ServiceLocator {
@Suppress("IMPLICIT_CAST_TO_ANY", "UNCHECKED_CAST")
override fun <A : Any> lookUp(name: String): A = when (name) {
// 4
NAVIGATOR -> NavigatorImpl(activity)
else -> throw IllegalArgumentException("No component lookup for the key: $name")
} as A
}
Accessing ActivityServiceLocator
As noted in the last paragraph, you can get the reference to ActivityServiceLocator
using the same Service Locator pattern.
const val LOCATION_OBSERVABLE = "LocationObservable"
// 1
const val ACTIVITY_LOCATOR_FACTORY = "ActivityLocatorFactory"
class ServiceLocatorImpl(
val context: Context
) : ServiceLocator {
private val locationManager =
context.getSystemService(Context.LOCATION_SERVICE) as LocationManager
private val geoLocationPermissionChecker = GeoLocationPermissionCheckerImpl(context)
private val locationObservable =
provideRxLocationObservable(locationManager, geoLocationPermissionChecker)
@Suppress("IMPLICIT_CAST_TO_ANY", "UNCHECKED_CAST")
override fun <A : Any> lookUp(name: String): A = when (name) {
LOCATION_OBSERVABLE -> locationObservable
// 2
ACTIVITY_LOCATOR_FACTORY -> activityServiceLocatorFactory
else -> throw IllegalArgumentException("No component lookup for the key: $name")
} as A
}
Using ActivityServiceLocator
For your last step, you need to use ActivityServiceLocator
. Open SplashActivity.kt and apply the following changes:
// ...
private val handler = Handler()
private val disposables = CompositeDisposable()
private lateinit var locationObservable: Observable<LocationEvent>
// 1
private lateinit var activityServiceLocator: ServiceLocator
private lateinit var navigator: Navigator
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
makeFullScreen()
setContentView(R.layout.activity_splash)
locationObservable = lookUp(LOCATION_OBSERVABLE)
// 2
activityServiceLocator =
lookUp<ServiceLocatorFactory<AppCompatActivity>>(ACTIVITY_LOCATOR_FACTORY)
.invoke(this)
// 3
navigator = activityServiceLocator.lookUp(NAVIGATOR)
}
// ...
Using multiple ServiceLocators
At this point, you’re using two different ServiceLocator
implementations in the SplashActivity
: one for the objects with application scope and one for the objects with activity scope. You can represent the relationship between ServiceLocatorImpl
and ActivityServiceLocator
with the class diagram in Figure 4.4:
ServiceLocator dependency
You can create a diagram to see the different objects within their scope, just as you did in Figure 2.14 of Chapter 2, “Meet the Busso App”. In this case, the result is the following:
// ...
// 1
locationObservable = lookUp(LOCATION_OBSERVABLE)
// 2
activityServiceLocator =
lookUp<ServiceLocatorFactory<AppCompatActivity>>(ACTIVITY_LOCATOR_FACTORY)
.invoke(this)
// 3
navigator = activityServiceLocator.lookUp(NAVIGATOR)
// ...
Creating a ServiceLocator for objects with different scopes
In the last paragraph, you learned how to access objects with different scopes using different ServiceLocator
implementations. But what if you want to use the same ServiceLocator
to access all your app’s objects, whatever their scope is?
// ...
class ActivityServiceLocator(
val activity: AppCompatActivity
) : ServiceLocator {
// 1
var applicationServiceLocator: ServiceLocator? = null
@Suppress("IMPLICIT_CAST_TO_ANY", "UNCHECKED_CAST")
override fun <A : Any> lookUp(name: String): A = when (name) {
NAVIGATOR -> NavigatorImpl(activity)
// 2
else -> applicationServiceLocator?.lookUp<A>(name)
?: throw IllegalArgumentException("No component lookup for the key: $name")
} as A
}
// 1
val activityServiceLocatorFactory: (ServiceLocator) -> ServiceLocatorFactory<AppCompatActivity> =
// 2
{ fallbackServiceLocator: ServiceLocator ->
// 3
{ activity: AppCompatActivity ->
ActivityServiceLocator(activity).apply {
applicationServiceLocator = fallbackServiceLocator
}
}
}
// ...
@Suppress("IMPLICIT_CAST_TO_ANY", "UNCHECKED_CAST")
override fun <A : Any> lookUp(name: String): A = when (name) {
LOCATION_OBSERVABLE -> locationObservable
ACTIVITY_LOCATOR_FACTORY -> activityServiceLocatorFactory(this) // HERE
else -> throw IllegalArgumentException("No component lookup for the key: $name")
} as A
// ...
Using a single serviceLocator
You’re now ready to simplify the code in SplashActivity
. Open SplashActivity.kt and change the implementation of onCreate()
to this:
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
makeFullScreen()
setContentView(R.layout.activity_splash)
activityServiceLocator =
lookUp<ServiceLocatorFactory<AppCompatActivity>>(ACTIVITY_LOCATOR_FACTORY)
.invoke(this)
// 1
locationObservable = activityServiceLocator.lookUp(LOCATION_OBSERVABLE)
// 2
navigator = activityServiceLocator.lookUp(NAVIGATOR)
}
Going back to injection
Dependency lookup is not exactly the same as dependency injection. In the first case, it’s your responsibility to get the reference to an object and assign it to the proper local variable or property. This is what you’ve done in the previous paragraphs. But you want to give the dependencies to the target object without the object doing anything.
The injector interface
Create a new file named Injector.kt in the di package and enter the following code:
interface Injector<A> {
fun inject(target: A)
}
class SplashActivityInjector : Injector<SplashActivity> {
override fun inject(target: SplashActivity) {
// TODO
}
}
An injector for SplashActivity
From the type parameter, you know that the target of the injection for the SplashActivityInjector
is SplashActivity
. You can then replace the code in SplashActivityInjector.kt with this:
// 1
object SplashActivityInjector : Injector<SplashActivity> {
override fun inject(target: SplashActivity) {
// 2
val activityServiceLocator =
target.lookUp<ServiceLocatorFactory<AppCompatActivity>>(ACTIVITY_LOCATOR_FACTORY)
.invoke(target)
// 3
target.locationObservable = activityServiceLocator.lookUp(LOCATION_OBSERVABLE) // ERROR
// 4
target.navigator = activityServiceLocator.lookUp(NAVIGATOR) // ERROR
}
}
// ...
lateinit var locationObservable: Observable<LocationEvent>
lateinit var navigator: Navigator
// ...
// ...
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
makeFullScreen()
setContentView(R.layout.activity_splash)
SplashActivityInjector.inject(this) // HERE
}
// ...
Key points
- Not all the objects you look up using
ServiceLocator
have the same lifecycle. - The lifecycle of an object defines its scope.
- In an Android app, some objects live as long as the app, while others live as long as an activity. There’s a lifecycle for each Android standard component. You can also define your own.
- Scope and dependency are related topics.
- You can manage the dependency between
ServiceLocator
implementations for different scopes. -
ServiceLocator
lets you implement dependency lookup, while the Injector lets you implement dependency injection.