개발일지/Android

Android Hilt - DI 사용

Hilt

Dagger DI 라이브러리 기반으로 빌드되어 Dagger Android 애플리케이션에 통합하는 표준 방법을 제공.

  • Android 앱을 위한 Dagger 관련 인프라 간소화
  • 앱간의 설정, 가독성 및 코드 공유를 용이하게 하기 위한 표준 구성요소 및 범위 세트 생성
  • 테스트, 디버그 또는 출시와 같은 다양한 빌드 유형에 서로 다른 결합을 프로비저닝하는 쉬운 방법 제공

DI를 구현하면 코드 재사용 가능, 리팩터링 편의성, 테스트 편의성의 이점이 있다.

1. 루트의 build.gradle에 hilt-android-gradle-plugin을 dependency 추가

buildscript {
    ...
    dependencies {
        ...
        class 'com.google dagger:hilt-android-gradle-plugin:2.28-alpha'
    }
}

2. app/build.gradle에 다음 dependency 추가.

...
apply plugin: 'kotlin-kapt'
apply plugin: 'dagger.hilt.android.plugin'

android {
    ...
}

dependencies {
    implementation "com.google.dagger:hilt-android:2.28-alpha"
    kapt "com.google.dagger:hilt-android-compiler:2.28-alpha"
}

3. Hilt는 자바8 기능을 사용함. app/build.gradle에 다음을 추가.

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

4. Hilt를 사용하는 모든 앱은 @HiltAndroidApp으로 애노테이션이 지정된 Application 클래스를 포함해야 합니다.

@HiltAndroidApp은 애플리케이션 수준 종속 항목 컨테이너 역할을 하는 애플리케이션의 기본 클래스를 비롯하여 Hilt의 코드 생성을 트리거 합니다.

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

5. Android Class에 디펜던시 항목 삽입

Application 클래스에 Hilt를 설정하고 애플리케이션 수준 구성요소를 사용할 수 있게 되면 Hilt는 @AndroidEntryPoint 애노테이션이 있는 다른 Android 클래스에 종속 항목을 제공할 수 있습니다.

@AndroidEntryPoint
class ExampleActivity : AppCompatActivity() { ... }
  • Hilt는 현재 다음 Android Class를 지원합니다.
    1. Application (@HiltAndroidApp)
    2. Activity
    • Fragment
    • View
    • Service
    • BroadcastReceiver
  • Android Class에 @AndroidEntryPoint로 애노테이션을 지정하면 이 클래스에 종속된 Android Class에도 애노테이션을 지정해야 합니다.예를 들면 Fragment에 애노테이션을 지정하면 이 Fragment를 사용하는 활동에도 애노테이션을 지정해야 합니다.

6. 구성요소에서 종속항목을 가져오려면 @Inject 애노테이션을 사용하여 필드 삽입을 실행합니다.

@AndroidEntryPoint
class ExampleActivity : AppCompatActivity() {
    @Inject lateinit var analytics: AnalyticsAdapter
    ...
}

생성자 삽입방법. 생성자에서 @inject 애노테이션을 사용하여 클래스의 인스턴스를 제공하는 방법을 Hilt에 알려줄 수 있습니다.

class AnalyticsAdapter @inject constructor(
    private val service: AnalyticsService
)
  • Hilt 모듈

    • @Module로 애노테이션된 클래스.

    • 이 모듈은 특정 유형의 인스턴스를 제공하는 방법을 Hilt에 알려줍니다. 그러나 Dagger모듈과는 달리 Hilt모듈에 @InstallIn 애노테이션을 지정하여 각 모듈을 사용하거나 설치할 Android Class를 Hilt에 알려야 합니다.

      interface AnalyticsService {
          fun analyticsMethods()
      }
      
      class AnalyticsServiceImpl @Inject constructor(
          ...
      ) : AnalyticsService { ... }
      
      @Module
      @InstallIn(ActivityComponent::class) // AnalyticsModule의 모든 종속 항목을 앱의 모든 활동에서 사용할 수 있음을 의미.
      abstract class AnalyticsModule {
      
          @Binds
          abstract fun bindAnalyticsService(
              analyticsServiceImpl: AnayticsServiceImpl
          ): analyticsService
      }

7. @Provides를 사용하여 인스턴스 삽입

