프로그래밍 놀이터/안드로이드, Java

[android] DI with Hilt - Hilt 에 대해 알아보자

돼지왕 왕돼지 2022. 1. 26. 16:54
반응형

 

Dependency 추가

 
-
build.gradle 에 hilt-android-gradle-plugin 을 추가하자.
buildscript {
   ...
   ext.hilt_version = '2.35'
   dependencies {
       ...
       classpath "com.google.dagger:hilt-android-gradle-plugin:$hilt_version"
   }
}
 
-
app/build.gradle 에 plugin 과 dependency 추가하자.
plugins {
   kotlin("kapt")
   id("dagger.hilt.android.plugin")
}

android {
   ...
}

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

 

-
참고로 data binding 과 Hilt 를 둘 다 쓰는 경우 Android Studio 4.0 이상이 요구된다.
 
-
Hilt 는 Java8 feature 를 사용하므로 app/build.gradle 에 Java8 이상을 설정해주자.
android {
    ...
    compileOptions {
        sourceCompatibility = JavaVersion.VERSION_1_8
        targetCompatibility = JavaVersion.VERSION_1_8
    }
}
 
 

Hilt application class

 
-
@HiltAndroidApp annotated 된 Application class 가 필요하다.
해당 annotation 이 Hilt code gen 을 trigger 한다.
 
-
생성된 Hilt component 는 Application 의 lifecycle 에 붙어서 dependency 를 제공해준다.
 
 

Android class 에 DI 하자.

 
-
Android class 들에 @AndroidEntryPoint annotation 을 추가해주자.
Activity, Fragment, View, Service, BroadcastReceiver 에 추가할 수 있다.
 
ViewModel 은 @HiltViewModel 을 세팅해준다.
 
-
@AndroidEntryPoint 로 마킹해줄 때는 dependent 하는 클래스들도 @AndroidEntryPoint 로 마킹해주어야 한다.
예를 들어 Fragment 에 annotate 해주면, 사용하는 Activity 도 같이 마킹해주어야 한다.
 
-
Hilt 는 AppCompatActivity 와 같이 ComponentActivity 를 상속하는 activity 에 대해서만 지원한다.
Hilt 는 androidx.Fragment 를 상속하는 fragment 에 대해서만 지원된다.
Hilt 는 retained fragment 에 대해서는 지원하지 않는다.
 
 
-
@AndroidEntryPoint 는 마킹된 각각에 대한 Hilt component 를 만든다.
이 component 들은 Component hierarchy 에 명시된 parent 로부터 dependency 를 받을 수 있다.
dependency 를 받기 위해서는 @Inject annotation 을 써준다.
Hilt 에 의해 inject 되는 field 는 private 이 될 수 없다. private 인 경우 compile error 가 발생한다.
@AndroidEntryPoint
class ExampleActivity : AppCompatActivity() {
  @Inject lateinit var analytics: AnalyticsAdapter
  ...
}
 
-
Hilt inject 하는 class 는 injection 을 이용하는 다른 base class 를 가질 수 있다.
해당 class 들이 abstract 인 경우 @AndroidEntryPoint annotation 을 적을 필요가 없다.
 
 

Hilt bindings 정의하기

 
-
field injection 을 수행하려면, Hilt 는 dependency 를 어디서부터 어떻게 제공할지를 알아야 한다.
"binding" 은 dependency 에 대한 정보를 담고 있다.
 
-
Hilt 에 binding 정보를 제공하는 한 가지 방법은 constructor injection 이다.
@Inject annotation 을 class 의 constructor 에 쓰면 된다.
class AnalyticsAdapter @Inject constructor(
  private val service: AnalyticsService
) { ... }
 
annotated 된 클래스의 param 은 class 의 dependency 이다.
예제에서 AnalyticsAdapter 는 AnalyticsService 를 dependency 로 갖는다.
따라서 Hilt 는 AnalyticsService 를 반드시 제공해야 한다.
 
-
빌드시 Hilt 는 Android class 에 대한 Dagger component 를 생성한다.
그리고 Dagger 는 코드를 돌면서 다음 과정을 거친다.
1. dependency graph 를 만들면서 검증한다. 
2. runtime 에 사용될 코드를 생성한다.
 
 

