ANDROID/Android DI

[DI/Android] Hilt 실습(2) (with. Codelab)

주 녕 2022. 2. 12. 21:41
728x90

지난 포스팅이 너무 길어져서 이어서 작성하려고 한다.

확실히 예제 코드를 보면서 한번 더 복습하니까 확실히 이해가 잘 되는 것 같다.

빨리 프로젝트에 적용해봐야지 둑흔🖤

 

[DI/Android] Hilt 라이브러리 이해하기

Hilt 2020년 6월 Google에서 발표한 Android 전용 DI 라이브러리 Hilt는 프로젝트의 모든 Android 클래스에 컨테이너를 제공하고 수명 주기를 자동으로 관리함으로써 애플리케이션에서 DI를 사용하는 표준

junyoung-developer.tistory.com

 

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
728x90