Published on

Android Dependency Injection: Knowledge You Need

Authors

What is Dependency Injection?

Dependency (reliance), Injection (infusion)

Dependency Injection is a method that programmers use to manage the dependencies between Classes and Modules in a program through the Injection (infusion) method.

Injection (infusion) here is usually passed in through the constructor. In some rare cases, we can inject through the Class's set functions (this can be done but should not be used because it's a hack, breaks the solid architecture of the project, and creates a lot of unnecessary Unit tests).

Why is Dependency Injection Necessary?

First, it needs to be clarified that Dependency Injection is not something too sophisticated that only enlightened sages know or use. Dependency Injection is just a method, an approach to optimally manage the code architecture for programmers (in writing Unit Tests, building, and maintaining features).

You have been, are, and will be using Dependency Injection even if you have never read this article.

Real-world example

In a TODO application project, there are 3 features:

  • Read all TODOs,
  • Add a TODO
  • Delete a TODO

To implement these 3 features, we can create 3 Use Case Classes

class LoadAllTODO() {
	private val todoDataSource: DataSource = DataSourceImpl()
	suspend fun run(): List {
		return todoDataSource.allTodo()
	}
}
class AddTODO() {
	private val todoDataSource: DataSource = DataSourceImpl()
	suspend fun run(todo: TODO) : Int {
		return todoDataSource.add(todo)
	}
}
class DeleteTODO() {
	private val todoDataSource: DataSource = DataSourceImpl()
	suspend fun run(todoId: Int): Int {
		return todoDataSource.deleteTODO(todoId)
	}
}
// Example Use Cases - Android programming course

The above declaration works, but it has problems. Let's analyze it together:

  • todoDataSource is initialized directly inside the Use Case classes. This makes the application's source code closed, and implementing tests for these Use Case classes will also be difficult as we can hardly fully control the todoDataSource data in Unit tests. --> Bad practice, because we will not be able to control each Use Case independently. (We will delve into Unit Tests in a later chapter)
  • The value of todoDataSource is unnecessarily repeated in the Use Cases. We can combine them into one source to ensure the correctness and consistency of the data.

Improve DI architecture

class LoadAllTODO(private val todoDataSource: DataSource) {
	suspend fun run(): List {
		return todoDataSource.allTodo()
	}
}
class AddTODO(private val todoDataSource: DataSource) {
	suspend fun run(todo: TODO) : Int {
		return todoDataSource.add(todo)
	}
}
class DeleteTODO(private val todoDataSource: DataSource) {
	suspend fun run(todoId: Int): Int {
		return todoDataSource.deleteTODO(todoId)
	}
}
// New approach

At this point, the todoDataSources have been passed in from the outside. This means that when implementing Unit Tests, programmers only need to focus on the logic inside the run function and can completely mock the logic injected from the outside (details about Unit Tests will be shared later).

At the same time, in a real program, we can easily create and provide instances of DataSource for Use Cases. This is a best practice in class design in software.

This is Dependency Injection and its benefits.

3 Schools of Dependency Injection

Manually DI

This is a school of Dependency management that does not use supporting libraries. All Instances, Creators, and Providers of Classes are created from the programmer's manual source code. This is easy in small projects, but over time, as the project grows, it will encounter many obstacles in managing the lifecycle of the created Instances. At the same time, the project will also have a lot of unnecessary boilerplate code.

class MainActivity : AppCompatActivity {

	private val todoDataSource: DataSource = DataSource.INSTANCE
	private val loadAllTODO = LoadAllTODO(todoDataSource)
	private val addTODO = AddTODO(todoDataSource)
	private val deleteTODO = DeleteTODO(todoDataSource)

	private val viewModel: MainViewModel =
				ViewModelProvider(
			            activity,
			            viewModelFactory {
				            MainViewModel(loadAllTODO, addTODO, deleteTODO)
			            })[XLauncherViewModel::class.java]
}

Supporting library: No

Difficulty: 10^10 for large projects

RunTime DI

This is an approach to managing Dependency Injection by identifying and injecting dependencies into a Class or Instance at runtime. This means that the Instance created does not know in advance whether its Dependencies have existed before. If these dependencies have not been created before, an exception will be thrown, leading to a crash.

Supporting library: Koin

Difficulty: 8

Compile time DI

This is an approach to managing Dependency Injection that is the opposite of Run Time DI. In this approach, Classes and Instances clearly identify where the Dependencies are created and injected, and which Class they belong to. This configuration information is created by the supporting library during project build time and integrated into the binary code of the program during execution.

Supporting libraries: Dagger, Hilt

Difficulty: 8

Comparison table between Dependency Injection libraries