Hilt modules

 
-
종종 constructor-inject 가 아닌 type 이 필요할 때가 있다.
external lib 에서 class 를 제공하는 경우라던지, interface 를 inject 하는 경우라던지..
이런 경우에 Hilt module 을 통해 Hilt 에 binding 정보를 제공해야 한다.
 
-
Hilt module 은 @Module 로 annoate 된다.
Dagger module 처럼 여기서 특정 type 의 instance 를 제공한다.
그러나 Dagger module 과는 다르게 Hilt module 은 @InstallIn 을 annotate 해서 Hilt 가 어떤 Android class 를 install 해야 하는지 명시해야 한다.
 
-
Hilt module 에 제공하는 dependency 는 Hilt module 이 설치된 모든 Android class 에서 사용할 수 있다.
 
 

* Inject interface instances with @Binds

 
-
@Binds annotation 은 Hilt 에게 interface 를 제공해야 할 때 어떤 녀석을 제공할지를 이야기해준다.
1. function return type 은 Hilt 에게 어떤 interface 를 제공할지를 이야기해준다.
2. function param 은 Hilt 에게 어떤 impl 을 제공할지를 이야기해준다.
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 은 @InstallIn(ActivityComponent::class) 로 annotate 되어있다. 이는 Activity 에 DI 를 한다는 의미이다.
모든 Activity 에서 AnalyticsModule 가 제공하는 것을 받을 수 있는 것을 의미한다.
 
 

* Inject instances with @Provides

 
-
외부 lib 의 class 를 제공하는 경우나 builder pattern 으로 만들어져야 하는 class 등의 경우에는 Constructor injection 이 어려울 수 있다.
이 경우에는 @Provides 를 사용할 수 있다.
 
-
Annotated function 은 다음의 정보를 Hilt 에 제공해준다.
1. function return type 은 Hilt 에게 어떤 type 을 제공하는지 알려준다.
2. function param 은 Hilt 에게 dependency type 을 알려준다.
3. function body 는 Hilt 에게 어떻게 type 을 제공할지를 알려준다. Hilt 는 제공할 때마다 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)
  }
}
 
 

* same type 에 대해 여러개의 binding 을 제공하기

 
-
같은 type 에 대해 다른 impl 을 제공하고 싶을 때가 있다.
이 경우 multiple binding 을 제공해야 한다.
이는 qualifier 를 정의하면 된다.
@Qualifier
@Retention(AnnotationRetention.BINARY)
annotation class AuthInterceptorOkHttpClient

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

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

 

이 경우 function 의 return type 은 동일하게 OkHttpClient 이지만, param 으로 전달되는 impl 이 각각 AuthInterceptor 와 OtherInterceptor 이다.
그리고 각각 @AuthInterceptorOkHttpClient 와 @OtherInterceptorOkHttpClient 로 annotate 되어 있다.
// 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
}
 
inject 되는 곳에 qualifier 를 넣어주면 알맞게 DI 된다.
 
위의 예에서 AnalyticsService 가 inject 될 때 OkHttpClient 를 dependency 로 찾게 되는데 이 때 qualifier 로 @AuthInterceptorOkHttpClient 가 명시되어 있기 때문에 interface 가 OkHttpClient 이지만 위에서 정의한 AuthInterceptor 가 inject 된다.
 
또, ExampleServiceImpl 이 inject 될 때 마찬가지로 OkHttpClient 를 dependency 로 찾게 되는데 이 때도 마찬가지로 qualifier 로 @AuthInterceptorOkHttpClient 가 명시되어 있기 때문에 interface 가 OkHttpClient 이지만 위에서 정의한 AuthInterceptor 가 inject 된다.
 
만약 @OtherInterceptorOkHttpClient 로 annotate 되었다면, OtherInterceptor 가 DI 된다.
 
-
만약 type 에 대해 qualifier 를 추가한다면, 제공하는 모든 dependency 에 대해 qualifier 를 제공하는 것이 좋다.
그렇지 않으면 잘못된 injecting 을 할 가능성이 있다.
 
 

