4 minute read

  • 공식문서

  • Hilt는 Dagger와 마찬가지로 수동 종속 항목 삽입(DI)의 편의를 위한 Android용 종속항목 삽입 라이브러리 입니다.
  • Hilt는 모든 Android 클래스에 컨테이너를 제공하고 수명 주기를 자동으로 관리하는 방법을 제공합니다
  • Dagger기반으로 빌드되었으며 Dagger의 장점을 그대로 물려받았습니다. 일단 의존 설정먼저 해 봅시다.
//  project/build.gradle

buildscript {
    dependencies {
        //  hilt-android-gradle-plugin이 필요합니다.
        classpath 'com.google.dagger:hilt-android-gradle-plugin:2.28-alpha'
    }
}

//  app/build.gradle

...
apply plugin: 'kotlin-kapt'
apply plugin: 'dagger.hilt.android.plugin'

android {
    ...
    compileOptions {
        //  Hilt는 자바8을 사용한답니다.
        sourceCompatibility JavaVersion.VERSION_1_8
        targetCompatibility JavaVersion.VERSION_1_8
      }
}

dependencies {
    implementation "com.google.dagger:hilt-android:2.28-alpha"
    kapt "com.google.dagger:hilt-android-compiler:2.28-alpha"
}
  • @HiltAndroidApp : Hilt의 사용은 먼저 Application객체의 선언문에 @HiltAndroidApp을 붙임으로써 시작합니다. 이 어노테이션은 앱 최상위 Component 객체의 생성을 트리거 합니다. 이를 사용하지 않고도 DaggerXXX와 같은 컴포넌트를 직접 생성할 수 있기도 하다.

  • @AndroidEntryPoint : Application클래스에 @HiltAndroidApp를 설정하여 Component를 구성하면 @AndroidEntryPoint 가 설정된 다른 Android 클래스들은 종속 항목을 Component로부터 제공받을 수 있습니다. 여기서 말하는 Android 클래스는 다음과 같습니다.
    • Application(@HiltAndroidApp을 사용하여)
    • Activity : ComponentActivity의 서브클래스
    • Fragment : androidx.Fragment의 서브클래스, support library의 Fragment를 지원하지 않습니다.
    • View
    • Service
    • BroadcastReceiver
  • @AndroidEntryPoint를 설정한 클래스가 또 다른 Android 클래스에 종속되어 있으면 이 클래스에서 @AndroidEntryPoint를 붙여줘야 합니다. 예를들어 Activity 내부에 Fragment가 있는 상황에서 두 클래스 모두에게 애너테이션을 붙여야 한다는 말.

  • Dagger와 마찬가지로 Component에서 종속 항목을 가져오기 위해 변수명에 @Inject를 설정합니다
@AndroidEntryPoint
class ExampleActivity : AppCompatActivity() {
  @Inject lateinit var analytics: AnalyticsAdapter  //  private이면 안됩니다.
}
  • Component가 종속 항목의 인스턴스를 생성하는 방법을 정의하는 방법은 Dagger와 마찬가지로 @Inject constructor를, 또는 Module을 사용하는 것입니다.

  • @Inject constructor를 통해 생성자에서 주입

class AnalyticsAdapter @Inject constructor(
  private val service: AnalyticsService
) { ... }
  • Hilt Module : Dagger의 Module과 그 목적이 같습니다. 생성자에서 주입할 수 없는 경우 사용합니다. 이럴 때 @Module에서 다른 주입하는 방법을 정의할 수 있습니다. @Component[XXXModule]과 같이 Component 어노테이션에 표기되었던 Dagger모듈과는 달리 Hilt Module은 Module에 @InstallIn를 지정하여 Module을 사용할 Component를 표기합니다(없으면 컴파일 에러).
@Module
@InstallIn(ActivityComponent::class)
object AnalyticsModule { ... }
  • @Binds를 통한 인터페이스 인스턴스 삽입 : @Binds는 인터페이스의 인스턴스를 제공해야 할 때 사용할 구현체를 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)    //  이 모듈을 ActivityComponent에 include합니다.
abstract class AnalyticsModule {

  @Binds
  abstract fun bindAnalyticsService(
    analyticsServiceImpl: AnalyticsServiceImpl
  ): AnalyticsService   //  AnalyticsService의 구현체는 AnalyticsServiceImpl입니다.
}
  • @Provides를 통한 인스턴스 삽입 : Dagger와 마찬가지로 반환 타입이 일치하는 요청에 대해 인스턴스 주입을 해 줍니다.
@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)
  }
}
  • @Provides함수들 중에는 간혹 같은 타입을 반환하는 여러 함수들이 있을 수 있습니다. 이럴때 @Qualifier를 사용해 이 들을 구분해 줄 수 있습니다. 공식 문서에서는 OkHttpClient객체에 여러 Intercepter를 주입하는 방법에 대해 안내하고 있습니다. Qualifier는 다음과 같이 생성합니다.
@Qualifier
@Retention(AnnotationRetention.BINARY)
annotation class AuthInterceptorOkHttpClient

@Qualifier
@Retention(AnnotationRetention.BINARY)
annotation class OtherInterceptorOkHttpClient
  • 생성한 Qualifier는 @Provides함수에 설정해 줍니다.
