Android: Hilt

yxnsx·2021년 4월 15일
1

Dev: Android

목록 보기
6/8
post-thumbnail

Hilt

  • 종속성 주입을 위해 사용하는 Android Jetpack의 라이브러리
  • 프로젝트의 모든 Android 클래스에 컨테이너를 제공하고 자동으로 수명주기를 관리한다.


Hilt 사용의 장점

  • DI 라이브러리인 Dagger를 기반으로 빌드되어, Dagger가 제공하는 컴파일 시간 정확성, 런타임 성능, 확장성 및 Android Studio 지원의 이점을 누릴 수 있다.

  • 안드로이드 애플리케이션에서 Dagger 사용과 관련된 많은 보일러플레이트 코드를 줄일 수 있다.

  • 컴포넌트에 사용할 Scope 어노테이션을 자동으로 생성해준다.

  • ApplicationActivity와 같은 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 plugindependencies 추가

  • HiltDataBinding을 모두 사용하는 프로젝트의 경우, Android Studio 버전이 4.0 이상이어야 한다.
...
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"
}

3️⃣ app/build.gradle에 Java 8 compileOptions 추가

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
  • @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 어노테이션으로 처리된 생성자의 파라미터들은 해당 클래스의 종속성이다.
  • AnalyticsAdapterAnalyticsService를 종속성으로 지니므로, HiltAnalyticsService의 인스턴스를 어떻게 제공하는지에 대한 방법을 반드시 알고있어야 한다.


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에 알려준다.
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 moduleAnalyticsModuleHiltExampleActivity로 종속성을 주입하도록 하기 위해 @InstallIn(ActivityComponent::class) 어노테이션으로 처리 되었다.
  • @InstallIn(ActivityComponent::class) 어노테이션은 AnalyticsModule의 모든 종속성을 앱의 모든 액티비티에서 사용할 수 있음을 의미한다.

✔️ @Provides를 사용하여 인스턴스 주입

  • 인터페이스는 타입을 constructor-inject 할 수 없는 유일한 경우가 아니다.
  • constructor-injection은 외부 라이브러리(Retrofit, Room 등)의 클래스 등과 같은 소유하고 있지 않은 클래스 및 빌더 패턴으로 생성된 인스턴스에서도 불가능하다.
  • 이러한 경우, Hilt module 내부에 함수를 만들고 해당 함수를 @Provides 어노테이션으로 처리하여 타입 인스턴스를 제공하는 방법을 Hilt에 알릴 수 있다.
  • @Provides 어노테이션이 있는 함수는 다음의 정보를 Hilt에 알려준다
  • 함수의 리턴타입은 함수가 인스턴스를 제공하는 타입을 Hilt에 알려준다.
  • 함수의 파라미터는 타입의 dependencies를 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)
    }
}

✔️ 동일한 타입에 대해 여러 바인딩 제공

  • 같은 타입의 종속성에 여러 구현을 제공하기 위해 Hilt가 필요한 경우, Hilt에 어려 종류의 바인딩을 제공해야 한다.
  • qualifier를 통해 여러 종류의 바인딩을 정의할 수 있다.
  • qualifier는 해당 타입에 여러 종류의 바인딩이 정의되어있는 경우, 해당 타입의 특정 바인딩을 식별하는데 사용한다.
  • qualifier를 타입에 추가하는 경우, 해당 종속성을 제공할 수 있는 모든 방법에 qualifier를 추가하는 것이 좋다.
  • qualifier 없이 기본 또는 공통 구현을 그대로 둘 경우, 오류가 발생하기 쉬우며 Hilt가 잘못된 종속성을 주입할 수 있다.
  • 예시로, OkHttpClient의 두 가지 다른 구현을 Hilt에 제공하기 위해서는 @Qualifier 어노테이션을 이용하여 @Binds 또는 @Provides 메서드에 사용할 qualifier를 정의해야 한다.
@Qualifier
@Retention(AnnotationRetention.BINARY)
annotation class AuthInterceptorOkHttpClient

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

  • 두 모듈은 같은 리턴타입을 지니고 있지만 qualifier가 두 모듈을 다른 바인딩으로 지정하고 있다.
@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로 어노테이션을 추가하여 필요한 특정 타입을 주입할 수 있다.
// 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
}

✔️ 사전 정의된 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 componentInject for
SingletonComponentApplication
ActivityRetainedComponentN/A
ViewModelComponentViewModel
ActivityomponentActivity
FragmentComponentFragment
ViewComponentView
ViewWithFragmentComponentView annotated with @WithFragmentBindings
ServiceComponentService
  • HiltSingletonComponent로부터 바로 BroadcastReceiver를 주입하기 때문에 BroadcastReceiver에 대해서는 컴포넌트를 생성하지 않는다.

