Hilt
- 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가 제공하는 것에 대해 공식 문서는 아래와 같이 설명합니다
- 따로 수동으로 생성해 줘야 하는 Dagger와 Android 프레임워크 클래스를 통합하기 위한 Component들
- 위 Hilt가 자동으로 생성하는 Component와 함께 사용할 Scope 어노테이션들
- Application 또는 Activity와 같은 Android 클래스를 나타내는 사전 정의된 결합 Component들
- @ApplicationContext 및 @ActivityContext를 나타내는 사전 정의된 Qualifier들
- Jetpack Viewmodel주입을 지원한다. @ViewModelInject, @Assisted private val savedStateHandle: SavedStateHandle
-
Jetpack Worker주입을 지원한다.
-
찰스의 안드로이드 블로그에 자세한 내용이 있다고 함.
- 참고 링크 Android 아키텍처 블루프린트 - hilt Dagger -> Hilt 마이그레이션 안내 Hilt 및 Jetpack 통합