Android DI 라이브러리 Dagger-Hilt 소개와 사용법

2024. 1. 5. 17:01Android

 


Hilt : Android DI Library

Hilt는 Android에서 DI를 사용하기 위한 Jetpack 권장 라이브러리이다.

Dagger라는 DI 라이브러리를 기반으로 빌드되어 Dagger를 Android 앱에 통합하는 Standard를 제공한다. 그래서 Dagger와 Hilt는 동일한 코드베이스에 공존할 수 있다. Dagger사용을 Hilt를 통해 관리하는 형식이다.

장점

  • Android 내 모든 클래스의 컨테이너를 제공하고 자동으로 DI를 구성해주며 수명 주기를 자동으로 관리함으로써 DI를 사용하는 표준 방법을 제공한다.
  • Hilt는 Android Studio에서 지원하는 Dagger의 컴파일 시간 정확성, 런타임 성능, 확장성을 기반으로 빌드되었다.
  • DI를 실행하기 위한 boilerplate 코드를 줄여준다.
    • Android 프레임워크 클래스에 통합된 Component
    • Hilt가 자동으로 생성하는 컴포넌트와 함께 사용하는 Scope annotation
    • Application이나 Activity와 같은 Android 클래스에서 기본적으로 DI할 수 있는 Predefined bindings
    • @ApplicationContext, @ActivityContext와 같은 Predefined qualifiers
  • Hilt에서는 다음을 자동으로 생성해서 제공해준다.

사용법)

1. Hilt 라이브러리 추가

hilt-android-gradle-plugin 플러그인을 프로젝트의 루트 build.gradle 파일에 추가한다.(.kt 파일)

plugins {
  ...
  id("com.google.dagger.hilt.android") version "2.44" apply false
}

 

 

Gradle 플러그인을 적용한 후,app/build.gradle 파일에 다음 항목을 추가한다. (.kt 파일)

plugins {
  kotlin("kapt")
  id("com.google.dagger.hilt.android")
}

android {
  ...
}

dependencies {
  implementation("com.google.dagger:hilt-android:2.44")
  kapt("com.google.dagger:hilt-android-compiler:2.44")
}

// Allow references to generated code
kapt {
  correctErrorTypes = true
}

 

 

Hilt는 자바 8 기능을 사용하기 때문에 프로젝트에서 자바 8 설정이 안되어 있다면 app/build.gradle 파일에 다음을 추가한다.(아마 추가되있을 것이다) (.kt 파일)

android {
  ...
  compileOptions {
    sourceCompatibility = JavaVersion.VERSION_1_8
    targetCompatibility = JavaVersion.VERSION_1_8
  }
}

 

 

2. @HiltAndroidApp Hilt Application Class 생성

Hilt를 사용하기 위해 가장 먼저, 앱 최상단에 어플리케이션 클래스 파일을 생성한다.

@HiltAndroidApp
class ExampleApplication : Application() { ... }

 

 

3. @AndroidEntryPoint Inject dependencies into Class

(Android 클래스에 종속성 삽입)

@AndroidEntryPoint
class ExampleActivity : AppCompatActivity() { ... }

 

 

4. @Inject Component에서 dependency 가져오기

@AndroidEntryPoint
class ExampleActivity : AppCompatActivity() {

  @Inject lateinit var analytics: AnalyticsAdapter
  ...
}

 

 

 

Hilt Binding

Hilt에서 Field Injection을 하려면 Hilt가 해당 컴포넌트에서 필요한 dependency의 인스턴스를 제공하는 방법을 알아야 한다. 바인딩은 dependency 인스턴스를 제공하기 위해 필요한 정보도 포함한다.

@Inject Constructor Injection

생성자 삽입: 다음과 같이 클래스 생성자에서 @Inject주석을 사용하여 클래스의 인스턴스를 제공하는 방법을 Hilt에 알려준다.

class AnalyticsAdapter @Inject constructor(
  private val service: AnalyticsService
) { ... }

 

💡 Hilt가 지원하는 Andoid 클래스는 다음과 같다.

Android 클래스에 @AndroidEntryPoint어노테이션을 붙이면 이 클래스에 종속된 Android 클래스에도 붙여야 한다. 예를 들어 Fragment에 @AndroidEntryPoint를 붙이면 이 Fragment를 사용하는 Activity에도 붙여야 한다. (Fragment는 단독으로 정의할 수 없고 반드시 Activity의 안에서 정의하기 때문)

 

 

 

그럼 Hilt에서 지원하지 않는 클래스에 DI하고 싶을 땐 어떻게 해야할까? Hilt가 지원하지 않는 클래스에 field-injection을 실행할 경우는 다음과 같이 하면 된다.

@EntryPoint

@EntryPoint 어노테이션을 사용하면 Hilt가 관리하는 코드로의 진입점을 만들 수 있다. Hilt가 관리하는 객체의 graph에 코드가 들어가는 지점이다.

예를 들어, Hilt는 content providers를 지원하지 않는다. content providers가 Hilt를 사용해 dependency를 가져오려면 원하는 곳에 @EntryPoint로 인터페이스를 정의하고 qualifiers를 포함해야 한다. 그리고 다음과 같이 @InstallIn을 추가하여 entry point을 설정할 컴포넌트를 정해준다.

class ExampleContentProvider : ContentProvider() {

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

  ...
}

 

 

EntryPointAccessors의 static method를 사용하여 entry point에 접근할 수 있다.

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()
    ...
  }
}

 

 

@Module @InstallIn Hilt Module

constructor-injection이 안되는 경우도 있다.

  • Interface인 경우
  • external library 클래스인 경우

이럴 때는 Hilt 모듈을 사용하여 Hilt에게 바인딩 정보를 줄 수 있다. 아래에서 이 두가지 경우에 사용할 수 있는 예시를 각각 설명하겠다.

Hilt-Module은 @Module 어노테이션이 있는 클래스이다. Hilt-Module은 Dagger module처럼 특정 타입의 인스턴스를 제공하는 법을 Hilt에 알려준다. 그러나 Hilt-Module은 Dagger module과 달리 @InstallIn 어노테이션을 통해 사용할 모듈의 Android 클래스를 Hilt에 알려줘야 한다.

@Binds Inject Interface Instance

@Binds 어노테이션은 인터페이스의 인스턴스를 제공할 때 사용되는 것으로, Hilt에게 다음 두 가지를 알려준다.

  • return type: 해당 함수가 어떤 인터페이스의 인스턴스를 사용하는지
  • parameter: 제공할 implementation이 어떤 것인지

AnalyticsService

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
}

 

AnalyticsModule의 dependency를 ExampleActivity에 inject하기 위해서 AnalyticsModule에 @InstallIn(ActivityComponent.class) 어노테이션을 추가한다. 이는 AnalyticsModule의 모든 dependency 항목을 앱의 모든 Activity에서 사용할 수 있도록 한다.

 

@Provides Inject Instance of External Library

Retrofit, OkHttpClient,  Room databases와 같은 external library를 사용할 경우 클래스가 프로젝트 내에 있지 않고 외부 라이브러리에서 제공되므로 역시 constructor-injection이 불가능하다.

이 경우에는 @Provides 어노테이션을 지정하여 Hilt에게 다음 정보를 알린다.

  • return type: 해당 함수가 어떤 유형의 인스턴스를 제공하는지
  • parameter: 해당 타입의 dependency
  • function body(함수 본문): 어떻게 해당 타입의 인스턴스를 줄 것인지(Hilt는 해당 타입의 인스턴스를 제공할 때마다 이 function의 body를 실행함)
@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)
  }
}

 

 

Qualifier

같은 타입에 대한 Multiple Binding

dependency와 같은 타입의 다양한 implementation를 제공하는 Hilt를 사용하고자 하는 경우 multiple-binding(멀티 바인딩)을 해야한다.

@Qualifier 어노테이션을 통해 멀티 바인딩이 정의되어 있는 경우, 특정 타입의 바인딩을 식별할 수 있다.

AnalyticsService의 호출을 intercept하는 경우를 가정해 보자. 당신은 OkHttpClient 오브젝트와 함께 interceptor를 사용할 것이다. 다른 service에서는 다른 방식으로 호출을 intercept할 수 있다. 이 경우, 서로 다른 두가지 OkHttpClient의 implementation을 Hilt에 알려주어야 한다.

먼저, @Qualifier  @Binds 나 @Provides 를 사용할 함수에 지정한다.

@Qualifier
@Retention(AnnotationRetention.BINARY)
annotation class AuthInterceptorOkHttpClient

@Qualifier
@Retention(AnnotationRetention.BINARY)
annotation class OtherInterceptorOkHttpClient

 

 

그 다음, Hilt에게 각 qualifier와 맞는 인스턴스를 제공하는 법을 알려준다. 이 경우, @Provides와 함께 @Module(Hilt-Module)을 사용한다.

@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()
  }
}

두 함수 모두 동일한 return type을 갖지만 qualifier label(@AuthInterceptorOkHttpClient, @OtherInterceptorOkHttpClient)를 통해 다음과 서로 다른 두 가지의 바인딩을 지정한다.

 

다음과 같이 field나 parameter에 qualifier 어노테이션을 지정하여 필요한 특정 타입만 inject할 수 있다.

// 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를 추가해야 한다. qualifier없이 두면 에러가 나기 쉽고 Hilt가 DI를 잘못할 수 있다.

Predefined qualifiers in Hilt

Hilt에서는 미리 정의된 qualifier를 제공한다. 예를 들어 Application이나 Activity에서 Context 클래스가 필요할 경우, Hilt에서 제공하는 @ApplicationContext 및 @ActivityContext qualifier를 사용할 수 있다.

class AnalyticsAdapter @Inject constructor(
    @ActivityContext private val context: Context,
    private val service: AnalyticsService
) { ... }

 

 

Generated components for Android classes

Android 클래스마다 @InstallIn 주석에 참조할 수 있는 관련 Hilt 컴포넌트

 

Hilt component Injector for
SingletonComponent Application
ActivityRetainedComponent N/A
ViewModelComponent ViewModel
   
ActivityComponent Activity
FragmentComponent Fragment
ViewComponent View
ViewWithFragmentComponent View annotated with @WithFragmentBindings
ServiceComponent Service

참고) Hilt는 SingletonComponent에서 직접 broadcast receiver를 삽입하므로 broadcast receiver의 컴포넌트는 생성하지 않는다.

 

Component lifetimes

Hilt는 Android 클래스의 lifecycle에 따라 생성된 컴포넌트의 인스턴스를 자동으로 만들고 삭제한다.

Generated component Created at Destroyed at
     
SingletonComponent Application#onCreate() Application destroyed
ActivityRetainedComponent Activity#onCreate() Activity#onDestroy()
ViewModelComponent ViewModel created ViewModel destroyed
ActivityComponent Activity#onCreate() Activity#onDestroy()
FragmentComponent Fragment#onAttach() Fragment#onDestroy()
ViewComponent View#super() View destroyed
ViewWithFragmentComponent View#super() View destroyed
ServiceComponent Service#onCreate() Service#onDestroy()

 

Component scopes

기본적으로 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

 

사용 예시)

@ActivityScoped
class AnalyticsAdapter @Inject constructor(
  private val service: AnalyticsService
) { ... }

@ActivityScoped 사용하여 AnalyticsAdapter의 범위를 ActivityComponent로 지정하면 Hilt는 해당 Activity의 lifecycle에따라 AnalyticsAdapter 인스턴스를 제공한다.

 

참고로, Hilt를 통해 제공된 object는 컴포넌트가 삭제될 때까지 메모리에 남아 있기 때문에 특정 컴포넌트에 바인딩하여 scope를 지정하면 많은 비용이 들 수 있다. internal state가 있는 바인딩, synchronization(동기화)가 필요한 바인딩이 아니라면 사용을 최소화해야한다.

예를 들어 AnalyticsService가 ExampleActivity뿐만 아니라 앱의 모든 위치에서 사용하는 state를 가지고 있다고고 했을 때, AnalyticsService의 범위는 SingletonComponent가 적절하다.

// 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)
  }
}

 

 

 

참고

https://developer.android.com/training/dependency-injection/hilt-android?hl=ko#kts

 

Hilt를 사용한 종속 항목 삽입  |  Android 개발자  |  Android Developers

Hilt를 사용한 종속 항목 삽입 컬렉션을 사용해 정리하기 내 환경설정을 기준으로 콘텐츠를 저장하고 분류하세요. Hilt는 프로젝트에서 종속 항목 수동 삽입을 실행하는 상용구를 줄이는 Android용

developer.android.com