본문 바로가기
Programando/Android

[Android/Kotlin] Hilt

Hilt는 Android용 종속성 주입 라이브러리로, 수동 종속성 주입을 수행하는 boilerplate code를 줄여줍니다. Hilt는 프로젝트의 모든 Android 클래스에 컨테이너를 제공하고 수명 주기를 자동으로 관리함으로써 애플리케이션에서 DI(Dependency Injection) 를 실행하는 표준 방법을 정의합니다.

Hilt는 Dagger가 제공하는 컴파일 타임 정확성, 런타임 성능, 확장성 및 Android 스튜디오 지원의 이점을 누리기 위해 인기 있는 DI 라이브러리 Dagger를 기반으로 빌드되었습니다.

 

종속성 추가

먼저, project 수준의 build.gradle에서 hilt-android-gradle-plugin 플러그인을 추가합니다.

buildscript {
    ...
    dependencies {
        classpath 'com.google.dagger:hilt-android-gradle-plugin:2.38.1'
    }
}

 

그리고 Gradle 플러그인을 적용하고, app 모듈의 build.gradle에 종속성을 추가합니다. Hilt는 Java 8 기능을 사용하기 때문에 compileOptions에 Java 8 사용을 설정합니다.

plugins {
    ...
    id 'kotlin-kapt'
    id 'dagger.hilt.android.plugin'
}

android {
    ...
    compileOptions {
        sourceCompatibility JavaVersion.VERSION_1_8
        targetCompatibility JavaVersion.VERSION_1_8
    }
}

dependencies {
    ...
    implementation 'com.google.dagger:hilt-android:2.38.1'
    kapt 'com.google.dagger:hilt-compiler:2.38.1'
}

 

Hilt Application 클래스

Hilt를 사용하는 모든 app은 @HiltAndroidApp 어노테이션이 지정된 Application 클래스를 반드시 포함해야 합니다.

@HiltAndroidApp
class ExampleApplication : Application() { ... }

 

Android 클래스에 종속성 주입

Application 클래스에 한 번 Hilt를 설정하고, Application 수준 구성요소를 사용할 수 있게 되면 Hilt는 @AndroidEntryPoint 어노테이션이 있는 다른 Android 클래스들에 대해 종속성을 제공할 수 있습니다.

@AndroidEntryPoint
class ExampleActivity : AppCompatActivity() { ... }

 

Hilt는 현재 다음 Android 클래스들을 지원합니다.

  • Application(@HiltAndroidApp을 사용하여)
  • ViewModel(@HiltViewModel을 사용하여)
  • Activity
  • Fragment
  • View
  • Service
  • BroadcastReceiver

만약 Android 클래스에 @AndroidEntryPoint 어노테이션을 지정하면 해당 클래스에 종속된 Android의 클래스에도 어노테이션을 지정해야 합니다.

예를 들어, Fragment에 어노테이션을 지정하면 이 Fragment를 사용하는 모든 Activity에도 어노테이션을 지정해야 합니다.

 

@AndroidEntryPoint는 프로젝트의 각 Android 클래스에 대해 개별 Hilt 구성요소를 생성합니다. 이러한 구성요소들은 각각의 상위 클래스에서 종속성을 받을 수 있습니다.

구성요소에서 종속성을 가져오려면 @Inject 어노테이션을 사용하여 필드 주입을 실행해야 합니다.

@AndroidEntryPoint
class ExampleActivity : AppCompatActivity() {

  @Inject lateinit var analytics: AnalyticsAdapter
  ...
}

 

Hilt가 주입하는 클래스는 주입을 사용하는 다른 기본 클래스를 가질 수 있습니다. 이러한 클래스는 추상적인 경우 @AndroidEntryPoint 어노테이션이 필요하지 않습니다.

 

반응형

 

생성자 주입

@Inject 어노테이션을 사용해 클래스의 객체를 제공하는 방법을 Hilt에 알려줍니다.

class AnalyticsAdapter @Inject constructor(
  private val service: AnalyticsService
) { ... }

주석이 지정된 클래스 생성자의 매개변수는 그 클래스의 종속 항목입니다. 이 예에서 AnalyticsAdapter에는 AnalyticsService가 종속 항목으로 있습니다. 따라서 Hilt는 AnalyticsService의 객체를 제공하는 방법도 알아야 합니다.

 

Hilt 모듈

인터페이스나 외부 라이브러리의 클래스와 같이 소유하지 않은 유형들은 생성자 주입을 할 수 없습니다. 이럴 때는 Hilt 모듈을 사용하여 Hilt에 결합 정보를 제공할 수 있습니다. Hilt 모듈은 @Module 어노테이션이 지정된 클래스입니다. 이 모듈은 특정 유형의 객체를 제공하는 방법을 Hilt에 알려줍니다. Hilt 모듈에 @InstallIn 어노테이션을 지정하여 각 모듈을 사용하거나 설치할 Android 클래스를 Hilt에 알려야 합니다.

@Module
@InstallIn(ApplicationComponent::class)
class A { ... }

 

@Binds를 사용하여 인터페이스 객체 주입

