🔥 Hilt
- 종속성 주입을 위해 사용하는
Android Jetpack
의 라이브러리 - 프로젝트의 모든 Android 클래스에 컨테이너를 제공하고 자동으로 수명주기를 관리한다.
✨ Hilt 사용의 장점
- DI 라이브러리인
Dagger
를 기반으로 빌드되어,Dagger
가 제공하는 컴파일 시간 정확성, 런타임 성능, 확장성 및Android Studio
지원의 이점을 누릴 수 있다. - 안드로이드 애플리케이션에서
Dagger
사용과 관련된 많은 보일러플레이트 코드를 줄일 수 있다. - 컴포넌트에 사용할 Scope 어노테이션을 자동으로 생성해준다.
Application
및Activity
와 같은 Android 클래스를 나타내는 미리 정의된 바인딩이 있다.-
@ApplicationContext
및@ActivityContext
를 나타내는 미리 정의된 qualifier가 있다. - DI 구현의 장점
- 코드의 재사용 용이
- 리팩토링 용이
- 테스트 용이
✨ Hilt 사용을 위한 환경 구축
1️⃣ 프로젝트의 루트 build.gradle
파일에 hilt-android-gradle-plugin
추가
buildscript {
...
ext.hilt_version = '2.33-beta'
dependencies {
...
classpath "com.google.dagger:hilt-android-gradle-plugin:$hilt_version"
}
}
2️⃣ app/build.gradle
파일에 apply plugin
과 dependencies
추가
Hilt
와DataBinding
을 모두 사용하는 프로젝트의 경우, Android Studio 버전이 4.0 이상이어야 한다. ```xml … apply plugin: ‘kotlin-kapt’ apply plugin: ‘dagger.hilt.android.plugin’
android { … }
dependencies { implementation “com.google.dagger:hilt-android:$hilt_version” kapt “com.google.dagger:hilt-compiler:$hilt_version” }
<br>
#### 3️⃣ `app/build.gradle`에 Java 8 `compileOptions` 추가
```xml
android {
...
compileOptions {
sourceCompatibility JavaVersion.VERSION_1_8
targetCompatibility JavaVersion.VERSION_1_8
}
}
✨ Hilt 애플리케이션 클래스 생성
Hilt
를 사용하는 모든 앱에는@HiltAndroidApp
어노테이션이 있는Application
클래스가 존재해야 한다.@HiltAndroidApp
은 애플리케이션 레벨의 종속성 컨테이너를 제공하는 베이스 클래스를 포함하는Hilt
의 코드를 생성한다.@HiltAndroidApp class ExampleApplication : Application() { … }
✨ Android 클래스에 종속성 삽입
Hilt
가 Application 클래스에 설정되고 애플리케이션 레벨의 구성요소를 사용할 수 있게 되면,Hilt
는@AndroidEntryPoint
어노테이션이 있는 다른 Android 클래스에 종속성을 제공할 수 있다.Hilt
가 지원하는 Android 클래스- Application (
@HiltAndroidApp
사용) - ViewModel (
@HiltViewModel
사용) - Activity
- Fragment
- View
- Service
- BroadcastReceiver
- Application (
@AndroidEntryPoint
를 이용하여 Android 클래스에 어노테이션을 달아주었다면, 해당 클래스에 종속된 Android 클래스에도 어노테이션을 추가해주어야 한다.@AndroidEntryPoint
는 프로젝트의 Android 클래스에 대해 개별적인Hilt
컴포넌트를 생성한다.- 이러한
Hilt
컴포넌트는 상위 클래스로부터 종속성 주입을 받을 수 있다. - 컴포넌트로부터 종속성을 얻으려면 필드 주입을 위한
@Inject
를 사용해야 한다.Hilt
에 의해 주입된 필드는private
일 수 없다.Hilt
를 통해private
필드를 주입한 경우, 컴파일 에러가 발생한다.
Hilt
가 주입한 클래스는 주입을 사용한 다른 베이스 클래스를 지닐 수 있다.- 이러한 클래스들이 추상 클래스일 경우,
@AndroidEntryPoint
어노테이션이 필요하지 않다.
@AndroidEntryPoint
class ExampleActivity : AppCompatActivity() {
@Inject lateinit var analytics: AnalyticsAdapter
...
}
✨ Hilt binding 정의
- 필드 주입을 수행하기 위해서는
Hilt
가 해당 컴포넌트에서 필요한 종속성의 인스턴스를 제공하는 방법을 알아야 한다. - binding은 종속 타입의 인스턴스를 제공하는데에 필수적인 정보를 포함한다.
Hilt
에 바인딩 정보를 제공하기 위해서 constructor injection을 사용한다.- 클래스의 생성자에서
@Inject
어노테이션을 사용하여 해당 클래스에 인스턴스를 제공하는 방법을Hilt
에 알린다.class AnalyticsAdapter @Inject constructor( private val service: AnalyticsService ) { ... }
@Inject
어노테이션으로 처리된 생성자의 파라미터들은 해당 클래스의 종속성이다.AnalyticsAdapter
는AnalyticsService
를 종속성으로 지니므로,Hilt
는AnalyticsService
의 인스턴스를 어떻게 제공하는지에 대한 방법을 반드시 알고있어야 한다.
✨ Hilt modules
- 인터페이스, 외부 라이브러리의 클래스 등 특정 타입은 constructor injection이 불가능 할 수 있다.
- 이러한 경우,
Hilt module
을 이용하여 바인딩 정보를Hilt
에 제공할 수 있다. Hilt module
은@Module
어노테이션이 있는 클래스이며,Hilt
에 특정 타입의 인스턴스를 제공하는 방법을 알린다.Dagger module
과는 달리Hilt module
에는 각 모듈이 사용되거나 설치될 Android 클래스를Hilt
에 알리는@InstallIn
어노테이션을 반드시 추가해야 한다.Hilt module
에서 제공하는 종속성은Hilt module
을 설치하는 Android 클래스와 연결된 모든 생성된 컴포넌트에서 사용할 수 있다.Hilt
의 코드 생성은Hilt
를 사용하는 모든Gradle module
에 접근해야 하므로, Application 클래스를 컴파일하는Gradle module
역시 모든Hilt module
과 constructor-injected 클래스를 지니고 있어야 한다.
✔️ @Binds
를 사용하여 인터페이스 인스턴스 주입
- constructor injection이 불가능할 경우,
Hilt module
내의@Binds
어노테이션을 이용하여 추상 함수를 만들어서 바인딩 객체와 함께 Hilt를 제공할 수 있다. @Bind
어노테이션은Hilt
가 인터페이스의 인스턴스를 필요로 할 때 어떤 implementation을 사용해야할지 알려준다.@Bind
어노테이션이 있는 함수는 다음의 정보를Hilt
에 알려준다- 함수의 리턴타입은 함수가 인스턴스를 제공하는 인터페이스를
Hilt
에 알려준다. - 함수의 파라미터는 어떤 implementation을 제공해야할지
Hilt
에 알려준다. ```kotlin 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 }
* `Hilt module`인 `AnalyticsModule`은 `Hilt`가 `ExampleActivity`로 종속성을 주입하도록 하기 위해 `@InstallIn(ActivityComponent::class)` 어노테이션으로 처리 되었다.
* `@InstallIn(ActivityComponent::class)` 어노테이션은 `AnalyticsModule`의 모든 종속성을 앱의 모든 액티비티에서 사용할 수 있음을 의미한다.
<br>
#### ✔️ `@Provides`를 사용하여 인스턴스 주입
* 인터페이스는 타입을 constructor-inject 할 수 없는 유일한 경우가 아니다.
* constructor-injection은 외부 라이브러리(`Retrofit`, `Room` 등)의 클래스 등과 같은 소유하고 있지 않은 클래스 및 빌더 패턴으로 생성된 인스턴스에서도 불가능하다.
* 이러한 경우, Hilt module 내부에 함수를 만들고 해당 함수를 `@Provides` 어노테이션으로 처리하여 타입 인스턴스를 제공하는 방법을 `Hilt`에 알릴 수 있다.
* `@Provides` 어노테이션이 있는 함수는 다음의 정보를 `Hilt`에 알려준다
* 함수의 리턴타입은 함수가 인스턴스를 제공하는 타입을 `Hilt`에 알려준다.
* 함수의 파라미터는 타입의 dependencies를 `Hilt`에 알려준다.
* 함수의 바디는 타입의 인스턴스를 제공하는 방법을 `Hilt`에 알려준다. 타입의 인스턴스를 제공할 필요가 있을 때마다 `Hilt`는 함수의 바디를 실행한다.
```kotlin
@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)
}
}
✔️ 동일한 타입에 대해 여러 바인딩 제공
- 같은 타입의 종속성에 여러 구현을 제공하기 위해
Hilt
가 필요한 경우,Hilt
에 어려 종류의 바인딩을 제공해야 한다. - qualifier를 통해 여러 종류의 바인딩을 정의할 수 있다.
- qualifier는 해당 타입에 여러 종류의 바인딩이 정의되어있는 경우, 해당 타입의 특정 바인딩을 식별하는데 사용한다.
- qualifier를 타입에 추가하는 경우, 해당 종속성을 제공할 수 있는 모든 방법에 qualifier를 추가하는 것이 좋다.
- qualifier 없이 기본 또는 공통 구현을 그대로 둘 경우, 오류가 발생하기 쉬우며
Hilt
가 잘못된 종속성을 주입할 수 있다. - 예시로,
OkHttpClient
의 두 가지 다른 구현을 Hilt에 제공하기 위해서는@Qualifier
어노테이션을 이용하여@Binds
또는@Provides
메서드에 사용할 qualifier를 정의해야 한다. ```kotlin @Qualifier @Retention(AnnotationRetention.BINARY) annotation class AuthInterceptorOkHttpClient
@Qualifier @Retention(AnnotationRetention.BINARY) annotation class OtherInterceptorOkHttpClient
<br>
* 두 모듈은 같은 리턴타입을 지니고 있지만 qualifier가 두 모듈을 다른 바인딩으로 지정하고 있다.
```kotlin
@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()
}
}
-
필드 또는 매개변수에 해당 qualifier로 어노테이션을 추가하여 필요한 특정 타입을 주입할 수 있다. ```kotlin // 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 } ``` <br>
✔️ 사전 정의된 Hilt
qualifier
- Hilt는 사용자 지정 바인딩에 종속성으로 주입할 수 있는 몇 가지의 미리 정의된 qualifier를 제공한다.
- 에를 들어, 애플리케이션 또는 액티비티에서
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 component | Inject for |
---|---|
SingletonComponent |
Application |
ActivityRetainedComponent |
N/A |
ViewModelComponent |
ViewModel |
Activityomponent |
Activity |
FragmentComponent |
Fragment |
ViewComponent |
View |
ViewWithFragmentComponent |
View annotated with @WithFragmentBindings |
ServiceComponent |
Service |
Hilt
는SingletonComponent
로부터 바로BroadcastReceiver
를 주입하기 때문에BroadcastReceiver
에 대해서는 컴포넌트를 생성하지 않는다.
✔️ 컴포넌트 생명주기
Hilt
는 생성된 컴포넌트 클래스의 인스턴스를 해당 Android 클래스의 생명주기에 따라 자동으로 생성하고 파괴한다.
|Generated component|Created at|Destroyed at|
|——————:|:——–:|:———-:|
|SingletonComponent
|Application#onCreate()
|Application#onDestroy()
|
|ActivityRetainedComponent
|Activity#onCreate()
|Activity#onDestroy()
|
|ViewModelComponent
|ViewModel
created|ViewModel
destroyed|
|Activityomponent
|Activity#onCreate()
|Activity#onDestroy()
|
|FragmentComponent
|Fragment#onAttach()
|Fragment#onDestroy()
|
|ViewComponent
|View#super()
|View
destroyed|
|ViewWithFragmentComponent
|View#super()
|View
destroyed|
|ServiceComponent
|Service#onCreate()
|Service#onDestroy()
|
ActivityRetainedComponent
는Activity#onCreate()
의 시작에 생성되고Activity#onDestroy()
의 끝에 파괴되므로, 구성 변경 전체에 걸쳐 살아있다.
✔️ 컴포넌트 Scope
- 기본적으로
Hilt
의 모든 바인딩은 Scope가 지정되지 않으며, 앱에서 바인딩을 요청할 때마다Hilt
는 필요한 유형의 새 인스턴스를 만든다. - 하지만
Hilt
의 바인딩을 특정 컴포넌트 Scope로 지정할 경우, 바인딩 범위가 지정된 컴포넌트의 인스턴스당 한 번만 Scope 바인딩을 생성하고 해당 바인딩에 대한 모든 요청은 동일한 인스턴스를 공유한다.
|Android class|Generated component|Scope|
|————:|:—————–:|:—-|
|Application
|SingletonComponent
|@Singleton
|
|Activity
|ActivityRetainedComponent
|@ActivityRetainedScoped
|
|ViewModel
|ViewModelComponent
|@ViewModelScoped
|
|Activity
|ActivityComponent
|@ActivityScoped
|
|Fragment
|FragmentComponent
|@FragmentScoped
|
|View
|ViewComponent
|@ViewScoped
|
|View
annotated with @WithFragmentBindings
|ViewWithFragmentComponent
|@ViewScoped
|
|Service
|ServiceComponent
|@ServiceScoped
|
- 컴포넌트의 바인딩 범위를 지정하면 제공된 객체의 컴포넌트가 제거될 때까지 메모리에 남아있기 때문에 비용이 많이 들 수 있다.
- 때문에 범위가 지정된 바인딩의 사용을 최소화하는 것이 좋다.
-
다만, 특정 범위 내에서 동일한 인스턴스를 사용해야 하는 내부 바인딩, 동기화가 필요한 바인딩, 생성 비용이 많이 드는 바인딩에 대해서는 컴포넌트 Scope 바인딩을 사용하는 것이 적절하다. ```kotlin // If AnalyticsService is an interface. @Module @InstallIn(SingletonComponent::class) abstract class AnalyticsModule {
@Singleton @Binds abstract fun bindAnalyticsService( analyticsServiceImpl: AnalyticsServiceImpl ): AnalyticsService }
// If you don’t own AnalyticsService. @Module @InstallIn(SingletonComponent::class) object AnalyticsModule {
@Singleton
@Provides
fun provideAnalyticsService(): AnalyticsService {
return Retrofit.Builder()
.baseUrl("https://example.com")
.build()
.create(AnalyticsService::class.java)
} } ``` <br>
✔️ 컴포넌트 Hierarchy
- 컴포넌트에 모듈을 installing하면 컴포넌트 hierarchy에 따라 해당 컴포넌트 또는 하위의 다른 자식 컴포넌트의 바인딩이 종속성으로서 해당 컴포넌트의 바인딩에 접근할 수 있다.
- Hilt가 생성하는 컴포넌트의 Hierarchy :arrow_down:
✔️ 컴포넌트 기본 바인딩
- 각각의
Hilt
컴포넌트에는Hilt
가 사용자 커스텀 바인딩에 종속성으로 주입할 수 있는 기본 바인딩이 함께 제공된다. Hilt
는 single activity 컴포넌트 정의를 바탕으로 모든 activity를 주입하기 때문에 이러한 바인딩은 특정 하위 클래스가 아닌 일반Activity
및Fragment
유형이다.- 각
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
|
- application context의 바인딩은
@ApplicationContext
어노테이션을 통해서도 가능하다. ```kotlin class AnalyticsServiceImpl @Inject constructor( @ApplicationContext context: Context ) : AnalyticsService { … }
// The Application binding is available without qualifiers. class AnalyticsServiceImpl @Inject constructor( application: Application ) : AnalyticsService { … }
<br>
* activity context의 바인딩은 `@ActivityBinding` 어노테이션을 통해서도 가능하다.
```kotlin
class AnalyticsAdapter @Inject constructor(
@ActivityContext context: Context
) { ... }
// The Activity binding is available without qualifiers.
class AnalyticsAdapter @Inject constructor(
activity: FragmentActivity
) { ... }
✨ Hilt에서 지원하지 않는 클래스에 종속성 주입
Hilt
는 일반적인 Android 클래스에 대한 지원을 제공하지만Hilt
가 지원하지 않는 클래스에서 필드 주입을 수행해야 하는 경우가 있을 수 있다.- 이러한 경우,
@EntryPoint
어노테이션을 사용하여 entry point를 만들 수 있다. - 이러한 entry point는
Hilt
에서 관리하는 코드와 그렇지 않은 코드 사이의 경계이며, Hilt가 관리하는 객체의 그래프에 코드가 처음 입력되는 지점이다. - entry point
Hilt
가 종속성 그래프 내의 종속성을 제공할 수 있도록Hilt
에서 관리하지 않는 코드를 사용할 수 있게 한다. - 예를 들어,
Hilt
가 지원하지 않는Content Provider
가Hilt
를 사용해서 일부 종속성을 가져오도록 하려면 각 바인딩 타입에@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 클래스와 일치하는지 확인해야 한다.
📝 References
- https://developer.android.com/training/dependency-injection