Hilt는 안드로이드를 위한 DI 프레임워크 중 하나다. 기존의 Dagger는 높은 러닝커브와 보일러 플레이트 코드, 안드로이드 생명주기를 고려하는 의존성 주입이 없다는 게 단점이었으나, Hilt가 이를 보완해 등장했다.
이 포스팅에서 Hilt 사용을 위한 초기 설정은 안드로이드 스튜디오 Chipmunk 버전, 코틀린을 기준으로 한다(안드로이드 스튜디오 버전에 따라 gradle 파일 구조가 조금씩 다르기 때문에 명시한다).
plugins {
id 'dagger.hilt.android.plugin'
id 'kotlin-kapt'
}
compileOptions {
sourceCompatibility JavaVersion.VERSION_1_8
targetCompatibility JavaVersion.VERSION_1_8
}
dependecies {
implementation 'com.google.dagger:hilt-android:2.42'
kapt 'com.google.dagger:hilt-compiler:2.42'
}
buildscript {
dependencies {
classpath 'com.google.dagger:hilt-android-gradle-plugin:2.42'
}
}
안드로이드 프로젝트에 Hilt를 적용하기 위해선 Hilt 어노테이션의 개념과 사용 방법에 대해 짚고 넘어가야한다.
@HiltAndroidApp
: 앱의 진입점으로, Hilt를 사용하는 안드로이드 앱은 해당 어노테이션으로 명시된 Application을 상속한 클래스가 반드시 필요하다. Hilt 컴포넌트로 생성된 이 클래스는 Application 객체의 수명 주기에 연결되고, 이와 관련한 종속 항목을 제공한다. 또한 앱의 상위 컴포넌트로서 다른 컴포넌트가 이 컴포넌트에서 제공하는 종속 항목에 액세스 할 수 있도록 한다.
@HiltAndroidApp
class App : Application() {
...
}
@AndroidEntryPoint
: Application 클래스에 Hilt를 설정하고 상위의 Hilt 컴포넌트로서 사용할 수 있게 되면, Hilt는 @AndroidEntryPoint가 명시된 다른 안드로이드 클래스에 종속 항목을 제공할 수 있게 된다. 즉, 우리는 필요한 모듈(ex ViewModel)을 주입해야 하는 안드로이드 클래스(ex Activity, Fragment)에 @AndroidEntryPoint을 지정할 수 있다.
@AndroidEntryPoint
class MainFragment : Fragment() {
...
}
@AndroidEntryPoint는 다음의 안드로이드 컴포넌트들에 사용할 수 있다.
그렇다면 필요한 '모듈'은 어떤 어노테이션으로 지정할 수 있을까?
먼저 내가 공식 문서를 참고하면서 ViewModel과 Repository에 Hilt를 적용하려고 약간의 삽질을 했던🤣 경험을 잠깐 적어보도록 하겠다.
Repository.kt
class Repository {
...
}
ViewModel.kt
@HiltViewModel
class ViewModel @Inject constructor(
private val repo: Repository
) : ViewModel() {
}
@HiltViewModel
: 주입해야하는 ViewModel을 지정하는 어노테이션이다.
Fragment.kt
@AndroidEntryPoint
class MainFragment : Fragment() {
private val viewModel : ViewModel by viewModels()
...
}
처음에는 이렇게만 하면 프래그먼트에 뷰모델을 주입할 수 있을 것 같았으나, 다음의 오류가 발생하였다.
Hilt cannot be provided without an @Provides-annotated method...
Hilt 모듈을 지정하기 위해서 사용하는 어노테이션은 @Module
이다. @Module을 사용하는 이유는 인터페이스나 외부 라이브러리 클래스(Retrofit, Room)와 같은 경우 Hilt가 객체를 어떻게 생성해야 할 지 모르기 때문에, 이러한 상황을 해결하기 위해 @Module을 사용해 필요한 클래스들을 모듈화 해야한다고 한다. 내가 만들었던 Repository 클래스의 경우도 마찬가지였다. 따라서 다음과 같이 Hilt 모듈을 만들고 그 안에 Repository 객체를 제공해주는 provideRepo 함수를 정의해주었다.
@Module
@InstallIn(ViewModelComponent::class)
internal object RepoModule {
@Provides
@ViewModelScoped
fun provideRepo() : Repository = Repository()
}
@Module
: Hilt 모듈을 지정하기 위한 어노테이션이다. 이 어노테이션이 지정된 클래스는 모듈 내에 정의된 특정 클래스에 대한 객체를 제공하는 방법을 Hilt에게 알려준다.
@Installin
: @Module로 지정된 클래스는 @Installin을 지정해서 모듈이 어떤 스코프에서 사용되어야 하는지 알려야한다.
위의 표는 Hilt 공식문서에 나와있는 Hilt 컴포넌트들의 생명 주기다. @Installin에 사용되는 Hilt 컴포넌트들은 각자의 생명 주기를 갖고 있고, 해당 모듈들은 이 컴포넌트의 생명주기(스코프)에 맞춰 그대로 따라간다.
@Provides
: 클래스의 인스턴스를 제공해야 할 때 사용한다.
@Binds
: 인터페이스의 인스턴스를 제공해야 할 때 사용한다.
@ViewModelScoped
: 해당 어노테이션이 지정된 모듈은 @HiltViewModel로 지정된 특정 ViewModel에 단일 객체로서 제공된다.
@ViewModelScoped에 대한 이해를 돕기 위해 Hilt 공식문서에 제시된 예제를 가져왔다.
@Module
@InstallIn(ViewModelComponent::class)
internal object ViewModelMovieModule {
@Provides
@ViewModelScoped
fun provideRepo(handle: SavedStateHandle) =
MovieRepository(handle.getString("movie-id"));
}
class MovieDetailFetcher @Inject constructor(
val movieRepo: MovieRepository
)
class MoviePosterFetcher @Inject constructor(
val movieRepo: MovieRepository
)
@HiltViewModel
class MovieViewModel @Inject constructor(
val detailFetcher: MovieDetailFetcher,
val posterFetcher: MoviePosterFetcher
) : ViewModel() {
init {
// Both detailFetcher and posterFetcher will contain the same instance of
// the MovieRepository.
}
}
두 클래스 MovieDetailFetcher, MoviePosterFetcher는 @ViewModelScoped로 지정된 MovieRepository 객체를 주입받고, @HiltViewModel로 지정된 MovieViewModel은 두 클래스의 객체를 주입받는다. detailFetcher와 posterFetcher는 서로 다른 클래스의 객체지만, @ViewModelScoped로 지정된 MovieRepository 객체를 주입받았기 때문에 동일한 MovieRepository 객체를 사용하게 된다.
ViewModel에 Repository를 주입해서 사용하는 것에 이상이 없다면 이제 @AndroidEntryPoint가 지정된 안드로이드 컴포넌트에 ViewModel을 주입해서 사용하면 되는데, 주입 방법은 by viewModles() 키워드를 사용하면 된다.
Fragment.kt
@AndroidEntryPoint
class MainFragment : Fragment() {
private val viewModel : ViewModel by viewModels()
...
}
Composable 내에서 Hilt를 사용한 종속성 주입이 가능하다. Compose에선 화면 수준의 Composable(Root Composable) 내에서 ViewModel 객체에 접근할 수 있다.
class MyViewModel : ViewModel() { /*...*/ }
@Composable
fun MyScreen(
viewModel: MyViewModel = viewModel()
) {
// use viewModel here
}
위 예제에서 사용된 viewModel() 함수는 @HiltViewModel로 지정된 ViewModel을 자동으로 사용한다.
@HiltViewModel
class MyViewModel @Inject constructor(
private val savedStateHandle: SavedStateHandle,
private val repository: ExampleRepository
) : ViewModel() { /* ... */ }
@Composable
fun MyScreen(
viewModel: MyViewModel = viewModel()
) { /* ... */ }
위 방법을 사용하기 위해선 build.gradle(Module)에 다음의 종속 항목을 추가해야 한다.
implementation 'androidx.lifecycle:lifecycle-viewmodel-compose:2.4.1'
Navigation Compose를 사용할 때는 hiltViewModel 함수를 사용하여 @HiltViewModel이 지정된 ViewModel의 인스턴스를 가져 올 수 있다.
import androidx.hilt.navigation.compose.hiltViewModel
@Composable
fun MyApp() {
NavHost(navController, startDestination = startRoute) {
composable("example") {
val viewModel = hiltViewModel<MyViewModel>()
MyScreen(viewModel)
}
}
}
위 방법을 사용하기 위해선 build.gradle(Module)에 다음의 종속 항목을 추가해야 한다.
implementation 'androidx.hilt:hilt-navigation-compose:1.0.0'
https://dagger.dev/hilt/gradle-setup
https://velog.io/@201/Hilt%EB%A5%BC-%EC%82%AC%EC%9A%A9%ED%95%98%EC%97%AC-%EC%95%88%EB%93%9C%EB%A1%9C%EC%9D%B4%EB%93%9C-MVVM-%EC%95%84%ED%82%A4%ED%85%8D%EC%B3%90-%EA%B5%AC%ED%98%84%ED%95%98%EA%B8%B0
https://velog.io/@heetaeheo/DIDagger-hilt
https://developer88.tistory.com/349
https://rkdxowhd98.tistory.com/144
https://f2janyway.github.io/android/hilt/
https://3edc.tistory.com/67