* Hilt 에 미리 지정된 qualifier

 
-
Hilt 는 Context 에 대해 @ApplicationContext@ActivityContext 를 제공한다.
class AnalyticsAdapter @Inject constructor(
    @ActivityContext private val context: Context,
    private val service: AnalyticsService
) { ... }
 
Android class 에 대해 생성된 component 들

 

-
Field injection 을 하는 각각의 Android class 에 대해 @InstallIn 으로 annotated 된 매칭되는 Hilt component 가 생긴다.
각각의 Hilt component 는 각각의 Android class 에 inject 하는 책임을 가진다.
 
 
-
ActivityComponent 를 사용하는 경우 다음의 component 가 생긴다.
Hilt component Inject for
SingletonComponent Application
ActivityRetainedComponent N/A
ViewModelComponent ViewModel
ActivityComponent Activity
FragmentComponent Fragment
ViewComponent View
ViewWithFragmentComponent @WithFragmentBinding 으로 annotate 된 View
ServiceComponent Service
 
-
Hilt 는 Broadcast receiver 를 위한 component 를 생성하지는 않는다.
이는 SingletonComponent 로부터 바로 inject 받기 때문이다.
 
 

* Component 생명주기

 
-
Hilt 는 android class 의 lifecycle 에 따라 자동으로 component 를 생성하고 파괴한다.
Component Created at Destroyed at
SingletonComponent Application#onCreate() Application#onDestroy()
ActivityRetainedComponent  Activity#onCreate() Activity#onDestory()
ViewModelComponent ViewModel create ViewModel destroyed
ActivityComponent  Activity#onCreate() Activity#onDestory()
FragmentComponent Fragment#onAttach() Fragment#onDestroy()
ViewComponent  View#super() View destroyed
ViewWithFragmentComponent  View#super() View destroyed
ServiceComponent  Service#onCreate() Service#onDestroy()
 
-
ActivityRetainedComponent 는 config change 에 대해 살아남는다.
따라서 첫 Activity#onCreate() 에 생성되고 마지막 Activity#onDestory() 에 파괴된다.
 
 

* Component scopes

 
-
기본적으로 Hilt 의 bindings 는 unscoped 이다.
이 말은 app 이 binding 을 요구할 때마다 Hilt 는 새로운 instance 를 만든다는 말이다.
Hilt 는 특정 component 에 대해 scoped binding 을 할 수 있다.
같은 scope 을 가진 binding 에 대해서는 동일한 instance 를 준다.
Android class
Generated Component Scope
Application
SingletonComponent @Singleton
Activity
ActivityRetainedComponent @ActivityRetainedScope
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 되어 있기 때문에, 해당 activity 의 life 동안 동일한 AnalyticsAdapter 를 제공한다.
 
-
scoped binding 은 memory 에 object 를 참조하고 있어야 해서 비용이 높다고 말할 수 있다.
그래서 scoped binding 을 최소화하는 것이 좋다.
에를 들면 생성할 때 비용이 많이 드는 것 등만 scoped binding 으로 사용하는 것이 좋다.
 
-
만약 AnalyticsService 가 다른 곳에서도 동일 instance 로 사용되어야 한다면, SingletonComponent 로 할당하는 것이 적합하다.
 
 

* Component hierarchy

 
-
SingletonCompoent
    - ServiceComponent
    - ActivityRetainedComponent
        - ViewModelComponent
        - ActivityComponent
            - ViewComponent
            - FragmentComponent
                - ViewWithFragmentComponent
 
-
기본적으로 view 에 대해 field injection 을 하면, ViewComponent 가 ActivityComponent 에 정의된 binding 을 사용한다.
만약 FragmentComponent 에 정의된 binding 을 사용하고 싶고, view 가 fragment 안에 있다면, @AndroidEntryPoint 와 함께 @WithFragmentBindings 을 annotate 해주어야 한다.
 
 

* Component default binding

 
-
각각의 hilt component 는 default binding 과 함께 온다.
이 binding 은 일반적인 activity, fragment type 이고 특정 subclass 는 아니다.
왜냐하면 Hilt 는 모든 activity inject 에 대해 하나의 activity component 를 사용하기 때문이다.
각각의 activity 는 각각 다른 component instance 를 갖는다.
 
