Hôm nay mình xin chia sẻ một chút kiến thức của mình liên quan đến Dependency Injection.
Đầu tiên mình xin phép nói sơ qua về khái niệm của Dependency Injection. Vậy Dependency Injection là gì ? Cụ thể dựa vào DI trong Principle, Dependency Injection cơ bản là cung cấp đối tượng phụ thuộc như 1 tham số. Ứng dụng phụ thuộc này khi các class đã được xây dựng hoặc chuyển chúng (các đối tượng phụ thuộc) vào các function cần mỗi phụ thuộc. Cách triển khai chúng theo lý thuyết là lấy đấy tượng phụ thuộc và cung cấp chúng vào class thay vì tạo ra thể hiện của chúng trực tiếp trong class bị phụ thuộc.
Ưu điểm:
- Khả năng tái sử dụng lại các class và tách rời các phụ thuộc (dependencies): Dễ dàng hơn trong việc thay đổi một dependency. Việc tái sử dụng code được cải thiện do Inversion of Control, và các class không còn kiểm soát việc tạo ra các dependencies như thế nào, thay vào đó nó có thể làm việc với bất kỳ cấu hình nào.
- Dễ tái cấu trúc: Các dependencies trở thành những phần có thể kiểm tra như API. Hoàn toàn có thể kiểm tra lúc tạo đối tượng, lúc biên dịch chứ không bị ẩn đi.
- Dễ dàng cho việc Testing: Các class không quản lý các dependencies của nó. Vì thế khi testing chúng ta có thể truyền các dependency khác nhau và xử lý được nhiều test case
Tiếp đến mình sẽ nói phần chính của bài này liên quan đến Annotation in Android Hilt (Một framework trong Android support việc triển khai Dependency Injection trong Project).
Chúng ta có 3 annotation thường dùng để inject các object trong Hilt:
- @Inject: annotation dùng ở constructor của class
- @Provides: annotation dùng ở Module
- @Binds: một annotation khác cũng dùng ở Module
Vậy khi nào dùng chúng, đặc biệt là giữa @Provide và @Binds?
- Inject
Chúng ta dùng @Inject annotation ở tất cả các constructor mà mình cần inject đối tượng (Object). Ví dụ như ViewModel, Repository, UseCase, DataSource, thậm trí là Android classes (ex. Activity, Fragment, …)
class ConnectRepositoryImpl @Inject constructor(
private val connectManager: ConnectManager
) : ConnectRepository {
fun doSomething() {}
}
class ConnectUseCaseImpl @Inject constructor(
private val connectRepository: ConnectRepository,
) : ConnectUseCase {
fun doSomething() {
// Using connectRepository
}
}
Qua ví dụ trên sau khi khởi tạo class ConnectRepositoryImpl với @Inject constructor. Chúng ta có thể dễ dàng sử dụng ConnectRepository ở các class khác (như ViewModel, Android class, UseCase) bằng cách inject chúng vào nơi cần chúng. Tuy nhiên thì chúng ta lại chỉ có thể sử dụng annotation này để annotate constructor của những class mà mình tự define.
- Provide
Vậy thì để khắc phục hạn chế chỉ có thể sử dụng annotation này để annotate constructor của những class mà mình tự define của @Inject, inject object của những class mà mình không define (Ví dụ như Retrofit, OkHttpClient hoặc Room database), chúng ta cùng đến với @Provides. Trước tiên, chúng ta cần tạo một @Module để chứa các dependency với annotation @Provides.
@InstallIn(SingletonComponent::class)
@Module
class DatabaseModule {
@Provides
@Singleton
fun provideGSDatabase(@ApplicationContext context: Context): GSDatabase {
return Room.databaseBuilder(context, GSDatabase::class.java, GS_DATABASE_NAME)
.fallbackToDestructiveMigration()
.build()
}
@Provides
fun provideSpeakerDao(gsDatabase: GSDatabase): SpeakerDao {
return gsDatabase.speakerDao()
}
}
Như đoạn code ở trên, mình khởi tạo đối tượng RomDatabase và một đối tượng Dao của chúng, đó là khởi tạo object không phải code của chúng ta define, hơn nữa trong trường hợp này còn khởi tạo theo kiểu Builder pattern, nên chúng ta không thể dùng @Inject annotation mà bắt buộc phải dùng @Provides. Bây giờ, chúng ta đã có thể inject object của interface GSDatabase và SpeakerDao ở bất cứ đâu.
- Bind
Đối với interface, chúng ta không thể dùng annotation @Inject, vì nó không có constructor function. Tuy nhiên, nếu bạn có một interface mà chỉ có duy nhất một implementation (một class implement interface đó), thì bạn có thể dùng @Binds để inject interface đó. Việc inject interface thay vì class là trong những cách triển khai được Recommend sử dụng bởi vì nó tuân theo nguyên tắc Dependence Inversion của SOLID, từ đó dễ dàng cho việc testing và maintain mai này.
interface ConnectRepository {
fun doSomething()
}
class ConnectRepositoryImpl @Inject constructor(
private val connectManager: ConnectManager
) : ConnectRepository {
fun doSomething() {}
}
@Module
@InstallIn(SingletonComponent::class)
abstract class ConnectDeviceModule {
@Binds
@Singleton
abstract fun provideConnectRepository(
connectRepository: ConnectRepositoryImpl
): ConnectRepository
}
class ConnectUseCaseImpl @Inject constructor(
private val connectRepository: ConnectRepository,
) : ConnectUseCase {
fun doSomething() {
// Using connectRepository
}
}
Cụ thể trong ví dụ trên, mình khai báo 1 interface ConnectRepository thể hiện cho đối tượng ConnectRepositoryImpl. Từ đó khi sử dụng ConnectRepositoryImpl chúng ta sẽ sử dụng và gọi nó thông qua ConnectRepository.
Ưu điểm của việc dùng @Binds thay cho @Provides là nó giúp giảm lượng code được generate, như là Module Factory class. Từ đó tăng thời gian build và tăng kích thước file .apk và .aab của Project.
Trên đây là chia sẻ của mình về 1 số Annotation của Hilt Android. Mong sẽ giúp ích được ít nhiều mọi người. Hẹn mọi người ở bài viết sắp tới