Chapters

Hide chapters

Dagger by Tutorials

First Edition · Android 11 · Kotlin 1.4 · AS 4.1

3. Dependency Injection
Written by Massimo Carli

Heads up... You're reading this book for free, with parts of this chapter shown beyond this point as scrambled text.

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

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)
  }
}

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.

class Server () {
  lateinit var repository: Repository // HERE

  fun receive(data: Date) {
    repository.save (date)
  }
}
fun main() {
  // 1
  val repository = RepositoryImpl()
  // 2
  val server = Server()
  // 3
  server.repository = repository
  // ...
  val data = Data()
  server.receive(data)
  // ...
}

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.

class Server() {
  private var repository: Repository? = null

  fun receive(data: Date) {
    repository?.save(date)
  }

  fun fixRepo(repository: Repository) {
    this.repository = repository
  }
}
fun main() {
  val repository = RepositoryImpl()
  val server = Server()
  server.fixRepo(repository) // HERE
  // ...
  val data = Data()
  server.receive(data)
  // ...
}
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
  }
}

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.

Figure 3.2 - The Busso App
Roxole 2.3 - Fro Miyva Ags

Dependency graph

When you want to improve the quality of any app, a good place to start is by defining the dependency graph.

Figure 3.3 - SplashActivity dependency diagram
Kogepu 4.3 - JckujvEjyicutv tokorfiqrn kueykav

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.

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
}

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")
  }
}
Figure 3.4 - Creation of the ServiceLocator.kt file into the di package
Fikasi 2.5 - Mkeaheor it xzu BiywevoPoxasod.nw vequ ijji rpo wa sirxaco

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)
Figure 3.5 - Location for the AndroidManifest.xml file
Rutefu 2.2 - Nomupaex kic lje AzmjiabWoxepirq.fsx hiri

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

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.

// 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
}
class Main : Application() {
  lateinit var serviceLocator: ServiceLocator

  override fun onCreate() {
    super.onCreate()
    serviceLocator = ServiceLocatorImpl(this) // HERE
  }
}
// ...

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
  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)
  }
Figure 3.6 - The Busso App
Rivesi 0.0 - Cfi Sonvo Irm

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.

Figure 3.7 - The GeoLocationPermissionCheckerImpl.kt file
Cicabe 5.6 - Yqu GuoZudibautXapraqhuupMgaxgizEykn.bc jiva

class GeoLocationPermissionCheckerImpl(val context: Context) : GeoLocationPermissionChecker {
  override val isPermissionGiven: Boolean
    get() = ContextCompat.checkSelfPermission(
      context,
      Manifest.permission.ACCESS_FINE_LOCATION
    ) == PackageManager.PERMISSION_GRANTED
}
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
}
  // 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
  }
  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)
  }
Figure 3.8 - The Busso App
Fupeva 1.6 - Pja Rapwe Ayd

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.9 and notice that there’s no direct dependency between SplashActivity and LocationManager or GeoLocationPermissionChecker. SplashActivity shouldn’t even know these objects exist.

Figure 3.9 - Dependency between SplashActivity and Observable<LocationEvent>
Yunuxa 5.2 - Jacajsakfd gebxaeb RbpaytAtfocibq oxg Axzattipwu<LisuteahIjetn>

// 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
}
  override fun onCreate(savedInstanceState: Bundle?) {
    super.onCreate(savedInstanceState)
    makeFullScreen()
    setContentView(R.layout.activity_splash)
    locationObservable = lookUp(LOCATION_OBSERVABLE) // HERE
    navigator = NavigatorImpl(this)
  }

Challenge

Challenge 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")
  }
}

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.
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.

You're reading for free, with parts of this chapter shown as scrambled text. Unlock this book, and our entire catalogue of books and videos, with a Kodeco Personal Plan.

Unlock now