Android Hilt
Hilt
- 프로젝트 내 모든 Android class에 대해 container를 제공하고 해당 lifecycle을 자동으로 관리함으로써 애플리케이션에서 DI를 사용하는 표준적인 방법을 제공
- Dagger를 기반으로 구축되어 Dagger가 제공하는 compile-time correctness, runtime performance, scalability, 그리고 Android Studio 지원의 이점을 활용
container
DI 관점에서 객체를 생성하고(construct), 보관하고(cache), 수명(lifecycle)에 맞춰 재사용하거나 폐기하는 주체
container가 하는 일
- dependency graph를 구성
- 인스턴스 생성과 주입
- scope에 따른 캐싱, 재사용
- lifecycle에 맞춘 관리
모든 Android 클래스에 대해 container를 제공한다는 것의 의미
Application/Activity/Fragment/View/ViewModel/Service 등 각 Android class 타입에 대응하는 component를 자동 생성한다. 그리고 그 component들이 parent-child 계층 구조로 연결되어, 상위 component에 설치된 binding을 하위에서 재사용할 수 있게 한다.
@HiltAndroidApp
Hilt를 사용하는 모든 앱은 @HiltAndroidApp으로 annotation된 Application class를 포함해야 한다.
@HiltAndroidApp은 Hilt의 codegen을 트리거하고 이 과정에는 application-level dependency container 역할을 하는 Application의 base class 생성이 포함된다.
이렇게 생성된 Hilt component는 Application 객체의 lifecycle에 연결되며, 해당 객체에 dependency를 제공한다. 이 component는 앱 전체에서 사용되는 parent component이므로, 다른 component들이 여기서 제공하는 dependency에 접근할 수 있다.
1
2
3
4
5
// android/nowinandroid
@HiltAndroidApp
class NiaApplication : Application(), ImageLoaderFactory {
...
}
@AndroidEntryPoint
Application 클래스에서 Hilt 설정이 완료되고 애플리케이션 수준의 component가 사용 가능해지면, Hilt는 @AndroidEntryPoint annotation이 적용된 다른 Android 클래스에 의존성을 제공할 수 있다.
Hilt가 현재 지원하는 Android 클래스
Application(@HiltAndroidApp사용)ViewModel(HiltViewModel사용)Activity,Fragment,View,Service,BroadcastReceiver
Android 클래스에 @AndroidEntryPoint를 annotation으로 적용한 경우, 해당 클래스에 의존하는 Android 클래스에도 반드시 @AndroidEntryPoint를 적용해야 한다.
1
2
3
// android/nowinandroid
@AndroidEntryPoint
internal class SyncNotificationsService : FirebaseMessagingService()
1
2
@AndroidEntryPoint
class MainActivity : ComponentActivity()
Android 클래스에 @AndroidEntryPoint를 annotation으로 적용한 경우, 해당 클래스에 의존하는 Android 클래스에도 반드시 @AndroidEntryPoint를 적용해야 한다.
@AndroidEntryPoint는 프로젝트 내 각 Android 클래스마다 개별적인 Hilt Component를 생성한다. 이 component들은 각각의 parent class 로부터 의존성을 받을 수 있다.
component로부터 의존성을 얻기 위해서는 @Inject annotation을 사용하여 field injection을 수행한다.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
@AndroidEntryPoint
class MainActivity : ComponentActivity() {
@Inject
lateinit var lazyStats: dagger.Lazy<JankStats>
@Inject
lateinit var networkMonitor: NetworkMonitor
@Inject
lateinit var timeZoneMonitor: TimeZoneMonitor
@Inject
lateinit var analyticsHelper: AnalyticsHelper
@Inject
lateinit var userNewsResourceRepository: UserNewsResourceRepository
...
}
[!NOTE] Hilt가 주입하는 field는
private일 수 없다.
Hilt가 injection하는 클래스는, injection을 사용하는 다른 base class를 함께 가질 수도 있다. 다만 그 base class가 abstract라면 @AndroidEntryPoint annotation이 필요하지 않다.
- 실제로 Android component인 클래스(Activity, Fragment 등) 가 abstract base class 를 상속하고 그 abstract base class에서도 injection(
@Inject)을 사용하더라도@AndroidEntryPoint는 최종 concrete Android 클래스에만 붙이면 된다.
Hilt 바인딩 정의하기
field injection을 수행하려면, Hilt는 해당 component로부터 필요한 dependency의 인스턴스를 어떻게 제공할 지 알아야 한다. binding은 특정 타입의 인스턴스를 dependency로 제공하기 위해 필요한 정보를 담고 있다.
Hilt에 바인딩 정보를 제공하는 한 가지 방법은 constructor injection이다. 클래스의 constructor에 @Inject annotation을 사용하면, 해당 클래스의 인스턴스를 Hilt가 어떻게 제공해야 하는지를 알려줄 수 있다.
1
2
3
class AnalyticsAdapter @Inject constructor(
private val service: AnalyticsService
) { ... }
위 문단에서 말하는 해당 클래스의 의미
@Inject constructor(...)가 직접 선언된 클래스- 즉, Hilt가 생성 책임을 가지게 되는 클래스
- 위 예시에서는
AnalyticsAdapter를 의미한다.
AnalyticsAdatper 타입이 필요하면 이 constructor를 호출해서 만들면 된다. 단, constructor parameter인 AnalyticsService도 필요하다 그러면 AnalyticsService에 대한 binding도 존재해야 한다.
annotation이 적용된 constructor의 parameter들은 해당 클래스의 dependency이다. 예를 들어, 위 예시에서 AnalyticsAdapter는 AnalyticsService를 dependency로 가지고 있다. 따라서 Hilt는 AnalyticsService의 인스턴스를 어떻게 제공할 수 있는지도 반드시 알고 있어야 한다.
[!NOTE] 빌드 시점에 Hilt는 Android 클래스를 대상으로 Dagger component를 생성한다. 그 다음 Dagger는 코드를 분석하며 다음 작업을 수행한다.
- dependency graph를 생성하고 검증하여, 해결되지 않은 dependency나 dependency cycle이 없는지 확인한다.
- 런타임에 실제 객체와 그 dependency를 생성하는 데 사용되는 클래스들을 자동으로 생성한다(generate).
Hilt modules
Hilt module은 @Module annotation이 적용된 클래스이다. 특정 타입의 인스턴스를 어떻게 제공할지 Hilt에 알려주는 역할을 한다. Hilt module은 반드시 @InstallIn annotation을 사용하여 해당 module이 어떤 Android 클래스에 설치(사용)될 것인지를 명시해야 한다.
Hilt modules에서 제공한 dependency는, 해당 module을 설치한 Android 클래스와 연관된 모든 generated component에서 사용할 수 있다.
- 어떤 Hilt module을 특정 scope에 설치하면, 그 scope 아래에 있는 모든 Android 클래스에서 그 dependency를 공통으로 사용할 수 있다.
1
2
3
4
5
6
7
@Module
@InstallIn(SingletonComponent::class)
object NetworkModule {
@Provides
fun provideApiService(): ApiService = ApiServiceImpl()
}
NetworkModule은SingletonComponent에 설치됨- 즉, Application scope
- 결과적으로
Activity,Fragment,ViewModel,Service등 아래에 존재하는 모든 generated component에서ApiService를 injection 받을 수 있다.
[!NOTE] Hilt의 codegen은 Hilt를 사용하는 모든 Gradle module에 접근할 수 있어야 하므로,
Application클래스를 컴파일 하는 Gradle moudle에는 모든 Hilt module과 constructor injection을 사용하는 클래스들이 transitive dependency로 포함되어 있어야 한다.
@Binds
1
2
3
class AnalyticsAdapter @Inject constructor(
private val service: AnalyticsService
) { ... }
AnalyticsService가 interface라면, constructor injection을 사용할 수 없다. 이 경우에는 Hilt module 안에서 @Binds annotation이 적용된 abstract 함수를 만들어 Hilt에 바인딩 정보를 제공해야 한다.
@Binds annotation은 interface 타입의 인스턴스가 필요할 때 어떤 implementation을 사용할 지 Hilt에 알려주는 역할을 한다.
annotation이 적용된 함수는 Hilt에 다음 정보를 제공한다.
- 함수의 return type: Hilt가 어떤 interface 타입의 인스턴스를 제공해야 하는지 알려준다.
- 함수의 parameter: 해당 interface에 대해 어떤 implementation을 제공할지 알려준다.
1
2
3
4
5
6
7
8
9
10
11
12
// android/nowinandroid
@Module
@InstallIn(SingletonComponent::class)
abstract class DataModule {
@Binds
internal abstract fun bindsTopicRepository(
topicsRepository: OfflineFirstTopicsRepository,
): TopicsRepository
...
}
@Binds를 사용할 경우 module은 반드시 abstract class를 사용하고 함수도 반드시 abstract를 사용해야 한다. 실제 구현 코드는 Hilt가 codegen으로 자동 생성하기 때문이다.- 위 코드 해석하기 : Application scope(singleton)에서
TopicRepository타입이 필요하면OfflineFirstTopicsRepository를 사용해라.
constructor injection vs binds
- constructor inejction : 클래스의 constructor에
@Inject를 붙여 “이 클래스의 인스턴스는 이렇게 만들어라”라고 Hilt에 알려주는 방식 @Binds: interface <-> implementation을 연결해주는 바인딩 방식
@Provides
다음과 같은 경우에도 constructor injection을 사용할 수 없다.
- 클래스가 외부 라이브러리에서 제공되어, 직접 소유하지 않은 경우
- ex.
Retrofit,OkHttpClient,Room
- ex.
- 인스턴스 생성 시 builder pattern을 사용해야 하는 경우
@Provides가 적용된 함수는 Hilt에 다음 정보를 제공한다.
- 함수의 return type : Hilt가 어떤 타입의 인스턴스를 제공하는 지 알려준다.
- 함수의 parameter들 : 해당 타입을 생성하는 데 필요한 dependency들을 알려준다.
- 함수의 body : 해당 타입의 인스턴스를 어떻게 생성할 지를 정의한다. Hilt는 이 타입의 인스턴스가 필요할 때마다 함수 body를 실행한다.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
// android/nowinnandroid
@Module
@InstallIn(SingletonComponent::class)
internal object NetworkModule {
...
@Provides
@Singleton
fun okHttpCallFactory(): Call.Factory =
OkHttpClient.Builder()
.addInterceptor(
HttpLoggingInterceptor()
.apply {
if (BuildConfig.DEBUG) {
setLevel(HttpLoggingInterceptor.Level.BODY)
}
},
)
.build()
}
외부 라이브러리 타입 OkHttpClient은 constructor injection이 불가능하거나, builder pattern으로 생성해야 하므로 @Provides로 Hilt에 생성 방법을 알려준다.
- return type :
Call.Factory타입을 제공한다. - parameters : 이 타입을 만들 때 필요한 dependency가 없다.
- body :
Call.Factory를 어떻게 생성하는지 - Hilt는 누군가
Call.Factory를 injection 받으려고 하면, 이 함수를 호출해 인스턴스를 얻는다.
외부 라이브러리 타입에 construction injection이 안되는 이유
외부 라이브러리 타입은 우리가 constructor에 @Inject를 붙일 수 없기 때문이다.
constructor injection이 가능하려면 다음 조건을 만족해야 한다.
- constructor에
@Inject를 붙일 수 있어야 함 - 그 클래스의 소스 코드를 직접 수정할 수 있어야 함.
- Hilt/Dagger가 그 constructor를 호출해 인스턴스를 생성함
외부라이브러리 타입에서 OkHttpClient를 예로 들어보면
- 외부 라이브러리에서 제공됨
- 소스 코드를 수정할 수 없음
- constructor가
@Inject로 annotation되어 있지 않음 - 대부분 builder pattern으로만 생성 가능
결국 다음과 같은 코드는 만들 수가 없다.
1
class OkHttpClient @Inject constructor(...)
builder 패턴이면 binds사용이 안되는 이유
@Binds는 구현체를 “어떻게 만들지를 정의하는 기능”이 없고 오직 “어떤 구현체를 쓸지”만 연결한다. @Binds는 이런 선언만 한다.
인터페이스(또는 상위 타입) T가 필요하면, 구현체 Impl을 써라
1
2
@Binds
abstract fun bindRepo(impl: RepoImpl): Repo
- 함수 body가 없음(abstract)
- 생성 로직을 쓸 수 없음
- Hilt는
RepoImpl을 알아서 만들어야 한다. 즉, 보통@Inject constructor로 생성 가능해야 한다.
하지만 builder/factory 타입은 알아서 만들 수가 없다.
1
2
3
OkHttpClient.Builder()
.addInterceptor(...)
.build()
1
2
3
4
5
6
7
8
9
10
11
12
13
14
// android/nowinandroid
@Module
@InstallIn(SingletonComponent::class)
internal object DatabaseModule {
@Provides
@Singleton
fun providesNiaDatabase(
@ApplicationContext context: Context,
): NiaDatabase = Room.databaseBuilder(
context,
NiaDatabase::class.java,
"nia-database",
).build()
}
이런 타입은
@Inject constructor가 없거나- constructor가 숨겨져 있거나 (private)
- builder/factory 호출이 필수이고
- 필요한 설정값/환경(context, baseUrl 등)이 들어간다.
즉, Hilt가 “constructor를 호출해서 만들기” 방식으로는 생성할 수 없다.
출처
- https://developer.android.com/training/dependency-injection/hilt-android
- https://github.com/android/nowinandroid