Hilt
- Hilt는 의존성 주입을 위한 라이브러리입니다.
의존성 주입
- 객체 간의 의존성을 외부에서 주입하여 코드의 결합도를 낮추고 테스트 가능성을 높이는 방법입니다.
- 객체 생성과 의존성 관리를 프레임워크나 DI 컨테이너에서 처리하도록 위임합니다.
Dagger
- Hilt는 Dagger를 기반으로 빌드되었습니다.
- Dagger가 제공하는 컴파일 시간 정확성, 런타임 성능, 확장성을 사용할 수 있으며, 안드로이드 의존성이 있다는 특징이 있습니다.
Hilt 애플리케이션
- Hilt를 사용하는 모든 앱은 @HiltAndroidApp으로 주석이 지정된 Application 클래스를 포함해야 합니다.
- 애플리케이션 수준 종속 항목 컨테이너 역할을 하는 애플리케이션의 기본 클래스를 포함하여 Hilt의 코드 생성을 트리거합니다.
@HiltAndroidApp
class ExampleApplication: Application()
Android 클래스에 종속 항목 삽입
- Application 클래스에 Hilt를 설정하면, Hilt는 @AndroidEntryPoint 주석이 있는 다른 Android 클래스에 종속 항목을 제공할 수 있습니다.
- @Inject 주석을 사용하여 필드 삽입을 진행할 수 있습니다.
// AppCompatActivity와 같은 ComponentActivity를 확장하는 활동 지원
@AndroidEntryPoint
class ExampleActivity : AppCompatActivity(){
@Inject lateinit var repository: SampleRepository
}
Android 클래스
- Hilt는 다음과 같은 Android 클래스를 지원합니다.
- Application (with HiltAndroidApp)
- ViewModel (with HiltViewModel)
- Activity
- Fragment
- View
- Service
- BroadcastReceiver
안드로이드 클래스의 예외 사항
- Hilt는 AppComatActivity와 같은 ComponentActivity를 확장하는 컴포넌트만 지원합니다.
- androidx.Fragment를 확장하는 프래그먼트만 지원하며, 보존된 프래그먼트(Activity 재생성 시에도 인스턴스가 유지되는 Fragment)를 지원하지 않습니다.
Hilt bindings 정의
- 필드를 삽입하기 위해 Hilt가 해당 컴포넌트에 필요한 종속 항목의 인스턴스를 제공하는 방법을 알아야 합니다.
- constructor injection를 통해 Binding을 정의할 수 있습니다.
- 클래스의 생성자에서 @Inject 주석을 사용하여 클래스의 인스턴스를 제공하는 방법을 Hilt에 알려줍니다.
class SampleRepository @Inject constructor(
private val service: SampleService,
)
- 주석이 지정된 클래스 생성자의 매개변수는 그 클래스의 종속 항목이 됩니다.
- 이를 제공하기 위해서는 @Inject 주석을 통해 Hilt에 SampleService의 인스턴스를 제공하는 방법을 알려야 합니다.
빌드 시간에 Hilt는 Android 클래스용 Dagger 구성요소를 생성하여 Dagger 코드를 검증합니다. 그 후 종속 항목 그래프를 빌드하여 유효성을 검사합니다. 런타임 시 실제 객체 및 종속 항목을 만드는 데 사용되는 클래스 인스턴스를 생성합니다.
Hilt 모듈
- 다음과 같은 경우에 생성자 삽입이 어려울 수 있습니다.
- 인터페이스를 삽입하는 경우
- 외부 라이브러리의 클래스를 삽입하는 경우
- 이럴 때 Hilt 모듈을 활용하여 Hilt에 binding 정보를 제공할 수 있습니다.
- Hilt 모듈은 @Module 주석으로 지정이 가능하며, 특정 유형의 인스턴스를 제공하는 방법을 Hilt에 알려줍니다.
InstallIn
- @InstallIn 주석을 지정하여 각 모듈을 사용하거나 설치할 Android 클래스를 Hilt에 알려야 합니다.
Binds
- @Binds 주석은 인터페이스 인스턴스 삽입에 사용할 수 있습니다.
interface SampleService {
fun service()
}
class SampleServiceImpl(
) : SampleService{
override fun service() {
TODO("Not yet implemented")
}
}
- 추상 함수를 생성하여 Hilt에 결합 정보를 제공합니다.
- 함수 반환 유형은 함수가 어떤 인터페이스의 인스턴스를 제공하는지 알려줍니다.
- 함수 매개변수는 제공할 구현을 Hilt에 알려줍니다.
@Module
@InstallIn(ActivityComponent::class)
abstract class SampleModule {
@Binds
abstract fun bindSampleService(
sampleServiceImpl: SampleServiceImpl
): SampleService
}
- @InstallIn(ActivityComponent::class) 주석을 통해 모듈의 모든 종속 항목을 앱에서 사용할 수 있습니다.
Provides
- 클래스가 외부 라이브러리에서 제공되어 클래스를 소유하지 않은 경우 활용할 수 있습니다.
- 빌더 패턴으로 인스턴스를 생성해야 하는 경우가 대표적입니다.
- Room 데이터 베이스
- Retrofit, OkHttpClient
- Provides 쿼리를 활용하는 경우 Hilt에 다음을 제공해야 합니다.
- 함수 반환 유형 : 함수가 어떤 유형의 인스턴스를 제공하는지
- 함수 매개변수 : 해당 유형의 종속 항목을 Hilt에 알려주어야 함
- 함수 본문 : 해당 유형의 인스턴스를 제공하는 방법을 Hilt에 알려주어야 함
- Hilt는 함수 본문을 실행하여 해당 유형의 인스턴스를 제공
@Module
@InstallIn(ActivityComponent::class)
object SampleProvideModule {
@Provides
fun provideService(): SampleService{
return Retrofit.Builder()
.baseUrl("https//sample.com")
.build()
.create(SampleService::class.java)
}
}
- 추상 클래스가 아닌 object 제공
- 함수 반환 유형, 매개변수, 본문 구성
동일한 유형에 대한 bindings 제공
- 종속 항목과 동일한 유형의 다양한 구현이 필요한 경우가 있습니다.
- 이런 경우 여러 경합을 제공해야 하는데, 한정자(@Qualifier)를 활용해야 합니다.
Qualifier
- OkHttpClient 객체를 2가지 유형으로 제공할 수 있습니다.
- 호출을 가로채서 인터셉터를 구현하는 경우
- 클라이언트만 제공하는 경우
@Qualifier
@Retention(AnnotationRetention.BINARY)
annotation class AuthInterceptorOkHttpClient
@Qualifier
@Retention(AnnotationRetention.BINARY)
annotation class OtherInterceptorOkHttpClient
- Binds 혹은 Provides 메서드에 주석을 지정하는 데 사용할 한정자를 제공할 수 있습니다.
Module
- 각 메서드는 동일한 반환 유형을 가지지만, Qualifier에 따라 다른 결합을 지원할 수 있습니다.
class OtherInterceptor() : Interceptor {
override fun intercept(chain: Interceptor.Chain): Response {
TODO("Not yet implemented")
}
}
class AuthInterceptor() : Interceptor {
override fun intercept(chain: Interceptor.Chain): Response {
TODO("Not yet implemented")
}
}
@Module
@InstallIn(ActivityComponent::class)
object SampleProvideModule {
@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()
}
}
- 필드 또는 매개변수에 해당 한정자로 주석을 지정하여 필요한 유형을 삽입할 수 있습니다.
@Provides
fun provideSampleService(
@AuthInterceptorOkHttpClient okHttpClient: OkHttpClient
): SampleService{
return Retrofit.Builder()
.baseUrl("<https://example.com>")
.client(okHttpClient)
.build()
.create(SampleService::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
}
사전 정의 한정자
- Hilt에서는 사전 정의된 한정자를 제공합니다.
- 애플리케이션이나 액티비티의 Context가 필요할 수 있으므로 @ApplicationContext 혹은 @ActivityCountext를 제공할 수 있습니다.
class SampleAdapter @Inject constructor(
@ActivityContext private val context: Context
) {
}
- 이 외에 다양한 사전 정의된 결합을 사용할 수 있습니다.
Hilt를 사용한 종속 항목 삽입 | Android Developers
Android 클래스 컴포넌트
- 의존성 주입을 실행할 수 있는 각 Android 클래스마다 @InstallIn 주석에 참조할 수 있는 관련 컴포넌트를 확인할 수 있습니다.
- Hilt 아래와 같은 컴포넌트를 제공합니다.
Hilt 구성요소 주입 대상
SingletonComponent | Application |
ActivityRetainedComponent | N/A |
ViewModelComponent | ViewModel |
ActivityComponent | Activity |
FragmentComponent | Fragment |
ViewComponent | View |
ViewWithFragmentComponent | @WithFragmentBindings 주석이 지정된 View |
ServiceComponent | Service |
- Hilt는 SingletonComponent에서 직접 broadcast receiver를 삽입하므로, broadcast receiver에 대한 컴포넌트를 생성하지 않습니다.
컴포넌트 라이크 사이클
- Android 클래스의 생명 주기에 따라 생성된 컴포넌트 클래스의 인스턴스를 자동으로 생성 후 제거합니다.
생성된 컴포넌트 생성 위치 소멸 위치
SingletonComponent | Application#onCreate() | Application 소멸됨 |
ActivityRetainedComponent | Activity#onCreate() | Activity#onDestroy() |
ViewModelComponent | ViewModel 생성됨 | ViewModel 소멸됨 |
ActivityComponent | Activity#onCreate() | Activity#onDestroy() |
FragmentComponent | Fragment#onAttach() | Fragment#onDestroy() |
ViewComponent | View#super() | View 소멸됨 |
ViewWithFragmentComponent | View#super() | View 소멸됨 |
ServiceComponent | Service#onCreate() | Service#onDestroy() |
범위 스코프
- 일반적으로 Hilt의 모든 bindings는 범위가 지정되지 않습니다.
- 앱이 결합을 요청할 때마다 필요한 유형의 새 인스턴스를 생성합니다.
- 특정 스코프를 지정하여, 지정된 구성요소의 인스턴스마다 결합을 생성할 수 있습니다.
- 이 결합에 관한 모든 요청은 동일한 인스턴스를 공유하게 됩니다.
Android 클래스 생성된 구성요소 범위
Application | SingletonComponent | @Singleton |
Activity | ActivityRetainedComponent | @ActivityRetainedScoped |
ViewModel | ViewModelComponent | @ViewModelScoped |
Activity | ActivityComponent | @ActivityScoped |
Fragment | FragmentComponent | @FragmentScoped |
View | ViewComponent | @ViewScoped |
@WithFragmentBindings 주석이 지정된 View | ViewWithFragmentComponent | @ViewScoped |
Service | ServiceComponent | @ServiceScoped |
- @ActivityScoped를 사용하면 해당 생명 주기 동안 인스턴스를 제공할 수 있습니다.
@ActivityScoped
class SampleAdapter @Inject constructor(
@ActivityContext private val context: Context
) {
}
스코프를 지정할 경우, 해당 컴포넌트의 생명주기가 유지되는 동안 메모리에 남기 때문에 잘못된 스코프 범위를 지정하면 메모리 낭비가 될 수 있습니다. 애플리케이션에서 범위가 지정된 결합의 사용을 최소화해야 하며, 가장 적은 비용의 결합을 지원해야 합니다.
컴포넌트 계층 구조
- 컴포넌트에 모듈을 설치하면, 다른 결합 또는 계층 구조에서 하위 구성 요소의 다른 결합의 종속 항목으로 설치된 모듈의 결합에 접근할 수 있습니다.
지원하지 않는 클래스에 주입
- Hilt는 일반적으로 Android 클래스에 대한 지원을 제공합니다.
- 하지만 Hilt가 지원하지 않는 서드 파티 라이브러리나, 컨텐트 프로바이더에 주입이 필요한 경우가 있습니다.
- 이 경우 @EntryPoint를 활용할 수 있습니다.
- 진입점을 통해 Hilt는 Hilt가 관리하지 않는 코드를 사용하여 종속 항목 그래프 내에서 종속 항목을 제공할 수 있습니다.
class ExampleContentProvider: ContentProvider() {
@EntryPoint
@InstallIn(SingletonComponent::class)
interface ExampleContentProviderEntryPoint {
fun service() : SampleService
}
}
Hilt와 Dagger
- Hilt는 Dagger 종속 항목 삽입 라이브러리를 기반으로 빌드되어 Dagger를 안드로이드 애플리케이션에 통합하는 표준 방법을 제공합니다.
- Android 앱을 위한 Dagger 관련 인프라 간소화
- 앱 간의 설정, 가독성 및 코드 공유를 용이하게 하기 위한 표준 구성요소 및 범위 세트 생성
- 테스트, 디버그 또는 출시와 같은 다양한 빌드 유형에 서로 다른 결합을 프로비저닝하는 쉬운 방법 제공
- 프로비저닝 : 앱을 빌드하고 배포하기 위해 필요한 설정, 리소스, 인증 정보 준비
Dagger 및 Hilt 코드는 동일한 코드베이스에 공존할 수 있으며, Hilt를 활용하면 Android에서 Dagger의 모든 사용을 관리할 수 있습니다.