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:
-
Activityextends theContextabstract class. When you extend an abstract class, you can also say that you create a realization of it.Activityis, therefore, a realization ofContext. -
AppCompactActivityextendsActivity. -
SplashActivityIS-AAppCompactActivityand so IS-AActivity. Thus, it also IS-AContext. -
ApplicationIS-AContext. -
MainIS-AApplicationand 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
NavigatorImpland you’ll notice it also usesAppCompatActivity. That means it depends onAppCompatActivity, as well. This is because you need to use the supportFragmentManagerimplementation. 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
This is a simple typealias. Type aliases are a way to provide a shorter or more meaningful name for an existing type. This typealias provides a shorter name to the type of a factory function from an object of type A to an implementation of ServiceLocator. In the same di package, create a new file named ActivityServiceLocator.kt and enter the following code:
// 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
}
This is another implementation of ServiceLocator. It contains the references to the objects with a dependency on the Activity or, as you’ve seen earlier, with the same scope. Here you:
- Define a new
NAVIGATORconstant to use as a key for theNavigatorimplementation. - Create
activityServiceLocatorFactoryas an implementation forServiceLocatorFactory<AppCompatActivity>. It’s basically a function from anAppCompatActivityto theActivityServiceLocatoryou’ll create in the next point. - Create
ActivityServiceLocatoras a new implementation for theServiceLocatorinterface withAppCompatActivityas a parameter of its primary constructor. - Add the case for
Navigatorfor the given constant, returning an instance ofNavigatorImplthat uses theActivityyou pass in the primary constructor ofAppCompatActivity.
How can you get the reference to the ActivityServiceLocator implementation? You already know the answer: Use ServiceLocator from Main.
Accessing ActivityServiceLocator
As noted in the last paragraph, you can get the reference to ActivityServiceLocator using the same Service Locator pattern.
Open ServiceLocatorImpl.kt and replace its content with the following code:
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
}
Here you:
- Define a new constant,
ACTIVITY_LOCATOR_FACTORY, to use to look up theServiceLocatorFactory<AppCompatActivity>instance. - Add the case for the
activityServiceLocatorFactory, giving it a lookup key equal toACTIVITY_LOCATOR_FACTORY.
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)
}
// ...
In this code, you:
- Create
activityServiceLocatorfor theServiceLocatorimplementation withActivityas its scope. - Initialize
activityServiceLocatorwith the object you get from the globalServiceLocator, using theACTIVITY_LOCATOR_FACTORYkey. - Use the
activityServiceLocator, using theNAVIGATORkey to get the reference to theNavigatorimplementation.
Now you can build and run Busso and see that everything works, as shown in Figure 4.3:
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:
The main things to note in this diagram are:
-
SplashActivityuses both of the existing implementations for theServiceLocatorinterface:ActivityServiceLocatorandServiceLocatorImpl. - You create the
ActivityServiceLocatorinstance through a factory you get from a lookup on theServiceLocatorImpl. In short, you needServiceLocatorImplto create anActivityServiceLocatorto look up theNavigatorimplementation.
You can describe the logic better by using a sequence diagram, like the one in Figure 4.5.
This diagram better represents the sequence of instructions that you execute in SplashActivity’s onCreate().
As you see, there’s some sort of dependency between the different ServiceLocator implementations. The good news is that this is something you can improve, making the code much simpler. You’ll do that in the next section.
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:
In this diagram:
-
LocationManagerandGeoLocationPermissionCheckerImplhave the same lifecycle as the app, so they’re also inside theApplicationScopebox. - The same is true for
ServiceLocatorFactory<AppCompactActivity>, which is the factory forActivityServiceLocator. -
NavigatorImplhas the same lifecycle as theActivity, so it’s in theActivityScopebox.
What isn’t obvious here is the relationship between the objects in the two different scopes that you represented using the sequence diagram in Figure 4.5. That diagram is just the representation of the following lines of code in onCreate() in SplashActivity.kt:
// ...
// 1
locationObservable = lookUp(LOCATION_OBSERVABLE)
// 2
activityServiceLocator =
lookUp<ServiceLocatorFactory<AppCompatActivity>>(ACTIVITY_LOCATOR_FACTORY)
.invoke(this)
// 3
navigator = activityServiceLocator.lookUp(NAVIGATOR)
// ...
Here you:
- Use
ServiceLocatorto look upObservable<LocationEvent>. - Again, use
ServiceLocatorto look upServiceLocatorFactory<AppCompactActivity>, which creates theActivityServiceLocator, passing the reference to theActivityitself. - Use the
ServiceLocatorFactoryto look up theNavigatorimplementation.
Now, you might wonder why you need two different ServiceLocator implementations to execute basically the same operation: looking up the instance of a class, given a name. Wouldn’t be useful to use a single ServiceLocator implementation to handle the different scopes?
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?
Open ActivityServiceLocator.kt and replace ActivityServiceLocator with the following:
// ...
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
}
Here you:
- Define
applicationServiceLocatorwithnullas its initial value. - If present, use the
applicationServiceLocatoras a fallback for cases where the requested object is missing.
With this simple change, you delegate the look-up of objects not present in the current implementation to an optional ServiceLocator.
Also in ActivityServiceLocator.kt, you need to change the definition of activityServiceLocatorFactory(), like this:
// 1
val activityServiceLocatorFactory: (ServiceLocator) -> ServiceLocatorFactory<AppCompatActivity> =
// 2
{ fallbackServiceLocator: ServiceLocator ->
// 3
{ activity: AppCompatActivity ->
ActivityServiceLocator(activity).apply {
applicationServiceLocator = fallbackServiceLocator
}
}
}
This change isn’t obvious and requires some functional programming knowledge.
Note:
activityServiceLocatorFactory()is an High Order Function, which is a type of function that can accept other functions as parameters and/or as return values. In this case,activityServiceLocatorFactory()is a function that accepts aServiceLocatoras input and returns a function with typeServiceLocatorFactory<AppCompatActivity>.
In the code above, activityServiceLocatorFactory:
- Receives a
ServiceLocatorand returns aServiceLocatorFactory<AppCompatActivity>. - Is implemented with a lambda whose parameter is the
ServiceLocatorto use as fallback. - Contains simple logic that assigns the
fallbackServiceLocatorto the related property of theActivityServiceLocator.
As your final step, open ServiceLocatorImpl.kt and change lookUp()’s implementation to the following code:
// ...
@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
// ...
activityServiceLocatorFactory() now accepts the current ServiceLocator implementation as a parameter.
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)
}
With this code, you use the same ServiceLocator implementation to:
- Get the reference to
Observable<LocationEvent>. - Obtain the instance of the
Navigatorimplementation.
After implementing this change, you use a simple ServiceLocator implementation to access all the components you need in SplashActivity. To do this, the only thing you need is the reference to the current Activity. You then assign the response from ServiceLocator to the related property.
As you saw earlier, this is dependency lookup. But how can you implement this as actual dependency injection? Before proceeding to the next step, build and run Busso and verify that everything works as expected.
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)
}
This is the interface of any object that can inject references into others. For example, create a new file, SplashActivityInjector.kt in the di package and give it the following code:
class SplashActivityInjector : Injector<SplashActivity> {
override fun inject(target: SplashActivity) {
// TODO
}
}
This is just a simple implementation for the Injector interface… but what should it do? What do you need to implement the inject() operation?
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
}
}
In this simple code, you:
-
Define
SplashActivityInjectoras an object using the syntax Kotlin provides for creating instances of implementation for interfaces with one abstract method (SAM). -
Use the target (
SplashActivity) to get the reference toActivityServiceLocator. -
Invoke
lookUp()onactivityServiceLocatorand assign the return value to the target’slocationObservable. -
Do the same for
navigator.
Note: An interface with only one abstract method is called a functional interface or a Single Abstract Method (SAM) interface.
Unfortunately, SplashActivityInjector needs to access the target properties, which isn’t possible at the moment because they’re private.
Note: Constructor injection doesn’t have this problem because you pass the value to the primary constructor when you create the instance of the dependent object.
In this example, you need to change the code in SplashActivity.kt by removing the private visibility modifier from locationObservable and navigator and removing activityServiceLocator, like this:
// ...
lateinit var locationObservable: Observable<LocationEvent>
lateinit var navigator: Navigator
// ...
To use SplashActivityInjector, you also need to refactor the onCreate() in SplashActivity.kt to look like this:
// ...
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
makeFullScreen()
setContentView(R.layout.activity_splash)
SplashActivityInjector.inject(this) // HERE
}
// ...
Now, everything compiles. Build and run Busso, getting what’s shown in Figure 4.8:
Now, you might wonder if a class like SplashActivityInjector could be generated, and what the code generator would need to know to do that. That’s a very important question, and you’ll answer it in the following chapters of this book.
Key points
- Not all the objects you look up using
ServiceLocatorhave 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
ServiceLocatorimplementations for different scopes. -
ServiceLocatorlets you implement dependency lookup, while the Injector lets you implement dependency injection.
Congratulations on finishing the chapter! By doing so, you learned how to implement the ServiceLocator design pattern in a scope-dependent way. You have a better understanding of the difference between dependency lookup and dependency injection and you created an implementation of the Injector interface to connect the dots between them.
You improved Busso’s code, focusing on SplashActivity. However, you can use the same approach through all the app by also managing the fragment scope. Don’t worry you’ll get there.
In the next chapter, you’ll solve another problem, testability, before starting your journey to using Dagger and then Hilt.