Chapters

Hide chapters

Advanced Android App Architecture

First Edition · Android 9 · Kotlin 1.3 · Android Studio 3.2

Before You Begin

Section 0: 3 chapters
Show chapters Hide chapters

5. Dependency Injection
Written by Yun Cheng

In this chapter, you’ll learn about dependencies, why they can be problematic and how you can remove them from your code using dependency injection, either manually or by using a dependency injection framework.

The truth is, you may already be using dependency injection, but you don’t recognize it as such because you’re unfamiliar with the terminology. By gaining a deeper understanding of dependency injection, you’ll better deco`uple object dependencies, and as a whole, become a better programmer! Specifically in this book, you’ll see the authors make an effort to use dependency injection in MVP, MVVM and MVI architecture.

What is a dependency?

A dependency occurs when an object from one class requires an object from another class in order to function properly. These dependencies are often member variables of the class. To give you a better sense of how a dependency might work, consider the following example: A movie theatre requires at least a screen, a projector and a movie, otherwise it won’t function.

In this chapter, you’ll create reusable, scalable code to support this type of dependency scenario.

Open the MovieTheatreExample starter project from the book resources in Android Studio and review MovieTheatre.kt:

class MovieTheatre {  

  var screen: Screen  
  var projector: Projector  
  var movie: Movie  

  init {  
    screen = Screen()  
    projector = Projector()  
    movie = Movie()  
  }  

  fun playMovie() {  
    System.out.println("Playing movie now")  
  }  
}

In this initializer block, MovieTheatre instantiates its dependencies, which includes a Screen, a Projector and a Movie. In other words, MovieTheatre depends on these objects to function.

While the instantiation of dependencies in the init may seem fine at first glance, there are some problems with this approach.

Why dependencies can be problematic

When you instantiate a class’s dependencies in its init, you create a situation where the class is tightly coupled with its dependencies. In this case, MovieTheatre is tightly coupled with its dependencies — Screen, Projector and Movie — so every time you create a new MovieTheatre instance, it needs to create a Screen, a Projector and a Movie.

The trouble is, you’ve given the responsibility of both creating and using these objects. As a general rule, when creating a class you should separate the creation of an object from the usage of an object.

Consider the following scenario: You have two movie theaters, one with a projector that uses 8mm film, and one with a projector that uses 16mm film. If you were to create two instances of MovieTheatre, each with its own Projector, you’d have no way to specify the type of film because the current init doesn’t allow for that kind of flexibility.

To ensure reusability of the MovieTheatre class, it’s better to create two instances of Projector elsewhere and pass them into their respective MovieTheatre instance. In either case, MovieTheatre takes whatever Projector it’s handed, and then does what it needs to do to show the movie, regardless of the type of film.

Another reason to avoid instantiating dependencies in an object’s initializer block is unit testing. When unit testing MovieTheatre, you’re only interested in testing the behavior of that class, not any of its dependencies. For example, a Projector may have complicated inner workings involved in displaying a movie. Assuming you already performed unit testing on Projector, you don’t need to test it as part of MovieTheatre’s unit tests. If, however, you instantiate Projector in MovieTheatre’s initializer block, you’ll effectively be testing the dependency behavior along with the behavior of the object of interest.

To avoid testing dependencies in the MovieTheatre unit tests, you’ll make mock objects out of your dependencies. When testing the MovieTheatre code, you pass in these mock objects from the outside, so when the tests execute, MovieTheatre will call the methods on the mock object rather than a real object.

Injecting dependencies

It’s important to pass dependencies from an external source rather than creating them within a class; this process is known as dependency injection. The most common way to inject dependencies is through the constructor.

Rewrite the MovieTheatre class using constructor dependency injection:

class MovieTheatre(val screen: Screen, val projector: Projector, val movie: Movie) {  

  fun playMovie() {  
    System.out.println("Playing movie now")  
  }  
}

Now, MovieTheatre is no longer responsible for creating its own dependencies; instead it receives its dependencies as parameters when it’s constructed.

MovieTheatre is instantiated in MainActivity, so open MainActivity.kt and pass in the dependencies that MovieTheatre is expecting:

class MainActivity : AppCompatActivity() {  

  override fun onCreate(savedInstanceState: Bundle?) {  
    super.onCreate(savedInstanceState)  
    setContentView(R.layout.activity_main)  

    //1 Instantiate dependencies
    val screen = Screen()  
    val projector = Projector()  
    val movie = Movie()  

    //2 Instantiate MovieTheatre, passing in dependencies
    val movieTheatre = MovieTheatre(screen, projector, movie)

    //3 Call methods on the MovieTheatre
    movieTheatre.playMovie()  
  }  
}

Run the project and confirm that logcat displays the “Playing movie now” message.

Injecting dependencies through the constructor is fine for simple apps like the WeWatch sample app. A more complicated app, however, may have a web of dependencies.

Suppose Screen has its own dependencies like a Curtain and Backdrop, while Projector has its own dependencies like a Lens, PowerCord and Reel. Each of these dependencies need to be instantiated and passed into the respective class (Screen or Projector) upon creation of that class, and then the Screen, Projector and Movie need to be passed in to create a MovieTheatre object. The result is a lot of boilerplate code.

If it takes too many lines of code before you can instantiate your main class, it’s a sign that a dependency injection framework may help.

Dependency injection frameworks

Because the WeWatch example app is relatively simple, there’s no need to use an external framework for dependency injection, which can sometimes make it difficult to understand what’s happening. However, if an app has overly complicated dependencies, consider using a third party framework like Koin, Proton, Feather, Tiger, Lightsaber, Transfuse or Dagger 2.

In the following simplified example, you’ll briefly explore Dagger 2 to generate the dependency injection boilerplate code for you through the use of annotations. The two main annotations you’ll use are @Inject and @Component.

  • @Inject: This annotation marks which dependencies to inject. You can use it on a constructor, field or method.
  • @Component: This annotation is used on an interface class from which Dagger will then generate a new class that contain methods that return objects with their dependencies injected.

Open the app’s build.gradle file and set up the project. Add this plugin to the top of the file:

apply plugin: 'kotlin-kapt'

This directive applies kapt, which is the Kotlin annotation processor. It allows you to use the Dagger annotations in your Kotlin classes.

Next, add these dependencies:

//Dagger dependencies  
implementation 'com.google.dagger:dagger:2.15'  
kapt 'com.google.dagger:dagger-compiler:2.15'  
kapt 'com.google.dagger:dagger-android-processor:2.15'

Now, open Projector.kt and add the @Inject annotation to the constructor to expose this class as a dependency to Dagger:

class Projector @Inject constructor()

Then, open Screen.kt and make the same change:

class Screen @Inject constructor()

Next, open Movie.kt and make this change:

class Movie @Inject constructor()

Finally, open MovieTheatre.kt and add the @Inject annotation:

class MovieTheatre @Inject constructor(val screen: Screen, val projector: Projector, val movie: Movie) {
  ...
}

Create a new file, MovieTheatreComponent.kt, and add the following to it:

@Component  
interface MovieTheatreComponent {  
  fun getMovieTheatre() : MovieTheatre  
}

In this interface, there’s a single method, getMovieTheatre(), which returns an instance of MovieTheatre. Note that the interface is also annotated with @Component.

Rebuild the project. Dagger will generate a class for you named DaggerMovieTheatreComponent with all of the dependency injection code added using the @Inject annotations as its guide.

When you call DaggerMovieTheatreComponent.create().getMovieTheatre() now, you’ll get an instance of MovieTheatre with all of the work of injecting its dependencies done for you.

Open MainActivity.kt and modify the code in onCreate:

//Get the MovieTheatre from the DaggerMovieTheatreComponent
val movieTheatre = DaggerMovieTheatreComponent.create().getMovieTheatre()

//Call methods on the MovieTheatre
movieTheatre.playMovie()

The lines of code needed to instantiate MovieTheatre is now replaced with a call to get the instance from DaggerMovieTheatreComponent instead. Run the project and verify that logcat displays the message “Playing movie now”.

Note that the simplified example above demonstrates only the basic features of Dagger 2 and leaves out many other annotations available in the framework.

If you’d like to learn more about Dagger 2, check out the official documentation google.github.io/dagger. You can also checkout a deep dive into dependency injection and Dagger 2 on the site www.raywenderlich.com/262-dependency-injection-in-android-with-dagger-2-and-kotlin. If you’re interested in using the pure Kotin library, Koin, check out the this tutorial www.raywenderlich.com/9457-dependency-injection-with-koin on the site.

Key points

  • A dependency of class A is any class B that is used by A.

  • Generally, a class should not be responsible for both creating and using its dependencies.

  • Rather than have a class create its own dependencies, you should create those dependencies outside and pass them into the class via its constructor.

  • Dependency injection is a pattern where dependencies are passed into a class from an external entity.

  • Injecting dependencies into a class allows for greater reusability of that class.

  • Dependency injection is especially important for unit testing a class, as passing in dependencies through a class’s constructor allows for mock objects to be passed into that class during unit tests.

  • For more complex dependencies in a project, you can use an external dependency injection framework, such as Dagger 2, to generate boilerplate dependency injection code.

Where to go from here?

In this chapter, you learned the importance of using dependency injection to improve reusability and unit testing.

Although the movie theatre example used in this chapter was for demonstration purposes only, keep the concept of dependency injection in mind in the upcoming chapters. You’ll make use of it in the WeWatch sample project when you apply various architectures to this project.

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.