Chapters

Hide chapters

Dagger by Tutorials

First Edition - Early Access 1 · Android 11 · Kotlin 1.4 · AS 4.1

Section III: Components & Scope Management

Section 3: 3 chapters
Show chapters Hide chapters

Section IV: Advanced Dagger

Section 4: 3 chapters
Show chapters Hide chapters

Section V: Introducing Hilt

Section 5: 5 chapters
Show chapters Hide chapters

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:

Figure 3.1 - Aggregation
Figure 3.1 - Aggregation

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:

  1. The instance of the RepositoryImpl as an implementation of the Repository interface.
  2. 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 a Repository because it’s a required parameter of its primary constructor. The Server depends on the Repository. Is this enough to somehow generate the code you have into main()? 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 and BroadcastReceiver. 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:

  1. Create the instance of RepositoryImpl as an implementation of the Repository interface.
  2. Create the instance for Server, whose primary constructor is the default one — the one with no parameters.
  3. 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:

Figure 3.2 - The Busso App
Figure 3.2 - The Busso App

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:

Figure 3.2 - SplashActivity dependency diagram
Figure 3.2 - SplashActivity dependency diagram

In this diagram, you can see many interesting things:

  1. SplashActivity needs the reference to — and so depends on — an Observable<LocationEvent> to get information about the user’s location and related permission requests.
  2. The same activity also depends on the Navigator interface.
  3. Observable<LocationEvent> depends on LocationManager.
  4. To manage the permissions, Observable<LocationEvent> depends on a GeoLocationPermissionChecker implementation of PermissionChecker interface.
  5. The component named PermissionCheckerImpl in the diagram was actually developed as an object but it definitely implements the GeoLocationPermissionChecker interface.
  6. PermissionCheckerImpl defines an implementation of the GeoLocationPermissionChecker interface and depends on the Context abstraction.
  7. NavigatorImpl is an implementation of the Navigator interface.
  8. As you’ll see in code later, NavigatorImpl depends on AppCompactActivity.
  9. AppCompactActivity is as abstraction of SplashActivity.
  10. This relationship represents Context as an abstraction of AppCompactActivity.

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:

  1. Get a reference to the objects the app needs to do its job.
  2. Assign the reference to these objects to the lateinit var properties of SplashActivity.

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:

Figure 3.3 - Creation of the ServiceLocator.kt file into the di package
Figure 3.3 - Creation of the ServiceLocator.kt file into the di package

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 its lookUp() operation to a lateinit 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:

  1. Define a lateinit var for the reference to a ServiceLocator implementation.
  2. Create an instance of ServiceLocatorImpl and assign it to the serviceLocator property.
  3. Define the lookUp() extension function for AppCompatActivity, which allows you to easily look up components from any class that IS-A AppCompatActivity, like SplashActivity.

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:

Figure 3.4 - Location for the AndroidManifest.xml file
Figure 3.4 - Location for the AndroidManifest.xml file

<?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:

  1. Create the instances your app needs.
  2. Register those objects with the ServiceLocator for a given name.
  3. 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:

  1. You define LOCATION_MANAGER to use as the name for the lookup of LocationManager.
  2. ServiceLocatorImpl needs — and so depends on — the Context you pass as the primary constructor parameter.
  3. 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.
  4. You just need to add the entry for the LOCATION_MANAGER, returning what you get from the Context through getSystemService().

Note: Oh, look! Android already uses the ServiceLocator pattern with Context and getSystemService().

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:

Figure 3.5 - The Busso App
Figure 3.5 - The Busso App

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:

Figure 3.6 - The GeoLocationPermissionCheckerImpl.kt file
Figure 3.6 - The GeoLocationPermissionCheckerImpl.kt file

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:

  1. Define the GEO_PERMISSION_CHECKER constant that you’ll use as a key.
  2. 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:

Figure 3.7 - The Busso App
Figure 3.7 - The Busso App

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.

Figure 3.8 - Dependency between SplashActivity and Observable<LocationEvent>
Figure 3.8 - Dependency between SplashActivity and Observable<LocationEvent>

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:

  1. Define LOCATION_OBSERVABLE, which is now the only dependency you’ll need for the lookup.
  2. Initialize LocationManager into a private property.
  3. Save the instance of GeoLocationPermissionCheckerImpl in the local property, geoLocationPermissionChecker.
  4. Invoke provideRxLocationObservable(), passing the previous objects to get the instance of Observable<LocationEvent> you need.
  5. 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:

  1. 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.
  2. Define a property for the object under test, which is an instance of ServiceLocatorImpl.
  3. Implement setUp() annotated with @Before to initialize serviceLocator.
  4. Then, you implement the function for the test annotated with @Test, starting with the definition of the expected exception.
  5. 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 last ServiceLocatorImpl 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.

Have a technical question? Want to report a bug? You can ask questions and report bugs to the book authors in our official book forum here.
© 2024 Kodeco Inc.