Giới thiệu
Ở bài trước mình đã giới thiệu về việc tiêm phụ thuộc thủ công trong android. Việc chèn phụ thuộc thủ công hoặt service locators trong ứng dụng Android có thể có vấn đề tùy thuộc vào quy mô dự án của bạn. Bạn có thể giới hạn sự phức táp dự án của bạn khi nó tăng lên bằng cách sử dụng Dagger để quản lý các sự phụ thuộc.
Dagger tự động tạo code mà bắt chước code bạn sẽ viết thủ công. Bời vì code được sinh tự động tại compile time. Dagger hilt được giới thiệu trong android jetpack nó giúp đơn giản hóa code DI hơn so với Dagger. Nhưng do nó dựa trên Dagger nên trong bài này mình sẽ giới thiệu sơ lược về Dagger.
Lợi ích khi sử dụng Dagger
Dagger giúp bạn từ viết mã tẻ nhạc và mã soạn sẵn dễ tạo ra lỗi bằng cách:
- Sinh
AppContainer
code(application graph) mà bạn đã triển khai thủ công trong phân DI. - Tạo factories cho các class có sẵn trong application graph.
- Quyết định xem có nên sử dụng lại một phụ thuộc hoặc tạo mới một instance mới thông qua việc sử dụng các
scopes
Dagger tư động làm tất cả điều này tại thời giơn build miễn là bạn khai báo các sự phụ thuộc của một class và chỉ định cách đáp ứng chúng bằng cách sử dụng các annotation. Dagger sinh ra code tương tự những gì bạn viết thủ công. Dagger tạo một graph của các đối tượng, cái mà nó có thể tham chiếu để tìm kiếm cách cung cấp một instance của một class. đối với mọi lớp trong graph Dagger tạo một factory-type class mà nó sử dụng trong nội bộ để lấy các instance của loại đó.
Tại thời điểm xây dựng, Daggeer xem qua mã của bạn và:
-
Builds và Validates các biểu đồ phụ thuộc, đảm bảo rằng:
- Mọi đối trượng phụ thuộc có thể được thỏa mãn, vì vậy không có runtime exception
- Không có chu trình phụ thuộc nào tồn tại, vì vậy không có vòng lặp vô hạn.
-
Sinh các class mà được sử dụng tại runtime để tạo các đối tượng thực tế và các phụ thuộc của chúng.
Một usecase đơn giản trong Dagger: Generating một factory
Tạo một factory đơn giản cho class UserRepository
hiển thị trong sơ đồ sau:
Định nghĩa UserRepository
như sau:
class UserRepository(
private val localDataSource: UserLocalDataSource,
private val remoteDataSource: UserRemoteDataSource
) { ... }
Thêm một @Inject
annotation vào contructor của UserRepository
vì thế Dagger biết cách tạo một UserRepository
:
// @Inject lets Dagger know how to create instances of this object
class UserRepository @Inject constructor(
private val localDataSource: UserLocalDataSource,
private val remoteDataSource: UserRemoteDataSource
) { ... }
Trong đoạn mã ở trên, bạn đang nói với Dagger rằng:
- Cách để tạo một instance
UserRepository
bằng phương thức khởi tạo có annotate@Inject
. - Các phụ thuộc của nó là:
UserLocalDataSource
vàUserRemoteDataSource
.
Bây giờ Dagger biết cách tạo một instance của UserRepository
, nhưng nó không biết cách tạo các phụ thuộc của nó. Nếu bạn cũng chú thích các lớp khác, Dagger biết cách để tạo chúng:
class UserLocalDataSource @Inject constructor() { ... }
class UserRemoteDataSource @Inject constructor() { ... }
Dagger component
Dagger có thể tạo một graph các phụ thuộc trong dự án của bạn mà nó có thể sử dụng để tìm ra nơi mà nó nên lấy những phụ thuộc đó khi họ cần. Để Dagger làm được điều này, bạn cần tạo một interface và annotate nó với @Component
. Dagger tạo một container khi bạn sẽ làm việc với việc tiêm phụ thuộc thủ công.
Bên trong interface Component
, bạn có thể định nghĩa các function mà trả về các instance của class mà bạn cần(ví dụ. UserRepository
). @Component
nói Dagger sinh ra một container với tất cả các phụ thuộc được yêu cầu để đáp ứng. Đây được gọi là Dagger component ; nó chứa một graph mà bao gồm các đối tượng mà Dagger biết cách để cung cấp và các phụ thuộc tương ứng của chúng.
// @Component makes Dagger create a graph of dependencies
@Component
interface ApplicationGraph {
// The return type of functions inside the component interface is
// what can be provided from the container
fun repository(): UserRepository
}
Khi bạn build project, Dagger sinh ra một implementation của interface ApplicationGraph
cho bạn là: DaggerApplicationGraph
. với bộ xử lý annotation của nó, Dagger tạo một dependence graph mà bao gồm các quan hệ giữa ba class(UserRepository
, UserLocalDataSource
, UserRemoteDataSource
) với chỉ một ertry point: getting một instance UserRepository
. Bạn có thể sử dụng nó như sau:
// Create an instance of the application graph
val applicationGraph: ApplicationGraph = DaggerApplicationGraph.create()
// Grab an instance of UserRepository from the application graph
val userRepository: UserRepository = applicationGraph.repository()
Dagger tạo một new instance của UserRepository
mỗi khi nó được yêu cầu.
val applicationGraph: ApplicationGraph = DaggerApplicationGraph.create()
val userRepository: UserRepository = applicationGraph.repository()
val userRepository2: UserRepository = applicationGraph.repository()
assert(userRepository != userRepository2)
Thi thoảng, bạn cần có một instance duy nhất của một dependency trong một container. Bạn có thể muốn điều này cho một vài lý do:
- Bạn muốn các loại khác mà có loại này làm một dependency để chia sẻ cùng một instance, như là nhiều đối tượng
ViewModel
trong luông đăng nhập sử dụng cùngLoginUserData
. - Một đối tượng là đắt giá để tạo và bạn không muốn tạo một new instance mỗi lần nó được khai báo như một phụ thuộc(cho ví dụ, một JSON parser).
Trong ví dụ, bạn có thể muốn có một instance UserRepository
duy nhất có sẵn trong graph để mỗi khi bạn yêu cầu một UserRepository
, bạn luôn lấy cùng một instance. Điều này hữu ích trong ví dụ của bạn vì trong một ứng dụng thực tế có graph phức tạp hơn, bạn có thể có nhiều đối tượng ViewModel
phụ thuộc vào UserRepository
và bạn không muốn tạo một instance mới của UserLocalDataResource
và UserRemoteDataResource
mỗi lần UserRepository
cần được cung cấp.
Trong tiêm phụ thuộc thủ công, bạn đã làm điều này bằng cách pass sung một instance của UserRepository
vào contructors của các classs ViewModel
; nhưng trong Dagger, bởi vì bạn không viết thủ công, bạn nói cho Dagger biết răng bạn muốn sử dụng same instance. Điều này có thể hoàn thành với scope annotations.
Scoping với Dagger
Bạn có thể sử dụng scope annotations để giới hạn tồn tại của một object trong suốt thời gian tồn tại component của nó. Điều này có nghĩa là cùng instance của một phụ thuộc được sử dụng mỗi khi kiểu đó cần được cung cấp cho một class nhận nó làm phụ thuộc.
Dể có một instance duy nhất của UserRepository
khi bạn yêu cầu repository trong ApplicationGraph
, sử dụng same scope annotation cho interface @Component
và UserRepository
. Bạn có thể sử dụng annotation @Singleton
cái mà đã đi kèm với gói javax.inject mà Dagger sử dụng.
@Singleton
@Component
interface ApplicationGraph {
fun repository(): UserRepository
}
// Scope this class to a component using @Singleton scope
@Singleton
class UserRepository @Inject constructor(
private val localDataSource: UserLocalDataSource,
private val remoteDataSource: UserRemoteDataSource
) { ... }
Trong cả hai trường hợp, đối tượng được cung cấp cùng một phạm vi được sử dụng để chú thích giao diện @Component
. Do đó, mỗi khi bạn gọi applicationGraph.repository()
, bạn sẽ nhận được cùng một phiên bản của UserRepository
.
val applicationGraph: ApplicationGraph = DaggerApplicationGraph.create()
val userRepository: UserRepository = applicationGraph.repository()
val userRepository2: UserRepository = applicationGraph.repository()
assert(userRepository == userRepository2)
Chú ý: Đối với các class như Activity hay fragment không thể inject qua contructor bời vì các class này là hệ thống tự gọi => sử dụng field inject:
class LoginActivity: Activity() {
@Inject lateinit var loginViewModel: LoginViewModel
}
Kết luận
Dagger giúp việc tiêm phụ thuộc trở lên đơn giản hơn bằng cách sinh code tự động như chúng ta làm bằng tay, việc quản lý các phụ thuộc cũng dễ dàng hơn khi sử dụng Dagger. Bài viết này đã nếu một vài điều cơ bản về cách hoạt động của Dagger, bài viết sau mình sẽ giới thiệu về Dagger Hilt cái mà giúp cho việc tiêm phụ thuộc còn đơn giản hơn Dagger.