https://developer.android.com/training/dependency-injection/hilt-android#hilt-modules
plugins {
kotlin("kapt")
id("com.google.dagger.hilt.android") version "2.44" apply false
android {
// Hilt는 Java8 기능을 사용하기에 프로젝트에서 Java8 사용 설정
compileOptions {
sourceCompatibility = JavaVersion.VERSION_1_8
targetCompatibility = JavaVersion.VERSION_1_8
}
}
dependencies {
implementation("com.google.dagger:hilt-android:2.44")
kapt("com.google.dagger:hilt-android-compiler:2.44")
}
kapt {
correctErrorTypes = true
}
Hilt application class
Hilt를 사용하는 모든 App은 @HiltAndroidApp 어노테이션으로 지정된 Application클래스를 포함해야 함.
@HiltAndroidApp은 application-level dependency container 역할을 하는 Application의 기본 클래스를 포함해 Hilt의 코드 생성(code generation)을 트리거함.
@HiltAndroidApp
class ExampleApplication : Application() {
...
}
생성된 Hilt Component는 Application 객체의 lifeCylce에 attach되며 이와 관련한 dependencies들을 제공함. 또한, 이 Application 클래스는 App의 상위 Component이므로 다른 Component는 이 상위 Component에서 제공하는 dependencies들에 액세스할 수 있음.
@AndroidEntryPoint
class ExampleActivity : AppCompatActivity() {
...
}
Hilt currently supports the following Android classes:
만약, Android class(B클래스)에 @AndroidEntryPoint 어노테이션을 지정하면, (A클래스의 생성자로 B클래스를 받을 때)
B클래스에 의존하는 A클래스 역시 어노테이션을 지정해줘야 함.
ex)
if you annotate a fragment(B클래스), then you must also annotate any activities(A클래스) where you use that fragment.
@AndroidEntryPoint는 프로젝트의 각 Android class에 관한 개별 Hilt Component를 생성.
이러한 Component는 Component 계층 구조에 설명된 대로 해당하는 각 상위 class에서 dependencies들을 받을 수 있다.
Hilt에 binding 정보(Component로부터 필요한 dependencies의 인스턴스를 제공하는 방법)를 제공하는 한 가지 방법은 Constructor injection.
=> Consturctor injection -> Hilt binding
ex)
---------------------------------
기본적인 코틀린 생성자 사용하는 ex
---------------------------------
class A(val name: String, val age: Int)
위는 name과 age 프로퍼티를 가진 A 클래스
A의 인스턴스를 val a = A(conatuseus, 30) 이런식으로 간단하게 생성 가능
클래스명 옆의 괄호가 프로퍼티의 선언과 초기화를 같이 함.
이 경우 constructor 키워드가 생략 가능함.
/////////////////////////////////////////////////
----------------------------------------------------------
주 생성자가 어노테이션이나 접근 제어자(private 등)을 가지면
constructor 키워드 생략불가.
그래서 @Inject 옆에 constructor를 적어주는 거.
----------------------------------------------------------
class AnalyticsAdapter @Inject constructor(
private val service: AnalyticsService
) {
...
}
class의 생성자에서 @Inject 어노테이션을 사용함으로써 Hilt에게 class에 인스턴스를 제공하는 방법을 알려주게 됨.
class AnalyticsAdapter @Inject constructor(
private val service: AnalyticsService
) {
...
}
어노테이션이 지정된 class 생성자의 매개변수는 그 클래스의 dependencies들이다.
위 예에선 AnalyticsAdapter가 AnalyticsService를 dependency로 가지고 있음.
따라서 Hilt는 AnalyticsService의 인스턴스를 어떻게 제공하는지에 관한 방법을 알아야 함.
Hilt Modules
interface는 constructor-inject 할 수 없음.
또한 외부 라이브러리의 class와 같이 소유하지 않은 타입도 constructor-inject할 수 없음.
이럴 때는 Hilt 모듈을 사용하여 Hilt에 Binding infomation을 제공할 수 있음.
(binding information = Component로부터 필요한 dependencies의 인스턴스를 제공하는 방법)
Hilt 모듈은 @Module로 어노테이션이 지정된 class.
Dagger 모듈과 마찬가지로 이 모듈은 특정 타입의 인스턴스를 제공하는 방법을 Hilt에 알려준다.
그러나 Dagger 모듈과는 달리, Hilt 모듈에 @InstallIn 어노테이션을 달아 각 모듈이 사용되거나 설치될 Android class를 Hilt에게 알려주어야 함.
Hilt 모듈에 제공하는 dependency는 Hilt 모듈을 설치하는 Android class와 연관되어 있는 모든 생성된 component에서 사용할 수 있음.
AnalyticsService가 interface라면 이 interface를 constructor injection 할 수 없다.
대신 Hilt 모듈 내부에 @Binds 어노테이션이 지정된 추상 함수를 생성함으로써 Hilt에 binding information를 제공.
(binding information = Component로부터 필요한 dependencies의 인스턴스를 제공하는 방법)
@Binds 어노테이션은 interface의 인스턴스를 제공해야 할 때 사용해야 할 implementation을 Hilt에 알려줌.
interface AnalyticsService {
fun analyticsMethods()
}
// Hilt는 AnalyticsServiceImpl 인스턴스도 제공하는 방법을 알아야
// 하므로 Constructor-injection 수행
class AnalyticsServiceImpl @Inject constructor(
...
) : AnalyticsService {
...
}
@Module
@InstallIn(ActivityComponent::class)
abstract class AnalyticsModule {
// Hilt 모듈 내부에 @Binds 어노테이션이 지정된 추상함수를
// 생성함으로써 Hilt에게 interface의 binding information을 제공
@Binds // 사용해야 할 implementation을 Hilt에게 알려줌.
abstract fun bindAnalyticsService( // AnalyticsService는 interface
analyticsServiceImpl: AnalyticsServiceImpl // 함수 매개변수는 Hilt에게 제공할 implementation을 알려줌.
) : AnalyticsService // 함수 리턴타입은 Hilt에게 해당 함수가 어떤 interface의 인스턴스를 제공하는지 알려줌.
}
개발자는 Hilt를 통해 AnalyticsModule의 dependency를 ExampleActivity에 삽입하기를 원하기 때문에 Hilt 모듈 AnalyticsModule @InstallIn(ActivityComponent::class) 어노테이션을 지정함.
이 @InstallIn(ActivityComponent::class) 어노테이션은 AnalyticsModule의 모든 dependency를 App의 모든 activities에서 사용할 수 있음을 의미.
타입을 constructor-injection 할 수 없는 것은 인터페이스만이 아니다.
ex)
만약, AnalyticsService class를 직접 소유하지 않는 경우에는
(위 예제에서는 소유하고 있지만, 소유하지 않았다고 가정한다면)
Hilt 모듈 내부에 함수를 생성하고 이 함수에 @Provides 어노테이션을 지정하여 Hilt에게 이 타입의 인스턴스를 제공하는 방법을 알릴 수 있다.
@Module
@InstallIn(ActivityComponent.class)
object AnalyticsModule {
// Hilt 모듈 내부에 함수를 생성하고 이 함수에 @Provides 어노테이션을 지정하여
// Hilt에게 이 타입의 인스턴스를 제공하는 방법을 알림.
@Provides
fun provideAnalyticsService(
// 함수 매개변수는 Hilt에게 해당 타입의 dependencies들을 알려줌.
'''
Potential dependencies of this type
'''
) : AnalyticsService { // 함수 리턴타입은 Hilt에게 함수가 어떤 타입의 인스턴스를 제공하는지 알려줌.
// 함수 body는 Hilt에게 해당 타입의 인스턴스를 제공하는 방법을 알려줌.
return Retrofit.Builder()
.baseUrl("https://example.com")
.build()
.create(AnalyticsService::class.java)
}
}
dependencies와 동일한 타입의 다양한 implementations을 제공하는 Hilt가 필요한 경우에는 Hilt에게 multiple binding을 제공해야 한다.
qualifiers(한정자)를 사용하여 동일한 타입에 대해 multiple bindings을 정의할 수 있다.
qualifiers는 특정 타입에 대해 다수의 binding이 정의되어 있을 때
그 타입의 특정 binding을 식별하는 데 사용하는 어노테이션.
ex)
AnalyticsService Call(호출)을 intercept해야 한다면
interceptor와 함께 OkHttpClient 객체(object)를 사용할 수 있다.
다른 서비스에서는 호출을 다른 방식으로 intercept해야 할 수도 있는데,
이 경우에는 서로 다른 두 가지 OkHttpClient implementation을 어떻게 제공하는지에 관한 방법을 Hilt에게 알려야 한다.
@Qualifier
@Retention(AnnotationRetention.BINARY)
annotation class AuthInterceptorOkHttpClient
@Qualifier
@Retention(AnnotationRetention.BINARY)
annotation class OtherInterceptorOkHttpClient
그런 다음, Hilt는 각 qualifer와 일치하는 타입의 인스턴스를 어떻게 제공하는지에 대한 방법을 알아야 함. 이 경우에는 @Provides와 함께 Hilt 모듈을 사용하면 됨. 두 메서드 모두 동일한 리턴타입을 갖지만 qualifers는 다음과 같이 두 가지의 서로 다른 binding으로 메서드에 label을 지정함.
@Module
@InstallIn(SingletonComponent::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()
}
}
다음과 같이 field 또는 매개변수에 해당 qualifer로 어노테이션함으로써 필요한 특정 타입을 injection할 수도 있음.
// 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를 타입에 추가한다면, 그 dependency를 제공하는 가능한 모든 방법에 qualifier를 추가하는 것이 좋다. ->
기본 또는 일반 implementation을 qualifier 없이 그대로 두면 에러가 발생하기 쉬우며 Hilt가 잘못된 dependency를 삽입할 수도 있기 때문
만약, 위 예의 AnalyticsAdapter 클래스에서 activity의 context가 필요하다고 가정하면, 다음과 같이 하면 됨.
class AnalyticsAdapter @Inject constructor(
@ActivityContext private val context: Context,
private val service: AnalyticsService
) {
...
}
각 Hilt Component는 해당하는 Android Class에 binding을 inject하게 된다.
ex) 위 예제 대부분에서는 Hilt 모듈에 ActivityComponent를 사용했음.
Hilt는 다음 Component를 제공한다.
Hilt는 해당 Android class의 수명 주기에 따라 생성된 Component classes들의 인스턴스를 자동으로 만들고 제거한다.
이 예에서 Hilt는 다른 타입의 dependency로 또는 field injection을 통해(ExampleActivity에서와 같이) AnalyticsAdapter를 제공할 때마다 AnalyticsAdapter의 새 인스턴스를 제공한다.
그러나
Hilt는 특정 Component로 범위를 지정할 수 있도록 binding을 제공할 수도 있다.
Hilt는 binding의 범위가 지정된 Component의 인스턴스마다 한 번만 범위가 지정된 binding을 생성하며, 이 binding에 관한 모든 요청은 동일한 인스턴스를 공유하게 됨.
생성된 각 Component의 Scope 어노테이션은 다음과 같다.
ex)
만약, @ActivityScoped를 사용하여 AnalyticsAdapter의 Scope를 ActivityComponent로 지정하면 Hilt는 해당 Activity의 lifeCycle동안 동일한 AnalyticsAdapter 인스턴스를 제공함.
@ActivityScoped
class AnalyticsAdapter @Inject constructor(
private val service: AnalyticsService
) {
...
}
AnalyticsService에 ExampleActivity 뿐만 아니라 앱의 모든 곳에서 매번 동일한 인스턴스를 사용해야 하는 internal state가 있다고 가정한다면,
AnalyticsService의 Scope를 SingletonComponent로 지정하는 것이 적절.
결과적으로 Component는 AnalyticsService의 인스턴스를 제공해야 할 때마다 매번 동일한 인스턴스를 제공하게 됨.
Hilt 모듈에서 어떻게 binding의 Scope를 Component로 지정하는지는 다음과 같다.
binding의 Scope는 binding이 install된 Component의 Scope와 일치해야 하므로 이 예에서는 ActivityComponent 대신 SingletonComponent에 AnalyticsService를 install해야 된다.
// If AnalyticsService is an interface.
@Module
@InstallIn(SingletonComponent::class)
abstract class AnalyticsModule {
@Singleton // @InstallIn(SingletonComponent::class)와 일치해야 함.
@Binds
abstract fun bindAnalyticsService(
analyticsServiceImpl: AnalyticsServiceImpl
): AnalyticsService
}
// If you don't own AnalyticsService.
@Module
@InstallIn(SingletonComponent::class)
object AnalyticsModule {
@Singleton // @InstallIn(SingletonComponent::class)와 일치해야 함.
@Provides
fun provideAnalyticsService(): AnalyticsService {
return Retrofit.Builder()
.baseUrl("https://example.com")
.build()
.create(AnalyticsService::class.java)
}
}
Component에 Module을 install하면 이 Component의 다른 bindings 또는 Component 계층 구조에서 그 아래에 있는 하위 Component의 다른 binding의 dependency로 install된 Module의 binding에 Access 할 수 있다.
각 Hilt Component는 Hilt가 고유한 custom binding에 dependency로 삽입할 수 있는 default binding set와 함께 제공됨.
이러한 binding은 general Activity 및 Fragment Type에 해당하며 특정 SubClass에는 해당되지 않는다. -> 이는 Hilt가 모든 Activity를 삽입하는 데 single activity Component 정의를 사용하기 때문. 각 Activity에는 이 Component의 다른 인스턴스가 있음.
class AnalyticsServiceImpl @Inject constructor(
@ApplicationContext context: Context
) : AnalyticsService {
...
}
// The Application binding is available without qualifiers.
class AnalyticsServiceImpl @Inject constructor(
application: Application
) : AnalyticsService {
...
}
class AnalyticsAdapter @Inject constructor(
@ActivityContext context: Context
) {
...
}
// The Activity binding is available without qualifiers.
class AnalyticsAdapter @Inject constructor(
activity: FragmentActivity
) {
...
}
예를 들어,
Hilt는 content providers를 직접 지원하지 않는데, Hilt를 사용하여 일부 dependecies들을 가져오도록 content providers를 사용하기 원한다면,
원하는 binidng type 마다 @EntryPoint 어노테이션이 지정된 interface를 정의하고 qulifiers를 포함해야 한다.
그리고 다음과 같이 @InstallIn을 추가하여 entryPoint를 install할 Component를 지정.
class ExampleContentProvider : ContentProvider() {
@EntryPoint
@InstallIn(SingletonComponent::class)
interface ExampleContentProviderEntryPoint {
fun analyticsService(): AnalyticsService
}
...
}
EntryPont에 Access하려면 EntryPointAccessors의 적절한 static 메서드를 사용해야 한다.
또한, 매개변수는 Component 인스턴스이거나 Component holder 역할을 하는 @AndroidEntryPoint 객체여야 함.
매개변수로 전달하는 Component와 EntryPointAccessors static 메서드가 모두 @EntryPoint interface의 @InstallIn 어노테이션에 있는 Android Class와 일치하는지 확인
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()
...
}
}
위 예는 EntryPoint가 SingletonComponent에 install되어 있으므로 ApplicationContext를 사용하여 entry point를 retrieve해야 한다.
만약, retrieve하려는 binding이 ActivityComponent에 있다면 ActivityContext를 대신 사용.
Hilt는 Dagger Dedenpency 라이브러리를 기반으로 빌드되어 Dagger를 Android 애플리케이션에 통합하는 표준 방법을 제공.
Dagger와 관련해 Hilt의 목표는 다음과 같다.
Android 운영체제는 많은 자체 프레임워크 클래스를 인스턴스화하므로 Android 앱에서 Dagger를 사용하려면 상당한 양의 boilerplate를 작성해야 한다. 이를 위해 Hilt는 Android 애플리케이션에서 Dagger 사용과 관련된 boilerplate code를 줄임.
Dagger 및 Hilt Code는 동일한 codebase에 공존할 수 있다.
그러나 대부분의 경우 Android에서 Dagger의 모든 사용을 관리하려면 Hilt를 사용하는 것이 가장 베스트
각 클래스별로 해당되는 어노테이션을 붙여주면 Hilt가 의존성을 보관하기 위한 Component라는 보관함을 만들고 Component 안에 생성된 의존객체는 다른 Component에 의존성을 주입할 수 있다.
앱이 객체를 요청할 때마다 새로운 객체가 만들어지는 것을 방지하기 위해
각 의존객체에는 Scope LifeCycle을 지정해줄 수 있다.
만약 A라는 의존객체에 @Singleton 어노테이션을 붙이면 앱 전체에서 하나의 객체만 만들 수 있게 된다.
만약 A라는 의존객체에 @ActivityScoped 어노테이션을 붙이면 해당 액티비티에서는 오직 하나의 의존객체만이 만들어질 수 있다.
Component 계층정책에 따라 하위 Component는 상위 scope를 가진 의존객체에 접근할 수 있다.
Component 내부에 의존객체를 만들었으면 이 의존객체를 주입 받을 객체를 연결하는 (=Binding) 과정을 수행하면 된다.
Hilt에서는 의존성을 제공할 객체와 주입받을 객체 양쪽에 @Inject 어노테이션을 붙이면서 바인딩이 이루어지는데, 이때, 의존성을 주입받을 객체가 Component를 가지고 있어야만 의존성을 주입받을 수 있기 때문에 주입받을 클래스에 @AndroidEntryPoint를 붙여줘서 Component를 만들어줘야 한다.
Hilt는 field, construtor 2가지 방법으로 의존성을 주입받을 수 있다.
// AnalyticsAdapter Instance를 ExampleActivity의 analytics field에 주입하기 위해서
// 주입받을 클래스에 @AndroidEntryPoint를 붙여 Component를 생성하고
// analytics field에 @Inject을 붙여 주입받을 Field임을 Hilt에게 알려준다.
class AnalyticsAdapter @Inject constructor() {
...
}
@AndroidEntryPoint
class ExampleActivity : AppCompatActivity() {
@Inject lateinit var analytics : AnalyticsAdapter
}
/*
주입받을 클래스에 @androidEntryPoint를 붙이고
생성자 쪽에 @Inject를 붙여 의존성을 주입하면 됨.
즉, Hilt에게
AnalyticsAdapter라는 의존성을 제공할 객체의 정보를 알려주고
ExampleActivity에서는 자신의 Component에 AnalyticsAdapter를 의존성으로 주입받을 것이라는 것을 알려주는 것이다.
*/
class AnalyticsAdapter @Inject constructor() {
...
}
@AndroidEntryPoint
class ExampleActivity @Inject constructor(
var analytics : AnalyticsAdapter
) : AppCompatActivity() {
...
}
모듈 내부의 모든 의존객체는 다른 Component에 주입할 수 있지만, 단, interface 혹은 외부 라이브러리의 인스턴스는 Hilt가 주입할 수 없음.
따라서, 이것들은 @Provides, @Binds 어노테이션을 이용해서 Hilt에게 정보를 알려주게 된다.
외부 라이브러리 혹은 빌드패턴 객체를 주입받아야 될 경우
@Provides 어노테이션을 이용한다.
// AnalyticsModule 클래스는 @Module 어노테이션을 통해 Hilt Module로 선언
// @InstallIn(ActivityComponent::class)를 통해
// AnalyticsModule이 ActivityComponent안에 설치됨.(scope는 Activity)
@Module
@InstallIn(ActivityComponent.class)
object AnalyticsModule {
// Hilt 모듈 내부에 함수를 생성하고 이 함수에 @Provides 어노테이션을 지정하여
// Hilt에게 이 AnalyticsModule이 AnalyticsService 인스턴스의 의존성을 제공하는 방법을 알림.
// 즉, 외부에 이 AnalyticsService 의존성을 제공할 수 있게 됨.
@Provides
fun provideAnalyticsService(
// 함수 매개변수는 Hilt에게 해당 타입의 dependencies들을 알려줌.
'''
Potential dependencies of this type
'''
) : AnalyticsService { // 함수 리턴타입은 Hilt에게 함수가 어떤 타입의 인스턴스를 제공하는지 알려줌.
// 함수 body는 Hilt에게 해당 타입의 인스턴스를 제공하는 방법을 알려줌.
return Retrofit.Builder()
.baseUrl("https://example.com")
.build()
.create(AnalyticsService::class.java)
}
}
// If AnalyticsService is an interface.
interface AnalyticsService {
fun analyticsMethods()
}
// AnalyticsService의 구현체인 AnalyticsServiceImpl 인스턴스를
// fun bindAnalyticsService 매개변수로 받고
// bindAnalyticsService함수는 interface인 AnalyticsService를 반환
// interface를 의존객체로 제공하는 것이므로 @Binds를 붙인다.
// 단 이때는 abstract로 만들어줘야 한다.
class AnalyticsServiceImpl @Inject constructor(
...
) : AnalyticsService {
...
}
@Module
@InstallIn(SingletonComponent::class)
abstract class AnalyticsModule {
@Singleton // @InstallIn(SingletonComponent::class)와 일치해야 함.
@Binds
abstract fun bindAnalyticsService(
analyticsServiceImpl: AnalyticsServiceImpl
): AnalyticsService
}