@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를 선택하여 인스턴스를 주입 받습니다.
@Provides
  fun provideAnalyticsService(
    @AuthInterceptorOkHttpClient okHttpClient: OkHttpClient
  ): AnalyticsService {
      return Retrofit.Builder()
               .baseUrl("https://example.com")
               .client(okHttpClient)
               .build()
               .create(AnalyticsService::class.java)
  }
  • Hilt는 @ApplicationContext 및 @ActivityContext Qualifier를 기본적으로 제공합니다.

  • Hilt에는 Android 클래스의 생명주기에 맞춘 Component들을 기본적으로 제공합니다. 기본적으로 Hilt는 모든 바인딩이 Scope가 없지만 이러한 Component들을 사용하여 Scope설정을 해 줄 수 있습니다. 종류가 좀 많으므로 공식 문서를 참조하세요. Android 클래스용으로 생성된 구성요소

  • 이러한 모듈이 Install될 Component(SingletonComponent, ActivityComponent, ViewModelComponent…)와 각 모듈이 제공하는 종속 항목들의 Scope(@Singleton, @ActivityScoped, @ViewModelScoped…), 그리고 실제로 주입을 받는 Class의 타입(Application, Activity, ViewModel…)은 서로 매칭되어야 한다. 안그러면 오류가 발생.

  • 만약 Scope Annotation이 지정되어있지 않다면 해당 객체를 제공하는 Component에게 객체를 요청할 때 마다 새로운 인스턴스를 생성해서 준다. 예를들어 SingletonComponent에 install되는 A모듈이 B의 인스턴스를 제공하는데, Scope가 붙어있지 않다면, B의 인스턴스는 singleton으로 유지되지 않고 계속 생성된다. 따라서 C Activity가 이 인스턴스를 주입받는다고 했을 때 이 Activity에게 config change가 일어날 때 마다 새로운 B 인스턴스를 주입받는 것이다.

@Module
@InstallIn(ViewModelComponent::class)
object Module {
    @ViewModelScoped
    @Provides
    fun provideSomeClass(): SomeClass = SomeClass()
}
@HiltViewModel
class MainViewModel @Inject constructor(
    var someClass: SomeClass
): ViewModel() {
  ...
}

만약 Module의 InstallIn이 ActivityComponent이고, scope도 @ActivityScoped로 설정되어 있는데, 위 코드처럼 ViewModel에서 @Inject한다면, Hilt가 ViewModelComponent에서 SomeClass의 인스턴스를 제공하는 Module을 찾을 것이고, 해당 모듈은 ActivityComponent에 있기 때문에 찾을수 없다는 컴파일 에러가 발생할 것이다.

만약 Module을 통하지 않고 @Inject를 통해 암시적으로 제공되는 객체라면 클래스 선언문 위에 scope를 붙인다.

@ActivityScoped
class SomeClass @Inject constructor() {
    
}

이 클래스의 인스턴스는 의존 주입을 받는 대상과 같은 Component에 들어가게 된다. 예를들어 Activity에서 위 클래스를 주입받는다면 ActivityComponent에 들어가게 될 것이다.

  • @EntryPoint : @AndroidEntryPoint가 지원하지 않는 클래스에 종속 항목 삽입을 가능하게 해 줍니다. 예를들어 외부 라이브러리 등. @EntryPoint는 인터페이스에서만 사용할 수 있습니다. @InstallIn이 반드시 필요하며 EntryPoint.getXXX()을 사용하여 엑세스. EntryPointAccessors가 EntryPoint를 래핑하여 쉽게 컴포넌트를 가져올 수 있도록 도와줌.
class ExampleContentProvider : ContentProvider() {

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

  ...
  
  override fun query(...): Cursor {
      val appContext = context?.applicationContext ?: throw IllegalStateException()
      val hiltEntryPoint = EntryPointAccessors.fromApplication(appContext, ExampleContentProviderEntryPoint::class.java)
      val analyticsService = hiltEntryPoint.analyticsService()
      ...
    }
}
  • ExampleContentProvider는 ApplicationComponent로 부터 종속 항목을 ExampleContentProviderEntryPoint를 통해 제공 받으며 EntryPointAccessors를 ExampleContentProviderEntryPoint구현체를 전달받아 이 구현체를 통해 종속 항목을 삽입 받습니다.

  • Dagger에서의 보일러 플레이트를 줄이기 위해 Hilt가 제공하는 것에 대해 공식 문서는 아래와 같이 설명합니다
    1. 따로 수동으로 생성해 줘야 하는 Dagger와 Android 프레임워크 클래스를 통합하기 위한 Component들
    2. 위 Hilt가 자동으로 생성하는 Component와 함께 사용할 Scope 어노테이션들
    3. Application 또는 Activity와 같은 Android 클래스를 나타내는 사전 정의된 결합 Component들
    4. @ApplicationContext 및 @ActivityContext를 나타내는 사전 정의된 Qualifier들
  • Jetpack Viewmodel주입을 지원한다. @ViewModelInject, @Assisted private val savedStateHandle: SavedStateHandle
  • Jetpack Worker주입을 지원한다.

  • 찰스의 안드로이드 블로그에 자세한 내용이 있다고 함.

  • 참고 링크 Android 아키텍처 블루프린트 - hilt Dagger -> Hilt 마이그레이션 안내 Hilt 및 Jetpack 통합

Categories:

Updated: