[DI/Android] Hilt 실습(2) (with. Codelab)
지난 포스팅이 너무 길어져서 이어서 작성하려고 한다.
확실히 예제 코드를 보면서 한번 더 복습하니까 확실히 이해가 잘 되는 것 같다.
빨리 프로젝트에 적용해봐야지 둑흔🖤
4. 한정자 사용하기
한정자(Qualifier)는 특정 타입에 대해 여러 결합이 정의되어 있을 때, 각 결합을 식별하기 위해 사용
LoggerDataSource.kt
interface LoggerDataSource {
fun addLog(msg: String)
fun getAllLogs(callback: (List<Log>) -> Unit)
fun removeLogs()
}
LoggerLocalDataSource.kt
@Singleton
class LoggerLocalDataSource @Inject constructor(private val logDao: LogDao) : LoggerDataSource {
...
}
LoggerInMemoryDataSource.kt
@ActivityScoped
class LoggerInMemoryDataSource @Inject constructor() : LoggerDataSource {
...
}
이전 포스팅에서도 설명했지만 Hilt에 생성자 주입을 통해 바인딩 정보를 제공할 수 없는 경우 (인터페이스, 외부 라이브러리 클래스)
@Module 어노테이션을 이용하여 모듈을 생성하여 바인딩 정보를 제공한다.
LoggingModule.kt
@InstallIn(ApplicationComponent::class)
@Module
abstract class LoggingDatabaseModule {
@Singleton
@Binds
abstract fun bindDatabaseLogger(impl: LoggerLocalDataSource): LoggerDataSource
}
@InstallIn(ActivityComponent::class)
@Module
abstract class LoggingInMemoryModule {
@ActivityScoped
@Binds
abstract fun bindInMemoryLogger(impl: LoggerInMemoryDataSource): LoggerDataSource
}
LoggerDataSource의 다양한 구현(LoggerLocalDataSource, LoggerInMemoryDataSource)은 컨테이너의 범위가 서로 다르기 때문에 동일한 모듈을 사용할 수 없음. 또한 범위가 지정된 경우에는 @Binds 메서드에 범위 지정 어노테이션이 있어야 함
- LoggerLocalDataSource는 Application container - @Singleton
- LoggerInMemoryDataSource는 Activity container - @ActivityScoped
이렇게 모듈을 만들어 Hilt에게 어떤 인스턴스를 제공할 것인지를 알려준다.
하지만 이 상태에서 프로젝트를 빌드하려고 하면 DuplicateBindings 오류가 나타난다.
🤔 현재 Hilt에 같은 유형(LoggerDataSource)에 2개의 바인딩이 있기 때문에 어떤 것을 사용해야 할지 모르는 상태!
ButtonsFragment.kt
class ButtonsFragment : Fragment() {
@Inject lateinit var logger: LoggerDataSource
...
}
→ Hilt는 다음과 같은 상황에서 어떤 결합을 사용해야 할지 알 수 없다.
바인딩을 구분하는 한정자 사용
LoggingModule.kt
@Qualifier
annotation class DatabaseLogger
@Qualifier
annotation class InMemoryLogger
@InstallIn(ApplicationComponent::class)
@Module
abstract class LoggingDatabaseModule {
@DatabaseLogger
@Singleton
@Binds
abstract fun bindDatabaseLogger(impl: LoggerLocalDataSource): LoggerDataSource
}
@InstallIn(ActivityComponent::class)
@Module
abstract class LoggingInMemoryModule {
@InMemoryLogger
@ActivityScoped
@Binds
abstract fun bindInMemoryLogger(impl: LoggerInMemoryDataSource): LoggerDataSource
}
class ButtonsFragment : Fragment() {
@DatabaseLogger
@Inject lateinit var logger: LoggerDataSource
...
}
@Qualifier 어노테이션을 이용하여 바인딩을 구분할 어노테이션을 선언함
생성한 한정자는 각 구현을 제공하는 @Binds/@Provides 함수에 해당 어노테이션을 달아야 함
또한 Hilt가 인스턴스를 삽입하려는 지점에도 맞는 한정자를 사용하여 Hilt에게 어떤 인스턴스를 삽입해야 하는지 알려줌
5. Hilt에서 지원하지 않는 클래스 이용하기
Hilt에서는 일반적인 Android 구성요소를 지원한다. 하지만 Hilt에서 지원하지 않거나 Hilt를 사용할 수 없는 클래스에 필드를 삽입해야 하는 경우가 발생할 수 있다. 이러한 경우, @EntryPoint 어노테이션을 사용하여 해결한다.
→ @EntryPoint는 Hilt에서 관리하는 컨테이너에 코드가 처음 진입하는 지점
LogDao.kt
@Dao
interface LogDao {
...
@Query("SELECT * FROM logs ORDER BY id DESC")
fun selectAllLogsCursor(): Cursor
@Query("SELECT * FROM logs WHERE id = :id")
fun selectLogById(id: Long): Cursor?
}
LogsContentProvider.kt
class LogsContentProvider: ContentProvider() {
...
override fun query(
uri: Uri,
projection: Array<out String>?,
selection: String?,
selectionArgs: Array<out String>?,
sortOrder: String?
): Cursor? {
val code: Int = matcher.match(uri)
return if (code == CODE_LOGS_DIR || code == CODE_LOGS_ITEM) {
val appContext = context?.applicationContext ?: throw IllegalStateException()
val logDao: LogDao = getLogDao(appContext) // 에러
val cursor: Cursor? = if (code == CODE_LOGS_DIR) {
logDao.selectAllLogsCursor()
} else {
logDao.selectLogById(ContentUris.parseId(uri))
}
cursor?.setNotificationUri(appContext.contentResolver, uri)
cursor
} else {
throw IllegalArgumentException("Unknown URI: $uri")
}
}
...
}
getLogDao(appContent) 호출이 컴파일되지 않음
→ Hilt의 Application container에서 LogDao 종속 항목을 가져와 구현해야 함
🤔 ContentProvider는 Hilt에서 지원하는 클래스가 아님
따라서 @EntryPoint가 달린 새로운 인터페이스를 통해 접근해야 함
class LogsContentProvider: ContentProvider() {
@InstallIn(ApplicationComponent::class)
@EntryPoint
interface LogsContentProviderEntryPoint {
fun logDao(): LogDao
}
...
@InstallIn 어노테이션을 통해 진입점을 설치할 구성요소를 지정함
@EntryPoint 어노테이션을 통해 진입점을 명시해줌
→ 지금 코드에서는 Application container의 인스턴스에서 종속 항목을 가져와야 하기 때문에 인터페이스를 @EntryPoint 어노테이션으로 처리하고 ApplicationComponent에 설치함
private fun getLogDao(appContext: Context): LogDao {
val hiltEntryPoint = EntryPointAccessors.fromApplication(
appContext,
LogsContentProviderEntryPoint::class.java
)
return hiltEntryPoint.logDao()
}
}
진입점에 엑세스하기 위해서는 EntryPointAccessors의 정적 메서드를 사용해야 함
- EntryPointAccessors.fromApplication
- EntryPointAccessors.fromActivity
- EntryPointAccessors.fromFragment
- EntryPointAccessors.fromView
또한 매개변수는 구성 요소 인스턴스, 구성 요소 소유자 역할을 하는 @AndroidEntryPoint 객체여야 함
→ 매개변수로 전달하는 구성요소와 EntryPointAccessors 정적 메서드가 모두 @EntryPoint 인터페이스의 @InstallIn 어노테이션의 Android 클래스와 일치해야 함
reference>
- https://developer.android.com/codelabs/android-hilt?hl=ko#9