-
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
Service Component Application, Service
 
 

Hilt 에 의해 제공되지 않는 DI

 
-
Hilt 는 대부분의 Android class 에 대한  DI 를 지원 한다.
하지만 Hilt 가 지원하지 않는 class 에 field injection 을 해야 할 때가 있다.
 
이 경우 @EntryPoint 를 만들 수 있다.
entry point 는 Hilt 에서 관리되는 code 와 일반 code 사이의 경계선이다.
 
예를 들어 Hilt 는 content providers 에 대해 직접적인 지원을 하지 않는다.
만약 이에 대해 Hilt 를 이용하고 싶다면, binding 하고 싶은 type 에 @EntryPoint 와 적합한 qualifier 를 annotate 한다.
그리고 @InstallIn 을 통해 특정 component 를 명시해주면 된다.
class ExampleContentProvider : ContentProvider() {
  @EntryPoint
  @InstallIn(SingletonComponent::class)
  interface ExampleContentProviderEntryPoint {
    fun analyticsService(): AnalyticsService
  }
  ...
}
 
entry point 에 접근하기 위해서는 EntryPointAccessors 를 사용하며, 알맞은 context 를 사용해야 한다.
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()
    ...
  }
}
 
 

Hilt 와 Dagger

 
-
Hilt 는 Dagger 를 기반으로 만들어졌다.
Dagger 대비 Hilt 는 다음과 같은 목적을 갖는다.
1. Android app 관련 Dagger 관련된 infra 를 간단히 한다.
2. 기본 component set 을 제공하여, 쉬운 설정, 높은 가독성, 앱간 코드 쉐어를 쉽게 한다.
3. 다양한 build type(test, debug, release) 에 대해 쉬운 설정 방법을 제공한다.
 
-
Dagger 를 Android 에 쓰기 위해서는 boiler plate 가 많이 필요했다.
Hilt 는 다음을 통해 이를 줄여준다.
1. 통합 Android framework class 를 위한 component 제공.
2. Scope annotations
3. Predefined bindings (Application, Activity)
4. Predefined qualifiers (@ApplicationContext, @ActivityContext)
 
 

돼왕의 Summary

 
-
@HiltAndroidApp
    Application 에 정의
 
-
@AndroidEntryPoint
    주입 받는 포인트
 
-
@Inject
    field injection (받음)
    constructor injection (주입), constructor injection 의 경우 param 들도 injection 되어야 하며, private 일 수 없음
 
-
@Module
    provider 들 정의, InstallIn 과 함께 사용한다
@InstallIn
    제공 scope 정의
 
@Binds
    @Module 안에 정의하며, param 으로 전달되는 concrete 를 return 으로 명시한 interface 를 요구하는 곳에 매핑해준다.
@Providers
    @Module 안에 정의하며, 내부 구현으로 return 할 object 를 생성해준다.
 
-
@Qualifier
    같은 interface return 에 대해 다른 concrete class 를 return 하고자 할 때 정의하고, 이 녀석을 @Provider 와 Inject 당하는 쪽에 적어준다.
@Retention(AnnotationRetention.BINARY)
annotation class AnnotationName
 
-
@ApplicationContext, @ActivityContext
 
-
InstallIn 에 사용되는 Component 는 아래와 같다.
SingletonComponent -> Application -> @Singleton
ActivityRetainedComponent -> N/A (ConfigChange 에 대해 살아남고, 최종적인 destroy 시 제거된다) -> @ActivityRetainedScope
ViewModelComponent -> ViewModel -> @ViewModelScope
ActivityComponent -> Activity -> @ActivityScope
FragmentComponent -> Fragment -> @FragmentScope
ViewComponent -> View -> @ViewScope
ViewWithFragmentComponent -> @WithFragmentBinding 으로 annotate 된 View -> @ViewScoped
ServiceComponent -> Service -> @ServiceScoped
 
-
@EntryPoint
    Hilt injection 을 지원받지 못하는 class 에서 hilt 에서 제공하는 component 접근 가능
@InstallIn
 
 
 

 

반응형