@Binds 어노테이션은 인터페이스의 객체를 제공해야 할 때 사용할 구현을 알려줍니다.

해당 어노테이션이 달린 함수는 Hilt에게 다음과 같은 정보들을 제공합니다.

  • 함수 반환 유형 : 함수가 어떤 인터페이스의 객체를 제공하는지
  • 함수 매개변수 : Hilt에 제공할 구현이 무엇인지
interface AnalyticsService {
  fun analyticsMethods()
}

// Constructor-injected, because Hilt needs to know how to
// provide instances of AnalyticsServiceImpl, too.
class AnalyticsServiceImpl @Inject constructor(
  ...
) : AnalyticsService { ... }

@Module
@InstallIn(ActivityComponent::class)
abstract class AnalyticsModule {

  @Binds
  abstract fun bindAnalyticsService(
    analyticsServiceImpl: AnalyticsServiceImpl
  ): AnalyticsService
}

 

@Provides를 사용하여 객체 주입

@Provides 어노테이션은 클래스가 외부 라이브러리(Retrofit, OkHttpClient 또는 Room과 같은 클래스)에서 생성되었거나 Builder 패턴으로 객체를 생성한 경우에 이러한 유형의 객체를 제공하는 방법을 Hilt에 알립니다.

해당 어노테이션이 달린 함수는 Hilt에게 다음과 같은 정보들을 제공합니다.

  • 함수 반환 유형 : 함수가 어떤 유형의 객체를 제공하는지
  • 함수 매개변수 : 해당 유형의 종속성이 무엇인지
  • 함수 본문 : Hilt에 해당 유형의 객체를 제공하는 방법
@Module
@InstallIn(ActivityComponent::class)
object AnalyticsModule {

  @Provides
  fun provideAnalyticsService(
    // Potential dependencies of this type
  ): AnalyticsService {
      return Retrofit.Builder()
               .baseUrl("<https://example.com>")
               .build()
               .create(AnalyticsService::class.java)
  }
}

 

동일한 유형에 대해 여러 Binding 제공

종속성과 동일한 유형의 다른 구현을 제공하기 위해서는 여러 binding을 제공해야 합니다. qualifier를 사용하여 동일한 유형에 대해 여러 binding을 제공할 수 있습니다.

먼저, @Binds 또는 @Provides 메서드에 어노테이션을 지정하는 데 사용할 qualifier를 정의합니다.

@Qualifier
@Retention(AnnotationRetention.BINARY)
annotation class AuthInterceptorOkHttpClient

@Qualifier
@Retention(AnnotationRetention.BINARY)
annotation class OtherInterceptorOkHttpClient

 

그리고 Hilt는 각 qualifier에 일치하는 유형의 객체를 제공하는 방법을 알아야 합니다.

아래의 두 메서드는 동일한 반환 유형을 가지지만 qualifier는 두 가지의 서로 다른 binding으로 라벨을 지정합니다.

@Module
@InstallIn(ApplicationComponent::class)
object NetworkModule {

  @AuthInterceptorOkHttpClient
  @Provides
  fun provideAuthInterceptorOkHttpClient(
    authInterceptor: AuthInterceptor
  ): OkHttpClient {
      return OkHttpClient.Builder()
               .addInterceptor(authInterceptor)
               .build()
  }

  @OtherInterceptorOkHttpClient
  @Provides
  fun provideOtherInterceptorOkHttpClient(
    otherInterceptor: OtherInterceptor
  ): OkHttpClient {
      return OkHttpClient.Builder()
               .addInterceptor(otherInterceptor)
               .build()
  }
}

 

필드 또는 파라미터에 일치하는 qualifier를 추가하여 필요한 특정 유형을 주입할 수 있습니다.

// As a dependency of another class.
@Module
@InstallIn(ActivityComponent::class)
object AnalyticsModule {

  @Provides
  fun provideAnalyticsService(
    @AuthInterceptorOkHttpClient okHttpClient: OkHttpClient
  ): AnalyticsService {
      return Retrofit.Builder()
               .baseUrl("<https://example.com>")
               .client(okHttpClient)
               .build()
               .create(AnalyticsService::class.java)
  }
}

// As a dependency of a constructor-injected class.
class ExampleServiceImpl @Inject constructor(
  @AuthInterceptorOkHttpClient private val okHttpClient: OkHttpClient
) : ...

// At field injection.
@AndroidEntryPoint
class ExampleActivity: AppCompatActivity() {

  @AuthInterceptorOkHttpClient
  @Inject lateinit var okHttpClient: OkHttpClient
}

 

유형에 qualifier를 추가하는 경우, 해당 종속성을 제공하기 위한 가능한 모든 방법에 qualifier를 추가하는 것이 좋습니다. qualifier 없이 기본 또는 공통 구현을 떠나는 것은 오류가 발생하기 쉬우며, Hilt가 잘못된 종속성을 주입할 수 있습니다.

 

미리 정의된 Hilt의 Qualifiers

만약 Application이나 Activity로부터 Context 클래스가 필요할 수 있으므로, Hilt는 @ApplicationContext@ActivityContext qualifier를 제공합니다.

