2024.06.27 - [어질어질 개발노트/Android] - [Android] Hilt 적용하기(2) - ViewModel
의존성 주입(Dependancy Injection, DI)란?
객체를 클래스 내부에서 생성하지 않고 외부에서 생성한 후 주입해 주는 것을 말합니다.
장점 | 단점 |
|
|
DI 미사용 코드
class Car {
private val engine = Engine()
fun start() {
engine.start()
}
}
DI 사용 코드
class Car(private val engine: Engine){
fun start() {
engine.start()
}
}
Dagger2가 있지만 Hilt를 사용하는 이유
Hilt는 Dagger DI 라이브러리를 기반으로 빌드되어 Android 애플리케이션에 통합하는 표준 방법을 제공합니다. Dagger 또한 DI 라이브러리지만, 아래와 같은 코드를 자동으로 생성함으로써 많은 양의 Boilerplate Code를 줄여주기 때문에 Android 애플리케이션에선 Hilt를 사용하기를 권장하고 있습니다.
- Android 프레임워크 클래스를 통합하기 위한 구성요소
- Hilt가 자동으로 생성하는 구성요소와 함께 사용할 Scope Annotation
Application
또는Activity
와 같은 Android 클래스를 나타내는 사전 정의된 Binding@ApplicationContext
및@ActivityContext
를 나타내는 사전 정의된 Qualifier
Android Project에 Hilt 적용하기
Dependancy 추가
hilt-android-gradle-plugin 플러그인을 프로젝트의 루트(project level) build.gradle 파일에 추가
plugins {
...
id 'com.google.dagger.hilt.android' version '2.44' apply false
}
App level build.gradle
...
plugins {
id 'kotlin-kapt'
id 'com.google.dagger.hilt.android'
}
android {
...
}
dependencies {
implementation "com.google.dagger:hilt-android:2.44"
kapt "com.google.dagger:hilt-compiler:2.44"
}
// Allow references to generated code
kapt {
correctErrorTypes true
}
Hilt는 JAVA8을 사용하므로, project에서 java8을 사용한다고 아래와 같이 추가해 줍니다.
android {
...
compileOptions {
sourceCompatibility JavaVersion.VERSION_1_8
targetCompatibility JavaVersion.VERSION_1_8
}
}
Hilt Application Class
Hilt를 사용하는 모든 앱은 `@HiltAndroidApp` 으로 주석이 지정된 `Application` 클래스를 포함해야 합니다.
`@HiltAndroidApp`은 애플리케이션 수준 Dependancy Container 역할을 하는 애플리케이션의 기본 클래스를 비롯하여 Hilt의 코드 생성을 트리거합니다.
생성된 Hilt 구성요소는 `Application` 객체의 LifeCycle에 연결되며 이와 관련한 Dependancy를 제공합니다. 앱의 상위 구성요소이므로 다른 구성요소는 이 상위 구성요소에서 제공하는 Dependancy에 액세스 할 수 있습니다.
@HiltAndroidApp
class SampleApplication : Application() {
...
}
AndroidManifest.xml에도 Application 추가해야 합니다.
<application
android:name=".SampleApplication"
Android 클래스에 Dependancy 삽입
Hilt는 `@AndroidEntryPoint` Annotation이 있는 다른 클래스에 Dependancy를 제공할 수 있습니다.
@AndroidEntryPoint
class SimpleActivity : AppCompatActivity() { ... }
지원하는 Android Class
- `Application` (`@HiltAndroidApp`을 사용하여)
- `ViewModel` (`@HiltViewModel`을 사용하여)
- `Activity`
- `Fragment`
- `View`
- `Service`
- `BroadcastReceiver`
`@AndroidEntryPoint`가 할당된 Class를 사용하는 모든 Class에도 Annotation을 지정해야 합니다.
Hilt 결합 정의
필드 삽입을 실행하려면 Hilt가 해당 구성요소에서 필요한 종속 항목의 인스턴스를 제공하는 방법을 알아야 합니다. Binding은 특정 유형의 인스턴스를 dependancy로 제공하는 데 필요한 정보가 포함됩니다.
Hilt에 binding 정보를 제공하는 한 가지 방법은 생성자 삽입입니다. 다음과 같이 클래스의 생성자에서 `@Inject` Annotation을 사용하여 클래스의 인스턴스를 제공하는 방법을 Hilt에 알려줍니다.
구성요소에서 종속항목을 가져올 경우
`@Inject` Annotation을 사용하여 필드 삽입 실행
@AndroidEntryPoint
class SampleActivity : AppCompatActivity() {
@Inject lateinit var localSourceRepository: LocalSourceRepository
...
}
클래스의 인스턴스를 제공하는 방법
`@Inject` Annotation을 사용하여 Hilt에 알려주어 생성자 삽입 실행
class LocalSourceRepository @Inject constructor(
private val service: LocalService
) { ... }
Annotation이 지정된 클래스 생성자의 매개변수 `service`는 클래스의 dependancy 입니다. 따라서, Hilt는 LocalService 클래스의 인스턴스를 제공하는 방법도 알아야 합니다.
Hilt Module
생성자 삽입이 불가능한 상황에 Module을 적용합니다.
생성자 삽입이 불가능한 경우,
- 인터페이스 → `@Binds` 사용
- 외부 라이브러리의 클래스 → `@Provides` 사용
- 빌더 패턴 → `@Provides` 사용
Hilt Module은 `@Module` Annotation이 지정된 클래스 입니다. 또한, Hilt에서는 Dagger와 달리 `@InstallIn` Annotation을 지정하여 각 모듈을 사용하거나 설치할 Android 클래스를 Hilt에 알려야 합니다.
`@InstallIn` Annotation에는 해당 모듈이 어떤 Component에 사용가능한지를 선언하는 것입니다. 아래 예시는 SingletonComponent에서 사용가능하다고 선언하였습니다. Component 참고
예시코드
@Module
@InstallIn(SingletonComponent::class)
object LocalServiceModule { ... }
** Hilt 모듈에 제공하는 dependancy는 Hilt 모듈을 설치하는 Android클래스와 연결되어 있는 모든 생성된 구성요소에서 사용할 수 있습니다.
@Binds를 사용하여 인터페이스 인스턴스 삽입
`LocalService`가 인터페이스라면 이 인터페이스를 생성자 삽입할 수 없습니다. 대신 Hilt 모듈 내에 `@Binds`로 Annotation이 지정된 추상 함수를 생성하여 Hilt에 Bind 정보를 제공합니다.
`@Bind` 는 인터페이스의 인스턴스를 제공해야 할 때 사용할 구현을 Hilt에 알려줍니다.
interface LocalService {
fun createMethod()
}
class LocalServiceImpl @Inject constructor(
...
) : LocalService { ... }
@Module
@InstallIn(LocalService::class)
abstract class LocalServiceModule {
@Binds
abstract fun bindLocalService(localServiceImpl: LocalServiceImpl): LocalService
}
@Provides를 사용한 인스턴스 삽입
클래스가 외부 라이브러리에서 제공되므로 클래스를 소유하지 않은 경우 또한 빌더 패턴으로 인스턴스를 생성해야 하는 경우에도 생성자 삽입이 불가능합니다.
** 외부 라이브러리에서 제공되는 클래스 예시
: Retrofit, OkHttpClient, RoomDB와 같은 클래스
`@Provides` Annotation이 지정된 함수는 다음과 같은 정보를 Hilt에게 제공합니다.
- 함수 반환 타입은 함수가 어떤 타입의 인스턴스를 제공하는지 Hilt에게 알림
- 함수 파라미터는 해당 타입의 종속 항목을 Hilt에 알림
- 함수 본문은 해당 타입의 인스턴스를 제공하는 방법을 Hilt에 알림. Hilt는 해당 타입의 인스턴스를 제공해야 할 때마다 함수 본문을 실행
@Module
@InstallIn(SingletonComponent::class)
object LocalServiceModule {
@Provides
fun provideLocalService(
// Potential dependencies of this type
): LocalService {
return Retrofit.Builder()
.baseUrl("https://example.com")
.build()
.create(LocalService::class.java)
}
}
동일한 유형에 대해 여러 결합 제공
dependancy와 동일한 타입의 다양한 구현을 제공하는 Hilt가 필요한 경우, Hilt에 여러 결합을 제공해야 합니다.
이때 한정자 `@Qualifier`를 사용하여 동일한 유형에 대해 여러 결합을 정의할 수 있습니다.
`@Qualifier`는 특정 유형에 대해 여러 결합이 정의되어 있을 때 그 유형의 특정 결합을 식별하는데 사용하는 Annotation입니다.
예를 들어, 만약 Retrofit 인스턴스가 필요하되 각 요청마다 다른 intercepter를 달아야 한다면 이경우에는 서로 다른 두가지 OkHttpClient 구현을 제공하는 방법을 Hilt에게 알려야 합니다.
@Qualifier
@Retention(AnnotationRetention.BINARY)
annotation class AuthInterceptorOkHttpClient
@Qualifier
@Retention(AnnotationRetention.BINARY)
annotation class OtherInterceptorOkHttpClient
아래와 같이 두 메서드 모두 동일한 반환 유형을 갖지만, `@Qualifier` 두 가지의 서로 다른 결합을 제공하고 해당 `@Provides` 메서드에 Annotation을 지정합니다.
@Module
@InstallIn(SingletonComponent::class)
object LocalServiceModule {
@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()
}
}
다음과 같이 필드 또는 매개변수에 해당 `@Qualifier`로 Annotation을 지정하여 필요한 특정 유형을 삽입할 수 있습니다.
@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
}
사전정의된 Qualifier
Hilt는 Application 또는 Activity의 Context 클래스가 필요할 수 있으므로 `@ApplicatonContext` 및 `@ActivityContext` Qualifier를 제공합니다. 또 다른 사전정의된 Qualifier
class DataAdapter @Inject constructor(
@ActivityContext private val context: Context,
private val service: LocalService
) { ... }
Android 클래스용으로 생성된 구성요소
필드 삽입을 실행할 수 있는 각 Android 클래스마다 `@InstallIn` Annotation에 참조할 수 있는 관련 Hilt 구성요소가 있습니다. 각 Hilt 구성요소는 해당 Android 클래스에 Bindings를 주입해야 합니다.
구성요소 전체 기간
Hilt는 해당 Android 클래스의 수명 주기에 따라 생성된 구성요소 클래스의 인스턴스를 자동으로 만들고 제거합니다.
구성요소 Scope
기본적으로 Hilt의 모든 결합은 범위가 지정되지 않습니다. 즉, 앱이 결합을 요청할 때마다 Hilt는 필요한 유형의 새 인스턴스를 생성합니다. 그러나, 특정 구성 요소로 범위를 지정할 수 있습니다.
Hilt는 결합의 범위가 지정된 구성요소의 인스턴스마다 한 번만 범위가 지정된 결합을 생성하며 이 결합에 관한 모든 요청은 동일한 인스턴스를 공유합니다.
** 제공된 객체는 구성요소가 제거될 때까지 메모리에 남아 있기 때문에 결합의 범위를 그 구성요소로 지정하면 많은 비용이 들 수 있습니다. 따라서 Application에서 범위가 지정된 결합의 사용을 최소화해야 합니다. 특정 범위 내에서 동일한 인스턴스를 사용해야 하는 내부 상태가 있는 결합 또는 동기화가 필요한 결합, 만드는 데 비용이 많이 들 것으로 측정된 결합에는 구성요소 범위 지정 결합을 사용하는 것이 적절합니다.
구성요소 기본 결합
각 Hilt 구성요소는 Hilt가 고유한 맞춤 결합에 종속 항목으로 삽입할 수 있는 기본 결합 세트와 함께 제공됩니다. 이러한 결합은 일반 Activity 및 Fragment 유형에 해당하며 특정 서브클래스에는 해당되지 않습니다. 이는 Hilt가 모든 Activity를 삽입하는 데 단일 Activity 구성요소 정의를 사용하기 때문입니다. 각 Activity에는 이 구성요소의 다른 인스턴스가 있습니다.
Hilt가 지원하지 않는 클래스에 Dependancy Injection
Hilt가 지원하지 않는 클래스에 필드 삽입을 실행해야 할 수도 있습니다. 이러한 경우 `@AndroidEntryPoint` 대신 `@EntryPoint` Annotation을 사용하여 진입점을 만들 수 있습니다.
진입점은 Hilt가 관리하는 코드와 그렇지 않은 코드 사이의 경계입니다.
Hilt를 사용하여 일부 종속 항목을 가져오도록 하려면 원하는 결합 유형마다 `@EntryPoint`로 Annotation이 지정된 인터페이스를 정의하고 Qualifier를 포함해야 합니다. 그리고 다음과 같이 `@InstallIn`을 추가하여 진입점을 설치할 구성요소를 지정합니다.
class ExampleContentProvider : ContentProvider() {
@EntryPoint
@InstallIn(SingletonComponent::class)
interface ExampleContentProviderEntryPoint {
fun analyticsService(): AnalyticsService
}
...
}
참고사이트
https://velog.io/@dlwpdlf147/Android-%EC%9D%98%EC%A1%B4%EC%84%B1-%EC%A3%BC%EC%9E%85%EA%B3%BC-Hilt
https://velog.io/@tnalxmsk/Hilt
https://velog.io/@haanbink/Android-Hilt-%EC%82%AC%EC%9A%A9%ED%95%98%EA%B8%B0
'어질어질 개발노트 > Android' 카테고리의 다른 글
[Android] 안드로이드 개발에서 Private 함수 테스트: 실전 팁과 예제 (0) | 2025.01.15 |
---|---|
[Android] Abstract Class vs Interface: 무엇을 선택해야 할까? (0) | 2025.01.14 |
[Android] 클린 아키텍처 (Clean Architecture): 소프트웨어 설계의 정수 (0) | 2025.01.11 |
[Android] Sealed Class와 Enum의 차이점과 적절한 사용법 - Kotlin 개발자를 위한 가이드 (0) | 2025.01.10 |
[Android] ListAdapter vs RecyclerViewAdapter: 무엇을 선택해야 할까? (0) | 2025.01.10 |
[Android] RxJava: flatMap vs switchMap - 차이를 명확히 이해하기 (0) | 2025.01.10 |
[Android] [Kotlin] Compose 기초 - 시작하기 (0) | 2024.08.27 |
[Android] Hilt 적용하기(2) - ViewModel (0) | 2024.06.27 |