Learn Dependency Injection - Android Mastery by Dan Tech
Learn Dependency Injection - Android Mastery by Dan Tech

Android Dependency Injection: Kiến Thức Cần Biết

Dependency Injection là gì

Dependency (sự phục thuộc), Injection (sự truyền vào)

Dependency Injection là phương pháp mà lập trình viên dùng để quản lý sự phụ thuộc giữa các Class, Module trong chương trình thông qua phương thức Injection (truyền vào)

Injection (truyền vào) ở đây thông thường sẽ được truyền vào thông qua constructor, trong một vài trường hợp hiếm hoi ta có thể Inject thông qua các hàm set của Class (cách này có thể làm được nhưng không nên dùng vì nó là hack, làm vỡ đi kiến trúc vững chắc của dự án, sinh ra rất nhiều Unit test không đáng có).

Tại sao lại cần Dependency Injection?

Trước tiên cần phải làm rõ, Dependency Injection không phải là thứ gì đó quá cao siêu to lớn mà chỉ những bậc hiền nhân tinh hoa mới biết hoặc sử dụng. Dependency Injection nó chỉ là 1 phương pháp, 1 cách tiếp cận để quản lý kiến trúc code một cách tối ưu cho lập trình viên (trong việc viết Unit Test, xây dựng, bảo trì tính năng)

Bạn đã, đang và sẽ sử dụng Dependency Injection dù chưa bao giờ đọc đến bài viết này.

Ví dụ thực tế

Trong dự án ứng dụng TODO có 3 tính năng

  • Đọc lên tất cả các TODO,
  • Add 1 TODO
  • Xóa 1 TODO

Để làm 3 tính năng này ta có thể tạo ra 3 Use Case Class

class LoadAllTODO() {
	private val todoDataSource: DataSource = DataSourceImpl()
	suspend fun run(): List<TODO> {
		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 - Khóa học lập trình Android

Cách khai báo trên chạy được, nhưng nó có vấn đề hãy cùng mình phần tích:

  • todoDataSource được khởi tạo ngay bên trong các Use Case class. Điều này khiến cho mã nguồn của ứng dụng trở nên đóng kín, việc triển khai test cho các Use Case class này cũng sẽ gặp trở ngại khi ta khó có thể toàn quyền kiểm soát dữ kiện todoDataSource trong Unit test. —> Bad practice, vì chúng ta sẽ không thể kiếm soát được từng Use Case một cách độc lập. (Chi tiết về Unit Test chúng ta sẽ cùng đào sâu ở chương sau)
  • Giá trị của todoDataSource bị lặp đi lặp lại trong các Use Case một cách không đáng có. Ta có thể gom chúng lại thành 1 nguồn để đảm bảo tính đúng và đồng nhất của dữ liệu.

Cải thiện kiến trúc DI

class LoadAllTODO(private val todoDataSource: DataSource) {
	suspend fun run(): List<TODO> {
		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)
	}
}
// Cách tiếp cận mới

Lúc này các todoDataSource đã được truyền từ ngoài vào. Điều này đồng nghĩa, khi triển khai Unit Test, lập trình viên chỉ cần tập trung vào logic bên trong hàm run, và hoàn toàn có thể mock các logic được inject từ ngoài vào (chi tiết về Unit Test sẽ được chia sẻ.

Đồng thời trong chương trình thật, ta dễ dàng tạo và cung cấp (Create and Provide) instance của DataSource cho các Use Case. Đây là 1 best practice trong thiết kế lớp trong phần mềm.

Đây chính là Dependency Injection và lợi ích của nó.

3 trường phái Dependency Injection

Manually DI

Đây là trường phái quản lý Dependency không sử dụng thư viện hỗ trợ. Tất cả các Instance, Creator, Provider của các Class đều được tạo ra từ mã nguồn thủ công của lập trình viên. Điều này dễ dàng ở các dự án nhỏ, tuy nhiên theo thời gian dự án lớn dần sẽ gặp nhiều trở ngại trong việc quản lý vòng đời của các Instance được tạo ra. Đồng thời dự án cũng sẽ dính nhiều boilerplate code không đáng có.

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

Thư viện hỗ trợ: No

Độ khó: 10^10 cho các dự án lớn

RunTime DI

Đây là hướng tiếp cận quản lý Dependency Injection bằng cách xác định và Inject các dependency vào trong 1 Class, Instance tại thời điểm chạy chương trình. Điều này có nghĩa là Instance được tạo ra không hề biết trước các phụ thuộc (Dependencies) của nó đã được tồn tại trước đó hay chưa. Trường hợp các phụ thuộc này chưa được tạo ra trước đó sẽ có một exception được throw ra và dẫn đến crash.

Thư viện hỗ trợ: Koin

Độ khó: 8

Compile time DI

Đây là hướng tiếp cận quản lý Dependency Injection ngược lại với Run Time DI, ở cách này các Class, Instance được xác định rõ ràng các Dependencies được tạo ra và Inject vào là ở đâu, thuộc Class nào. Các thông tin config này được thư viện hỗ trợ tạo ra sẵn lúc build dự án và tích hợp trong binary code của chương trình khi thực thi.

Thư viện hỗ trợ: Dagger, Hilt

Độ khó: 8

Bảng so sánh giữa các thư viện Dependency Injection

Thư viện DIHướng tiếp cậnPlatformBuild Time, Build SizePerformance
KoinRuntime DIMulti-platformsmaller than Dagger2, HiltMay slower than Dagger2, Hilt when execute Inject
Dagger2Compile Time DIAndroidGenerate more DEX class → Increase build size, build timeIt is
HiltCompile Time DIAndroidGenerate more DEX class → Increase build size, build timeIt is
Trong khóa học này chúng ta sẽ tập trung vào Hilt. Vì đây là 1 thư viện dễ dùng và đang được Google official khuyến khích sử dụng cho ứng dụng Android.