프로그래밍 놀이터/안드로이드, 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
끝
반응형