✔️ 컴포넌트 생명주기

  • Hilt는 생성된 컴포넌트 클래스의 인스턴스를 해당 Android 클래스의 생명주기에 따라 자동으로 생성하고 파괴한다.

Generated componentCreated atDestroyed at
SingletonComponentApplication#onCreate()Application#onDestroy()
ActivityRetainedComponentActivity#onCreate()Activity#onDestroy()
ViewModelComponentViewModel createdViewModel destroyed
ActivityomponentActivity#onCreate()Activity#onDestroy()
FragmentComponentFragment#onAttach()Fragment#onDestroy()
ViewComponentView#super()View destroyed
ViewWithFragmentComponentView#super()View destroyed
ServiceComponentService#onCreate()Service#onDestroy()

  • ActivityRetainedComponentActivity#onCreate()의 시작에 생성되고 Activity#onDestroy()의 끝에 파괴되므로, 구성 변경 전체에 걸쳐 살아있다.

✔️ 컴포넌트 Scope

  • 기본적으로 Hilt의 모든 바인딩은 Scope가 지정되지 않으며, 앱에서 바인딩을 요청할 때마다 Hilt는 필요한 유형의 새 인스턴스를 만든다.
  • 하지만 Hilt의 바인딩을 특정 컴포넌트 Scope로 지정할 경우, 바인딩 범위가 지정된 컴포넌트의 인스턴스당 한 번만 Scope 바인딩을 생성하고 해당 바인딩에 대한 모든 요청은 동일한 인스턴스를 공유한다.

Android classGenerated componentScope
ApplicationSingletonComponent@Singleton
ActivityActivityRetainedComponent@ActivityRetainedScoped
ViewModelViewModelComponent@ViewModelScoped
ActivityActivityComponent@ActivityScoped
FragmentFragmentComponent@FragmentScoped
ViewViewComponent@ViewScoped
View annotated with @WithFragmentBindingsViewWithFragmentComponent@ViewScoped
ServiceServiceComponent@ServiceScoped

  • 컴포넌트의 바인딩 범위를 지정하면 제공된 객체의 컴포넌트가 제거될 때까지 메모리에 남아있기 때문에 비용이 많이 들 수 있다.
  • 때문에 범위가 지정된 바인딩의 사용을 최소화하는 것이 좋다.
  • 다만, 특정 범위 내에서 동일한 인스턴스를 사용해야 하는 내부 바인딩, 동기화가 필요한 바인딩, 생성 비용이 많이 드는 바인딩에 대해서는 컴포넌트 Scope 바인딩을 사용하는 것이 적절하다.
// 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)
    }
}

✔️ 컴포넌트 Hierarchy

  • 컴포넌트에 모듈을 installing하면 컴포넌트 hierarchy에 따라 해당 컴포넌트 또는 하위의 다른 자식 컴포넌트의 바인딩이 종속성으로서 해당 컴포넌트의 바인딩에 접근할 수 있다.
  • Hilt가 생성하는 컴포넌트의 Hierarchy :arrow_down:


✔️ 컴포넌트 기본 바인딩

  • 각각의 Hilt 컴포넌트에는 Hilt가 사용자 커스텀 바인딩에 종속성으로 주입할 수 있는 기본 바인딩이 함께 제공된다.
  • Hilt는 single activity 컴포넌트 정의를 바탕으로 모든 activity를 주입하기 때문에 이러한 바인딩은 특정 하위 클래스가 아닌 일반 ActivityFragment 유형이다.
  • Activity는 이 컴포넌트의 다른 인스턴스를 지닌다.

Android componentDefault bindings
SingletonComponentApplication
ActivityRetainedComponentApplication
ViewModelComponentSavedStateHandle
ActivityComponentApplication, Activity
FragmentComponentApplication, Activity, Fragment
ViewComponentApplication, Activity, View
ViewWithFragmentComponentApplication, Activity, Fragment, View
ServiceComponentApplication, Service

  • application context의 바인딩은 @ApplicationContext 어노테이션을 통해서도 가능하다.
class AnalyticsServiceImpl @Inject constructor(
    @ApplicationContext context: Context
) : AnalyticsService { ... }

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

  • activity context의 바인딩은 @ActivityBinding 어노테이션을 통해서도 가능하다.
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 ProviderHilt를 사용해서 일부 종속성을 가져오도록 하려면 각 바인딩 타입에 @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

0개의 댓글