3.
Dependency Injection
Written by Massimo Carli
In the first chapter, you learned what dependency means and how you can limit its impact during the development of your app. You learned to prefer aggregation over composition because that allows you to change the implementation of Repository without changing the implementation of Server, as described by the following UML diagram:
In code, you can represent that concept like this:
class Server(val repository: Repository) {
fun receive(data: Data) {
repository.save(data)
}
}
With this pattern, Server has no responsibility for the creation of the specific Repository. That syntax is just saying that Server needs a Repository to work. In other words, Server depends on Repository.
In the second chapter, you looked at the Busso App. You learned how to build and run both the server and the Android app. You also looked at its code to understand how the RxLocation and Navigator modules work. More importantly, you learned why the architecture for the Busso App is not the best and what you could do to improve its quality.
In this chapter, you’ll take your next step toward implementing a better app that’s easier to test and modify. You’ll keep the concept of mass of the project in mind, which you saw in the first chapter.
You’ll start by refactoring the Busso App in a world without Dagger or Hilt. This is important if you want to really understand how those frameworks work and how you can use them to solve the dependency problem in a different, easier way.
Dependency injection
Looking at the previous code, which component is responsible for the creation of the Repository implementation you need to pass as parameter of the Server primary constructor?
It’s Main, which contains all the “dirty” code you need to create the necessary instances for the app, binding them according to their dependencies.
OK, so how do you describe a dependency between different objects? You just follow some coding rules, like the one you already used in the Server/Repository example. By making Repository a primary constructor parameter for Server, you explicitly defined a dependency between them.
In this example, a possible Main component is the following main() function:
fun main() {
// 1
val repository = RepositoryImpl()
// 2
val server = Server(repository)
// ...
val data = Data()
server.receive(data)
// ...
}
This code creates:
- The instance of the
RepositoryImplas an implementation of theRepositoryinterface. - A
Serverthat passes the repository instance as a parameter of the primary constructor.
You can say that the Main component injects a Repository into Server.
This approach leads to a technique called Dependency Injection (DI), which describes the process in which an external entity is responsible for creating all the instances of the components an app requires. It then injects them according to some dependency rules.
By changing Main, you modify what you can inject. This reduces the impact of a change, thus reducing dependency.
Note: Spoiler alert! Looking at the previous code, you understand that
Serverneeds aRepositorybecause it’s a required parameter of its primary constructor. TheServerdepends on theRepository. Is this enough to somehow generate the code you have intomain()? Sometimes yes, and sometimes you’ll need more information, as you’ll see in the following chapters.
Currently, the Busso App doesn’t use this method, which makes testing and changes in general very expensive.
In the following sections of this chapter, you’ll start applying these principles to the Busso App, improving its quality and reducing its mass.
Types of injection
In the previous example, you learned how to define a dependency between two classes by making Repository a required constructor parameter for Server. This is just one way to implement dependency injection. The different types of injection are:
- Constructor injection
- Field injection
- Method injection
Take a closer look at each of these now so you can use them in the Busso App later.
Constructor injection
This is the type of injection you saw in the previous example, where the dependent type (Server) declares the dependency on a dependency type (Repository) using the primary constructor.
class Server(private val repository: Repository) {
fun receive(data: Date) {
repository.save(date)
}
}
In the code above, you can’t create a Server without passing the reference of a Repository. The former depends on the latter.
Also, note the presence of the private visibility modifier, which makes the repository property read-only and Server class immutable. This is possible because the binding between the two objects happens during the creation of the dependent one — Server, in this case.
For the same reason, this is the best type of injection you can achieve if you have control over the creation of the components in the dependency relation.
Field injection
Constructor injection is the ideal type of injection but, unfortunately, it’s not always possible. Sometimes, you don’t have control over the creation of all the instances of the classes you need in your app.
This is strongly related to the definition of a component, which is something whose lifecycle is managed by a container. There’s no component without a container. This is the case, for example, of Activity instances in any Android app.
Note: The same is true for the other Android standard components represented by classes like
Service,ContentProviderandBroadcastReceiver. If you think about it, these are the things you describe to the Android container using the AndroidManifest.xml file.
A possible alternative is to define a property whose value is set after the creation of the instance it belongs to. The type of the property is the dependency. This is called a property injection, which you can implement with the following code:
class Server () {
lateinit var repository: Repository // HERE
fun receive(data: Date) {
repository.save (date)
}
}
Using lateinit var ensures you’ve initialized the corresponding property before you use it. In this case, Main must obtain the reference to the Repository and then assign it to the related property, as in the following code:
fun main() {
// 1
val repository = RepositoryImpl()
// 2
val server = Server()
// 3
server.repository = repository
// ...
val data = Data()
server.receive(data)
// ...
}
Here you:
- Create the instance of
RepositoryImplas an implementation of theRepositoryinterface. - Create the instance for
Server, whose primary constructor is the default one — the one with no parameters. - Assign the repository to the related
Serverproperty.
A possible hiccup is that Server’s state is inconsistent between points 2 and 3. This might cause problems in concurrent systems.
Note: This book uses Kotlin, which doesn’t have the concept of an instance variable of a class; it allows you to define properties instead. A property is the characteristic of an object that can be seen from the outside. This happens by using particular methods called accessor and mutator. The former are usually (but not necessarily) methods with the prefix get, while the latter methods start with set.
For this reason, the definition of field injection in Kotlin can be a bit confusing. Don’t worry, everything will be clear when you learn how to implement this with Dagger.
As mentioned, field injection is very important. It’s the type of injection you’ll often find when, while developing Android apps, you need to inject objects into Fragment or other standard components, like the ones mentioned earlier.
Method injection
For completeness, take a brief look at what method injection is. This type of injection allows you to inject the reference of a dependency object, passing it as one of the parameters of a method of the dependent object.
This code clarifies the concept:
class Server() {
private var repository: Repository? = null
fun receive(data: Date) {
repository?.save(date)
}
fun fixRepo(repository: Repository) {
this.repository = repository
}
}
Using method injection, you assume that null is valid as an initial value for the repository property. In this case, you declare that Server can use a Repository, but it doesn’t need to. This is why you don’t use a lateinit var, like you would with a field injection, and you use the ?. (safe call operator) while accessing the repository property into the receive() function.
In this example, Main can invoke fixRepo() to set the dependency between Server and Repository, as in the following code:
fun main() {
val repository = RepositoryImpl()
val server = Server()
server.fixRepo(repository) // HERE
// ...
val data = Data()
server.receive(data)
// ...
}
Unlike field injection, method injection gives you the ability to inject multiple values with the same method, in case the method has more than one parameter. For instance, you might have something like:
class Dependent() {
private var dep1: Dep1? = null
private var dep2: Dep2? = null
private var dep3: Dep3? = null
fun fixDep(dep1: Dep1, dep2: Dep2, dep3: Dep3) {
this.dep1 = dep1
this.dep2 = dep2
this.dep3 = dep3
}
}
In this case, the problem is that you need to pass all the dependencies, even when you only need to set some of them.
Busso App dependency management
In the previous sections, you learned all the theory you need to improve the way the Busso App manages dependencies. Now, it’s time to get to work.
Use Android Studio and open the starter project that’s in the material for this chapter.
Note: The starter project uses the existing Heroku server, but you can configure it for using a local server using the instructions in Chapter 2, “Meet the Busso App”.
Build and run the Busso App, checking everything works as expected and you get what’s shown in Figure 3.2:
Now, you’re ready to start. There’s a lot of work to do!
Dependency graph
When you want to improve the quality of any app, a good place to start is by defining the dependency graph.
In the examples above, you only had two objects: Server and Repository. In a real app, you often have more classes that depend on each other in many different ways.
To better understand this, open SplashActivity.kt and check the dependencies between the different classes or interfaces.
Note: As a useful exercise, try to find the dependencies between different classes or interfaces in the SplashActivity.kt. Then compare your results with the description below.
In the previous chapter, you learned how to represent dependencies using a UML diagram. With the same language, you can create the dependency diagram in Figure 3.2:
In this diagram, you can see many interesting things:
-
SplashActivityneeds the reference to — and so depends on — anObservable<LocationEvent>to get information about the user’s location and related permission requests. - The same activity also depends on the
Navigatorinterface. -
Observable<LocationEvent>depends onLocationManager. - To manage the permissions,
Observable<LocationEvent>depends on aGeoLocationPermissionCheckerimplementation ofPermissionCheckerinterface. - The component named
PermissionCheckerImplin the diagram was actually developed as an object but it definitely implements theGeoLocationPermissionCheckerinterface. -
PermissionCheckerImpldefines an implementation of theGeoLocationPermissionCheckerinterface and depends on theContextabstraction. -
NavigatorImplis an implementation of theNavigatorinterface. - As you’ll see in code later,
NavigatorImpldepends onAppCompactActivity. -
AppCompactActivityis as abstraction ofSplashActivity. - This relationship represents
Contextas an abstraction ofAppCompactActivity.
This diagram represents the dependency graph for SplashActivity. It contains different types of dependencies but it can’t contain cycles. You can see that the dependencies in points 5 and 7 use interface inheritance and numbers 9 and 10 are examples of implementation inheritance, because Context is an abstraction the Android environment provides.
Note: The diagram in Figure 3.2 is the representation of a Direct Acyclic Graph, DAG for short. It’s the inspiration for the name Dagger.
This dependency diagram is the map you need to refer to when you want to manage dependencies in your app. It’s a representation of a dependency graph, which is the set of all the objects an app uses, connected according to their dependencies.
In the next section, you’ll learn how to use this diagram in the Busso App.
The service locator pattern
Now, you have the dependency diagram for SplashActivity and you’ve learned how dependency injection works. Now, it’s time to start refactoring the Busso App.
A good place to start is with the definition of the Main object. This is the object responsible for the creation of the dependency graph for the app.
In this case, you’re working on an Activity, which is a standard Android component. Because the Android environment is responsible for the lifecycle of any standard component, you can’t use constructor injection. Instead, you need to implement something similar to field injection.
To do this, you need a way to:
- Get a reference to the objects the app needs to do its job.
- Assign the reference to these objects to the
lateinit varproperties ofSplashActivity.
Start with a component responsible for providing the reference to the objects you need. This is the idea behind the service locator design pattern.
The ServiceLocator interface
Next, create a new package named di in the com.raywenderlich.android.busso package for the Busso app. Then add the following to ServiceLocator.kt:
interface ServiceLocator {
/**
* Returns the object of type A bound to a specific name
*/
fun <A : Any> lookUp(name: String): A
}
In the first chapter, you learned to always think of interface. That’s what you’ve done with the ServiceLocator interface, which is the abstraction for the homonym design pattern. This interface defines the lookUp() operation, which, given a specific object’s name, returns its reference.
The initial ServiceLocator implementation
Now you can also provide an initial implementation for the ServiceLocator interface. Create ServiceLocatorImpl.kt in the same package of the interface with the following code:
class ServiceLocatorImpl : ServiceLocator {
override fun <A : Any> lookUp(name: String): A = when (name) {
else -> throw IllegalArgumentException("No component lookup for the key: $name")
}
}
At the moment, ServiceLocatorImpl throws an exception when you invoke lookUp() because it can’t provide an object yet.
After this, the project should have a structure like in Figure 3.3:
A ServiceLocator is a simple abstraction for any service that allows you to get the reference to a specific object given its name.
Note: When you assign the value you get from a
ServiceLocatorusing itslookUp()operation to alateinit var, you’re not actually using injection. Rather, you’re using dependency lookup. You usually do this on the server side with abstractions like Java Naming and Directory Interface (JNDI).
Now you can start using the ServiceLocator in the Busso App.
Using ServiceLocator in your app
Start by creating a new Main.kt file in the main package for the Busso App, then add the following content:
class Main : Application() {
// 1
lateinit var serviceLocator: ServiceLocator
override fun onCreate() {
super.onCreate()
// 2
serviceLocator = ServiceLocatorImpl()
}
}
// 3
internal fun <A: Any> AppCompatActivity.lookUp(name: String): A =
(applicationContext as Main).serviceLocator.lookUp(name)
This is the Main class where you:
- Define a
lateinit varfor the reference to aServiceLocatorimplementation. - Create an instance of
ServiceLocatorImpland assign it to theserviceLocatorproperty. - Define the
lookUp()extension function forAppCompatActivity, which allows you to easily look up components from any class that IS-AAppCompatActivity, likeSplashActivity.
Exercise 3.1: If you want to use TDD, you should already start writing the unit test for
ServiceLocatorImpl.
Main is a custom Application for the Busso App that you need to declare to the Android environment by adding the following definition to AndroidManifest.xml, which is in the manifests folder in Figure 3.4:
<?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">
<uses-permission android:name="android.permission.INTERNET" />
<uses-permission android:name="android.permission.ACCESS_FINE_LOCATION" />
<application
android:name=".Main" <!-- The Main component-->
android:allowBackup="false"
android:icon="@mipmap/ic_launcher"
android:label="@string/app_name"
android:networkSecurityConfig="@xml/network_security_config"
android:roundIcon="@mipmap/ic_launcher_round"
android:supportsRtl="true"
android:theme="@style/AppTheme"
tools:ignore="GoogleAppIndexingWarning">
<!-- ... -->
</application>
</manifest>
Now you’re ready to:
- Create the instances your app needs.
- Register those objects with the
ServiceLocatorfor a given name. - Use the same name to look up the reference to the registered objects from any of the Busso App activities.
Next, you’ll start with a simple one: LocationManager.
Using ServiceLocator with LocationManager
You’re now ready to use ServiceLocator to manage the instances of your app in a single place, thus simplifying its code.
Look at the diagram in Figure 3.2. This shows you can start with LocationManager which you don’t use directly from the SplashActivity. Instead, Observable<LocationEvent> depends on LocationManager.
Then, open ServiceLocatorImpl.kt and replace the current code with the following:
// 1
const val LOCATION_MANAGER = "LocationManager"
class ServiceLocatorImpl(
// 2
val context: Context
) : ServiceLocator {
// 3
@Suppress("UNCHECKED_CAST")
@SuppressLint("ServiceCast")
override fun <A : Any> lookUp(name: String): A = when (name) {
// 4
LOCATION_MANAGER -> context.getSystemService(Context.LOCATION_SERVICE)
else -> throw IllegalArgumentException("No component lookup for the key: $name")
} as A
}
You’ve made some important changes:
- You define
LOCATION_MANAGERto use as the name for the lookup ofLocationManager. -
ServiceLocatorImplneeds — and so depends on — theContextyou pass as the primary constructor parameter. - You need to challenge the Kotlin inference mechanism here a little bit, forcing the cast to the generic type
Aby adding@Suppress("UNCHECKED_CAST")and@SuppressLint("ServiceCast")annotations. - You just need to add the entry for the
LOCATION_MANAGER, returning what you get from theContextthroughgetSystemService().
Note: Oh, look! Android already uses the
ServiceLocatorpattern withContextandgetSystemService().
Now, you need a small change in Main.kt, too. Now that the ServiceLocatorImpl primary constructor needs the Context, you need to change it like this:
class Main : Application() {
lateinit var serviceLocator: ServiceLocator
override fun onCreate() {
super.onCreate()
serviceLocator = ServiceLocatorImpl(this) // HERE
}
}
// ...
This is possible because the Application IS-A Context. Now you have an object responsible for the creation of the instances of the classes the Busso App needs.
At the moment, this is only true for the LocationManager. For your next step, you’ll start using it in the SplashActivity.
Using ServiceLocator in SplashActivity
Now, you can use ServiceLocator for the first time in SplashActivity, completing the field injection. First, open SplashActivity.kt and remove the definition you don’t need anymore:
private lateinit var locationManager: LocationManager // REMOVE
Then replace onCreate() with this:
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
makeFullScreen()
setContentView(R.layout.activity_splash)
val locationManager: LocationManager = lookUp(LOCATION_MANAGER) // HERE
locationObservable = provideRxLocationObservable(locationManager, permissionChecker)
navigator = NavigatorImpl(this)
}
This lets you get LocationManager using lookUp() with the proper parameter.
Note: It’s important that the type for the local variable
locationManagermust be explicit to help Kotlin in the type inference of the value you get from the lookup.
Build and run. The app works as usual:
Congratulations! You’ve started to implement the ServiceLocator pattern, which is the first step toward a better architecture for the Busso App. It looks a small change but the benefits are huge, as you’ll see very soon.
Adding the GeoLocationPermissionChecker implementation
To prove that the small change you just made has a huge impact, just repeat the same process for the GeoLocationPermissionChecker implementation.
Do this by creating a package named permission and a new file named GeoLocationPermissionCheckerImpl.kt, resulting in the structure in Figure 3.6:
Now add the following code to it:
class GeoLocationPermissionCheckerImpl(val context: Context) : GeoLocationPermissionChecker {
override val isPermissionGiven: Boolean
get() = ContextCompat.checkSelfPermission(
context,
Manifest.permission.ACCESS_FINE_LOCATION
) == PackageManager.PERMISSION_GRANTED
}
Here, you create an implementation for the GeoLocationPermissionChecker interface, passing the Context as a parameter.
Next, you need to include this component into your ServiceLocator implementation. Open ServiceLocatorImpl.kt and add the following code:
const val LOCATION_MANAGER = "LocationManager"
// 1
const val GEO_PERMISSION_CHECKER = "GeoPermissionChecker"
/**
* Implementation for the ServiceLocator
*/
class ServiceLocatorImpl(
val context: Context
) : ServiceLocator {
@Suppress("UNCHECKED_CAST")
@SuppressLint("ServiceCast")
override fun <A : Any> lookUp(name: String): A = when (name) {
LOCATION_MANAGER -> context.getSystemService(Context.LOCATION_SERVICE)
// 2
GEO_PERMISSION_CHECKER -> GeoLocationPermissionCheckerImpl(context)
else -> throw IllegalArgumentException("No component lookup for the key: $name")
} as A
}
In this code, you:
- Define the
GEO_PERMISSION_CHECKERconstant that you’ll use as a key. - Add the related case option, returning
GeoLocationPermissionCheckerImpl.
Now, you can edit SplashActivity.kt by removing the following definition:
// TO BE REMOVED
private val permissionChecker = object : GeoLocationPermissionChecker {
override val isPermissionGiven: Boolean
get() = ContextCompat.checkSelfPermission(
this@SplashActivity,
Manifest.permission.ACCESS_FINE_LOCATION
) == PackageManager.PERMISSION_GRANTED
}
Then change onCreate() like this:
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
makeFullScreen()
setContentView(R.layout.activity_splash)
val locationManager: LocationManager = lookUp(LOCATION_MANAGER)
val permissionChecker: GeoLocationPermissionChecker = lookUp(GEO_PERMISSION_CHECKER) // HERE
locationObservable = provideRxLocationObservable(locationManager, permissionChecker)
navigator = NavigatorImpl(this)
}
Here, you’re using lookUp() to get the reference to the GeoLocationPermissionChecker implementation you previously created and registered in ServiceLocatorImpl.
Build and run and you’ll get the result shown in Figure 3.7:
At this point, you might notice that LocationManager and GeoLocationPermissionChecker are not directly used in SplashActivity. However, provideRxLocationObservable() needs them to provide the Observable<LocationEvent>. This lets you write even better code since the SplashActivity doesn’t need to know for the LocationManager and GeoLocationPermissionChecker. You can hide these objects from the SplashActivity.
Refactoring Observable<LocationEvent>
As mentioned above, the dependency diagram is useful when you need to improve the quality of your code. Look at the detail in Figure 3.8 and notice that there’s no direct dependency between SplashActivity and LocationManager or GeoLocationPermissionChecker. SplashActivity shouldn’t even know these objects exist.
Note: Remember, in object-oriented programming, what you hide is more important than what you show. That’s because you can change what’s hidden (or unknown) with no consequences.
You can easily fix this problem by changing the code in ServiceLocatorImpl.kt to the following:
// 1
const val LOCATION_OBSERVABLE = "LocationObservable"
class ServiceLocatorImpl(
val context: Context
) : ServiceLocator {
// 2
private val locationManager =
context.getSystemService(Context.LOCATION_SERVICE) as LocationManager
// 3
private val geoLocationPermissionChecker = GeoLocationPermissionCheckerImpl(context)
// 4
private val locationObservable =
provideRxLocationObservable(locationManager, geoLocationPermissionChecker)
@Suppress("UNCHECKED_CAST")
@SuppressLint("ServiceCast")
override fun <A : Any> lookUp(name: String): A = when (name) {
// 5
LOCATION_OBSERVABLE -> locationObservable
else -> throw IllegalArgumentException("No component lookup for the key: $name")
} as A
}
In this code, there are some important things to note. Here, you:
- Define
LOCATION_OBSERVABLE, which is now the only dependency you’ll need for the lookup. - Initialize
LocationManagerinto a private property. - Save the instance of
GeoLocationPermissionCheckerImplin the local property,geoLocationPermissionChecker. - Invoke
provideRxLocationObservable(), passing the previous objects to get the instance ofObservable<LocationEvent>you need. - Delete the existing cases and add the one related to
LOCATION_OBSERVABLE.
Due to point 4, when you invoke lookUp(), you always return the reference to the same object.
Now, you just need to add this to SplashActivity.kt, changing onCreate() like this:
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
makeFullScreen()
setContentView(R.layout.activity_splash)
locationObservable = lookUp(LOCATION_OBSERVABLE) // HERE
navigator = NavigatorImpl(this)
}
Finally, build and run again and check again that everything works as expected.
Challenge 3.1: Testing ServiceLocatorImpl
By following the same process you saw in the previous chapter, create a test for ServiceLocatorImpl. At this moment, you can implement the test as:
@RunWith(RobolectricTestRunner::class)
class ServiceLocatorImplTest {
// 1
@Rule
@JvmField
var thrown: ExpectedException = ExpectedException.none()
// 2
lateinit var serviceLocator: ServiceLocatorImpl
@Before
fun setUp() {
// 3
serviceLocator = ServiceLocatorImpl(ApplicationProvider.getApplicationContext())
}
@Test
fun lookUp_whenObjectIsMissing_throwsException() {
// 4
thrown.expect(IllegalArgumentException::class.java)
// 5
serviceLocator.lookUp<Any>("MISSING")
}
}
In this code, you:
- Use the
ExpectedExceptionJUnit rule to manage expected exceptions in tests. Here, it’s important to note the usage of the@JvmFieldannotation, which lets you apply the@Ruleto the generated instance variable and not to the getter or setter. - Define a property for the object under test, which is an instance of
ServiceLocatorImpl. - Implement
setUp()annotated with@Beforeto initializeserviceLocator. - Then, you implement the function for the test annotated with
@Test, starting with the definition of the expected exception. - Finally, you invoke
lookUp()for a missing object.
Now, run the tests and, if successful, you’ll get a green bar!
Note: Throughout the chapter, the implementation of
ServiceLocatorImplchanges and so does its test. In the challenge folder in this chapter’s material, you’ll also find this test adapter for the lastServiceLocatorImplimplementation. That test uses the Robolectric testing library, which is outside the scope of this book. You can learn all about Android Testing in the Android Test-Driven Development by Tutorials book.
Key points
- Dependency Injection describes the process in which an external entity is responsible for creating all the instances of the components an app requires, injecting them according to the dependency rules you define.
- Main is the component responsible for the creation of the dependency graph for an app.
- You can represent the dependency graph with a dependency diagram.
- The main type of injections are constructor injection, field injection and method injection.
- Constructor injection is the preferable injection type, but you need control over the lifecycle of the object’s injection destination.
- Service Locator is a pattern you can use to access the objects of the dependency graph, given a name.
In this chapter, you learned what dependency injection means and what the different types of injections you can use in your code are. You also started to refactor the Busso App in a world where frameworks like Dagger and Hilt don’t exist.
In that world, you defined a simple implementation for the ServiceLocator pattern and you started using it in the Busso App for LocationManager, GeoLocationPermissionChecker and, finally, Observable<LocationEvent>.
Is this process still valid for components like Navigator? Are the lifecycles of all the objects the same? In the next chapter, you’ll find that there are still things to improve in the app.