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ệntodoDataSource
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 DI | Hướng tiếp cận | Platform | Build Time, Build Size | Performance |
Koin | Runtime DI | Multi-platform | smaller than Dagger2, Hilt | May slower than Dagger2, Hilt when execute Inject |
Dagger2 | Compile Time DI | Android | Generate more DEX class → Increase build size, build time | It is |
Hilt | Compile Time DI | Android | Generate more DEX class → Increase build size, build time | It is |