class AnalyticsAdapter @Inject constructor(
    @ActivityContext private val context: Context,
    private val service: AnalyticsService
) { ... }

 

Android 클래스용으로 생성된 구성요소

필드 주입을 실행할 수 있는 각 Android 클래스마다 @InstallIn 주석에 참조할 수 있는 관련 Hilt 구성요소가 있습니다. 각 Hilt 구성요소는 해당 Android 클래스에 결합을 주입해야 합니다.

그리고 Hilt는 해당 Android 클래스의 lifecylce에 따라 생성된 구성 요소 클래스의 인스턴스를 자동으로 만들고 삭제합니다.

Hilt Component Injector for 생성 위치 제거 위치
SingletonComponent Application Application#onCreate() Application destroyed
ActivityRetainedComponent N/A Activity#onCreate() Activity#onDestroy()
ViewModelComponent ViewModel ViewModel created ViewModel destroyed
ActivityComponent Activity Activity#onCreate() Activity#onDestroy()
FragmentComponent Fragment Fragment#onAttach() Fragment#onDestroy()
ViewComponent View View#super() View destroyed
ViewWithFragmentComponent View annotated with @WithFragmentBindings View#super() View destroyed
ServiceComponent Service Service#onCreate() Service#onDestroy()

 

구성요소의 기본 binding

각 Hilt 구성 요소에는 Hilt가 사용자 정의 binding에 종속성으로 주입할 수 있는 기본 binding 집합이 함께 제공됩니다. 이러한 binding은 특정 하위 클래스가 아닌 일반 Activity 및 Fragment 유형에 해당합니다. Hilt는 단일 Activity 구성요소 정의를 사용하여 모든 Activity들을 주입하기 때문입니다. 각 Activity에는 이 구성 요소의 인스턴스가 다릅니다.

Android Component Default Bindings
SingletonComponent Application
ActivityRetainedComponent Application
ViewModelComponent SavedStateHandle
ActivityComponent Application, Activity
FragmentComponent Application, Activity, Fragment
ViewComponent Application, Activity, View
ViewWithFragmentComponent Application, Activity, Fragment, View
ServiceComponent Application, Service

 

@ApplicationContext를 사용해 Application Context를 binding 할 수 있습니다.

class AnalyticsServiceImpl @Inject constructor(
  @ApplicationContext context: Context
) : AnalyticsService { ... }

// qualifier 없이도 Application binding을 사용할 수 있습니다.
class AnalyticsServiceImpl @Inject constructor(
  application: Application
) : AnalyticsService { ... }

 

 

@ActivityContext를 사용해 Activity Context를 binding 할 수 있습니다.

class AnalyticsAdapter @Inject constructor(
  @ActivityContext context: Context
) { ... }

// qualifier 없이도 Activity binding을 사용할 수 있습니다.
class AnalyticsAdapter @Inject constructor(
  activity: FragmentActivity
) { ... }

 

Hilt에서 지원하지 않는 클래스에 종속성 주입

Hilt가 지원하지 않는 클래스에 필드 주입을 하기 위해서는 @EntryPoint 어노테이션을 사용해 Entry point를 만들 수 있습니다. Entry Point는 Hilt에서 관리되는 코드와 그렇지 않은 코드 사이의 경계입니다. Entry point는 Hilt가 관리하는 객체의 그래프에 코드가 처음으로 들어가는 지점입니다. Entry point를 통해 Hilt가 관리하지 않는 코드를 사용하여 종속성 그래프 내에서 종속성을 제공할 수 있습니다.

예를 들어, Hilt는 Content Provider를 직접 지원하지 않습니다. Content Provider가 Hilt를 사용하여 일부 종속성을 가져오도록 하려면 원하는 각 binding 유형에 대해 @EntryPoint 어노테이션이 지정된 인터페이스를 정의하고 qualifier를 포함해야 합니다. 그리고 @InstallIn을 추가하여 Entry point를 설치할 구성요소를 지정합니다.

class ExampleContentProvider : ContentProvider() {

  @EntryPoint
  @InstallIn(SingletonComponent::class)
  interface ExampleContentProviderEntryPoint {
    fun analyticsService(): AnalyticsService
  }

  ...
}

 

Entry point에 접근하기 위해서는 EntryPointAccessors로부터 적절한 정적 메서드를 사용해야 합니다. 매개변수는 구성요소의 인스턴스 또는 구성요소의 홀더 역할을 하는 @AndroidEntryPoint 객체여야 합니다. 매개변수로 전달하는 구성요소와 EntryPointAccessors의 정적 메서드가 모두 EntryPoint 인터페이스의 @InstallIn 어노테이션에 있는 Android 클래스와 일치하는지 확인해야 합니다.

class ExampleContentProvider: ContentProvider() {
    ...

  override fun query(...): Cursor {
    val appContext = context?.applicationContext ?: throw IllegalStateException()
    val hiltEntryPoint =
      EntryPointAccessors.fromApplication(appContext, ExampleContentProviderEntryPoint::class.java)

    val analyticsService = hiltEntryPoint.analyticsService()
    ...
  }
}

 

그 외

자료 참조

https://developer.android.com/training/dependency-injection/hilt-android

반응형