클래스가 외부 라이브러리에서 제공되므로 클래스를 소유하지 않은 경우(Retrofit, OkHttpClient) 또는 Room 데이터베이스) 또는 빌더 패턴으로 인스턴스를 생성해야 하는 경우에도 생성자 삽입이 불가능합니다.

  • @Provides 달린 함수는 Hilt에 다음 정보를 제공합니다.

    • 함수 반환 유형은 함수가 어떤 유형의 인스턴스를 제공하는지 Hilt에 알려줍니다.
    • 함수 매개변수는 해당 유형의 종속 항목을 Hilt에 알려줍니다.
    • 함수 본문은 해당 유형의 인스턴스를 제공하는 방법을 Hilt에 알려줍니다. Hilt는 해당 유형의 인스턴스를 제공해야 할 때마다 함수 본문을 실행합니다.
  • @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)
        }
    }

8. 동일한 유형에 대해 여러 결합제공.

서로 다른 두가지 OkHttpClient 구현을 제공하는 방법을 Hilt에 알려야 한다.

먼저 다음과 같이 @Binds 또는 @Provides 메소드에 애노테이션을 지정하는데 사용할 한정자를 정의합니다.

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

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

@Provides와 함께 Hilt 모듈을 사용할 수 있다. 두 메소드는 동일한 반환 유형을 갖지만 한정자는 다음과 같이 두 가지의 서로 다른 결합으로 메서드에 라벨을 지정한다.

@Module
@InstallIn(ApplicationComponent::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()
    }
}

다음과 같이 필드 또는 매개변수에 해당 한정자로 애노테이션을 지정하여 필요한 특정 유형을 삽입할 수도 있다.

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

class ExampleServiceImpl @Inject constructor(
    @AuthInterceptorOkHttpClient private val okHttpClient: OkHttpClient
) : ...

@AndroidEntryPoint
class ExampleActivity: AppCompatActivity() {

    @AuthInterceptorOkHttpClient
    @Inject lateinit var okHttpClient: OkHttpClient
}

9. 사전 정의된 한정자

사전 정의된 한정자를 제공하는데 예로 Context 한정자는 아래와 같다.

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

class AnalyticsServiceImpl @Inject constructor(
    @ApplicationContext context: Context
) : AnalyticsService { ... }

// The Application binding is available without qualifiers.
class AnalyticsServiceImpl @Inject constructor(
  application: Application
) : AnalyticsService { ... }

// The Activity binding is available without qualifiers.
class AnalyticsAdapter @Inject constructor(
  activity: FragmentActivity
) { ... }
Android 구성요소 기본 결합
ApplicationComponent Application
ActivityRetainedComponent Application
ActivityComponent Application, Activity
FragmentComponent Application, Acitivity, Fragment
ViewComponent Application, Activity, View
ViewWithFragmentComponent Application, Acitivyt, Fragment, View
ServiceComponent Application, Service

10. Hilt가 지원하지 않는 클래스에 종속 항목 삽입

Hilt가 지원하지 않는 클래스에 필드 삽입을 실행해야 할 경우가 있는데 이때에는 @EntryPoint 애노테이션을 사용하여 만들 수 있습니다. EntryPoint는 Hilt가 관리하는 코드와 그렇지 않은 코드 사이의 경계입니다.

@EntryPoint로 지정된 인터페이스를 정의하고 한정자를 포함해야 합니다. 그리고 다음과 같이 @InstallIn을 추가하여 설치할 구성요소를 지정합니다.

class ExampleContentProvider: ContentProvider() {
    @EntryPoint
    @InstallIn(ApplicationComponent::class)
    interface ExampleContentProviderEntryPoint {
        fun anlyticsService(): AnalyticsService
    }
}

EntryPoint에 엑세스하기 위해서는 EntryPointAccessors를 사용합니다. 매개변수는 구성요소 인스턴스이거나 구성요소 소유자 역할을 하는 @AndroidEntryPoint객체여야 합니다. 매개변수로 전달하는 구성요소와 EntryPointAccessors 정적 메서드가 모두 @EntryPoint 인터페이스의 @InstallIn 애노테이션에 있는 Android클래스와 일치하는지 확인합니다.

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가 ApplicationComponent에 설치되어 있으므로 ApplicationContext를 사용하여 검색한다. 결합이 ActivityComponent에 있다면 ActivityContext를 대신 사용해야 합니다.

참고

반응형