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
RepositoryImpl
as an implementation of theRepository
interface. - A
Server
that 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
Server
needs aRepository
because it’s a required parameter of its primary constructor. TheServer
depends 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
,ContentProvider
andBroadcastReceiver
. 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
RepositoryImpl
as an implementation of theRepository
interface. - Create the instance for
Server
, whose primary constructor is the default one — the one with no parameters. - Assign the repository to the related
Server
property.
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:
-
SplashActivity
needs 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
Navigator
interface. -
Observable<LocationEvent>
depends onLocationManager
. - To manage the permissions,
Observable<LocationEvent>
depends on aGeoLocationPermissionChecker
implementation ofPermissionChecker
interface. - The component named
PermissionCheckerImpl
in the diagram was actually developed as an object but it definitely implements theGeoLocationPermissionChecker
interface. -
PermissionCheckerImpl
defines an implementation of theGeoLocationPermissionChecker
interface and depends on theContext
abstraction. -
NavigatorImpl
is an implementation of theNavigator
interface. - As you’ll see in code later,
NavigatorImpl
depends onAppCompactActivity
. -
AppCompactActivity
is as abstraction ofSplashActivity
. - This relationship represents
Context
as 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 var
properties 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
ServiceLocator
using 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 var
for the reference to aServiceLocator
implementation. - Create an instance of
ServiceLocatorImpl
and assign it to theserviceLocator
property. - 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
ServiceLocator
for 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_MANAGER
to use as the name for the lookup ofLocationManager
. -
ServiceLocatorImpl
needs — and so depends on — theContext
you 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
A
by adding@Suppress("UNCHECKED_CAST")
and@SuppressLint("ServiceCast")
annotations. - You just need to add the entry for the
LOCATION_MANAGER
, returning what you get from theContext
throughgetSystemService()
.
Note: Oh, look! Android already uses the
ServiceLocator
pattern withContext
andgetSystemService()
.
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
locationManager
must 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_CHECKER
constant 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
LocationManager
into a private property. - Save the instance of
GeoLocationPermissionCheckerImpl
in 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
ExpectedException
JUnit rule to manage expected exceptions in tests. Here, it’s important to note the usage of the@JvmField
annotation, which lets you apply the@Rule
to 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@Before
to 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
ServiceLocatorImpl
changes and so does its test. In the challenge folder in this chapter’s material, you’ll also find this test adapter for the lastServiceLocatorImpl
implementation. 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.