[DI] Hilt

Minji Jeong·2022년 7월 14일
0

Android

목록 보기
27/39
post-thumbnail
Koin에 이어 다른 DI 프레임워크도 공부해보고 싶어서 Hilt를 공부하게 되었다. Koin하고 비슷하긴 하지만 좀 더 복잡하고, 확실히 공부하지 않으면 이해를 못할 것 같아서 이렇게 포스팅을 남기게 되었다. 참고로 웬만하면 Hilt 공식문서를 참고하는것이 정신건강에 이롭다. 안드로이드 공식문서는 업데이트가 느린건지 Hilt에서 deprecated 된 부분도 수정 안한 게 많이 보인다. 참고로 Hilt에 대해 이해하기 위해선 DI에 대한 개념을 어느정도 알고 있어야 한다(알고 있어도 조금 어렵다 🤣).
👉 관련 포스팅 : Dependency Injection

✅ Hilt

Hilt는 안드로이드를 위한 DI 프레임워크 중 하나다. 기존의 Dagger는 높은 러닝커브와 보일러 플레이트 코드, 안드로이드 생명주기를 고려하는 의존성 주입이 없다는 게 단점이었으나, Hilt가 이를 보완해 등장했다.

Why use Hilt?

  • 보일러 플레이트 코드 감소
  • 빌드 종속성 간 결합도 감소
  • Dagger보다 단순화된 구성
  • Dagger보다 쉬워진 테스트 방법
  • 표준화된 컴포넌트 계층

1. Hilt Set up

이 포스팅에서 Hilt 사용을 위한 초기 설정은 안드로이드 스튜디오 Chipmunk 버전, 코틀린을 기준으로 한다(안드로이드 스튜디오 버전에 따라 gradle 파일 구조가 조금씩 다르기 때문에 명시한다).

1.1 Build.gradle(Module) dependencies에 plugin & 종속 항목 추가

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'
}

1.2 Build.gralde(Project) dependencies에 classpath 추가

buildscript {
    dependencies {
        classpath 'com.google.dagger:hilt-android-gradle-plugin:2.42'
    }
}

2. 안드로이드 프로젝트에 Hilt 적용

안드로이드 프로젝트에 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는 다음의 안드로이드 컴포넌트들에 사용할 수 있다.

  • Acitivty
  • Fragment
  • View
  • Service
  • BroadcastReceiver
💡 참고로, 안드로이드 클래스에 @AndroidEntryPoint로 어노테이션을 지정하면 해당 클래스에 종속된 안드로이드 클래스에도 동일하게 지정해야 한다. 예를 들어 프래그먼트에 @AndroidEntryPoint를 지정했다면 이를 호스트하는 액티비티에도 @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 컴포넌트들은 각자의 생명 주기를 갖고 있고, 해당 모듈들은 이 컴포넌트의 생명주기(스코프)에 맞춰 그대로 따라간다.

💡 나는 ViewModel에 Repository를 주입해서 사용하고 싶었기 때문에 RepoModule의 스코프를 ViewModel로 지정했다(@InstallIn(ViewModelComponent::class)).

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

➕ Hilt with Compose

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'

➕ Hilt with Navigation Compose

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'

References

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

profile
Mobile Software Engineer

0개의 댓글