[Kotlin] Hilt
Hilt
Hilt 는 프로젝트의 모든 Android 클래스에 컨테이너를 제공하고 수명 주기를 자동으로 관리함으로써 애플리케이션에서 DI를 실행하는 표준 방법을 정의한다.
쉽게 얘기하면 자동으로 종속 항목을 삽입해주므로써 의존성 관리를 편리하게 해준다.
Gradle 설정
프로젝트 수준 Gradle
ext {
hilt_version = "2.38.1"
}
dependencies {
classpath "com.google.dagger:hilt-android-gradle-plugin:$hilt_version"
}
모듈 수준 Gradle
plugins {
id 'dagger.hilt.android.plugin'
id 'kotlin-kapt'
}
...
dependencies {
...
implementation "com.google.dagger:hilt-android:$hilt_version"
kapt "com.google.dagger:hilt-android-compiler:$hilt_version"
// compose에서 hilt
implementation "androidx.hilt:hilt-navigation-compose:1.0.0"
}
@HiltAndroidApp
Hilt를 사용하려면 가장 먼저 Application 클래스를 만들어 알려줘야 한다.
안드로이드의 시작점은 Apllication이다.
Application 클래스위에 @HiltAndroidApp을 쓰면 된다.
@HiltAndroidApp
class MainApplication: Application() {
}
Manifest.xml에 추가
activity 처럼 application도 만들었으면 시스템에서 알 수 있게 끔 Manifest 파일에 등록시켜 주자.
<application
...
android:name=".MainApplication">
<activity
android:name=".MainActivity"
...>
</activity>
</application>
자, 이제 Hilt를 사용할 준비가 되었다.
@AndroidEntryPoint
액티비티나 프라그먼트에 위에 붙여 Hilt가 쓰인다는 것을 알려준다.
즉, 객체가 주입되는 대상이라고 보면 된다.
MainActivity.kt
@AndroidEntryPoint
class MainActivity : ComponentActivity() {
@Inject
lateinit var ship: Ship
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContent {
ship.start()
}
}
}
우리는 분명 Ship 객체를 만들지 않았지만 어떻게 ship.start() 를 사용할 수 있을까?
자동 종속 항목 삽입이 주는 편리함을 Hilt가 하는 것이다.
@Inject 는 종속 항목을 나타내며 실행 하면 해당하는 클래스를 찾아서 힐트가 자동으로 객체를 만들어준다.
Ship.kt
class Ship @Inject constructor() {
fun start() = Log.d("daeYoung", "출항 시작")
}
@Inject constructor() 를 적어줘서 힐트에게 어딘가로부터 사용된다는 것을 알려준다.
즉, @Inject constructor() 는 보낼준비가 된 종속 항목이다.
@Inject 는 받을준비가 된 종속 항목이다.
❗️원래 코틀린에서는 constructor()를 생략할 수 있지만 Hilt에서 @Inject 뒤에 오는 constructor() 는 생략이 불가능하다.
인터페이스에 힐트를 어떻게 적용하지?
때로는 객체를 생성자로 삽입할 수 없는 상황이 생긴다. 대표적으로 예로 인터페이스로 생성자 삽입이 불가능 하고 Retrofit과 같은 외부 라이브러리 또한 생성자 삽입할 수 없다. 밑에 코드를 보고 이해를 해보자
MainActivity.kt
@AndroidEntryPoint
class MainActivity : ComponentActivity() {
@Inject
lateinit var ship: BattleShip
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContent {
ship.start()
}
}
}
Ship.kt
interface Ship {
fun start()
}
BattleShip.kt
class BattleShip @Inject constructor(): Ship {
override fun start() {
Log.d("daeYoung", "배틀 쉽 시작")
}
}
실행 시켰을 때 BattleShip 객체가 성공적으로 만들어진다. 그러나 MainActivity.kt 를 조금만 바꿔보자
@AndroidEntryPoint
class MainActivity : ComponentActivity() {
@Inject
lateinit var ship: Ship // 변경된 코드
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContent {
ship.start()
}
}
}
빌드에서 부터 오류가 뜬다, 힐트가 BattleShip 과 ContainerShip 중에 어떤 객체를 생성해야 할지 모르기 떄문다.
이 부분을 해결하기 위해 @Module 를 사용한다.
@Module, @InstallIn, @Qualifier
일단 기존의 Ship.kt, BattleShip.kt, ContainerShip.kt 는 유지하고
새로운 ShipModule.kt, BattleShipQualifier.kt, ContainerShipQualifier.kt 를 만든다.
ShipModule.kt
@Module
@InstallIn(ActivityComponent::class)
abstract class ShipModule {
@BattleShipQualifier
@ActivityScoped
@Binds // 연결한다 라는 뜻
abstract fun BattleShipImpl(battleShip: BattleShip): Ship // 매개변수는 구현하고자 하는 하위 클래스, 함수의 반환타입은 상위 인터페이스 타입
@ContainerShipQualifier
@ActivityScoped
@Binds
abstract fun ContainerShipImpl(containerShip: ContainerShip): Ship
}
@Qualifier
annotation class BattleShipQualifier
@Qualifier
annotation class ContainerShipQualifier
@Module 은 말 그대로 모듈화 한다는 뜻이고, @InstallIn() 은 Hilt의 표준 컴포넌트들 중 어떤 컴포넌트에 모듈을 설치할지를 결정한다. 컴포넌트는 여러종류가 있으니 참고하길 바란다.
@Binds 는 연결한다는 의미로 구체화된 하위 클래스로 연결시켜준다.
@Binds 를 사용하기 위해서는 모듈은 abstract class, 함수는 abstract function 이여야 한다.
Qualifier 는 Ship이 BattleShip 인지 ContainerShip 인지 힐트가 구분할 수 있게 도와주는 역할이다.
@BattleShipQualifier 과 @ContainerShipQualifier 을 사용함으로써 구분하게 된다. 밑의 MainActicity.kt 코드를 보자
@AndroidEntryPoint
class MainActivity : ComponentActivity() {
@ContainerShipQualifier // 추가된 코드, @BattleShipQualifier 사용시 BattleShip 객체 불러옴
@Inject lateinit var ship: Ship
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContent {
ship.start()
}
}
}
@ContainerShipQualifier 를 사용해서 성공적으로 ContainerShip 객체가 생성되는 것을 볼 수 있다.
외부 라이브러리에 힐트를 어떻게 적용하지?
외부 라이브러리에는 @Provides 를 사용한다. 그렇다면 언제 사용해야 할까?
- Retrofit, OkHttpClient, RoomDB 와 같은 외부 라이브러리
- bulid 패턴으로 인스턴스를 생성해야하는 경우
에 사용한다.
@Provides 는 다음과 같은 규칙을 따라야 한다.
- 함수 반환 유형은 함수가 어떤 유형의 인스턴스를 제공하는지 Hilt에 알려줍니다.
- 함수 매개변수는 해당 유형의 종속 항목을 Hilt에 알려줍니다.
- 함수 본문은 해당 유형의 인스턴스를 제공하는 방법을 Hilt에 알려줍니다. Hilt는 해당 유형의 인스턴스를 제공해야 할 때마다 함수 본문을 실행합니다.
RetrofitModule.kt
@Module
@InstallIn(SingletonComponent::class)
object RetrofitModule {
private const val URL = "http:/192.168.0.16:8080"
@Singleton
@Provides
fun getRetrofitImpl(): ApiService {
return Retrofit.Builder()
.baseUrl(URL)
.addConverterFactory(GsonConverterFactory.create())
.build()
.create(ApiService::class.java)
}
}
ViewModel에서 Hilt사용
@HiltViewModel 을 사용해서 해당 컴포넌트가 ViewModel 임을 알려준다.
@HiltViewModel
class HiltViewModel @Inject constructor(private val repository: HiltRepository): ViewModel() {
...
}
Hilt에서 Context 사용
애플리케이션에서 Context가 필요하면 @ApplicationContext 또는 @ActivityContext 를 이용한다.
Hilt 구성요소의 인젝터 대상
Hilt 구성요소(컴포넌트)에 생성된 모듈에는 인젝터 대상이 있다.
Hilt 구성요소의 수명
Hilt는 해당 Android 클래스의 수명 주기에 따라 생성된 구성요소 클래스의 인스턴스를 자동으로 만들고 제거한다.
Hilt 구성요소 범위
기본적으로 Hilt의 모든 결합은 범위가 지정되지 않는다. 즉, 앱이 종속항목을 자동으로 요청할 때 마다 Hilt는 필요한 유형의 새 인스턴스를 생성한다.
위의 코드를 보며 예를 들어보겠다. ShipModule 종속항목이 필요할 때마다 새로운 인스턴스를 새로 만들겠다는 것이다.
그러나 Hilt는 범위를 지정해서 해당 범위에 안에 속해있다면 동일한 인스턴스로 공유한다,
예를 들어 ShipModule 이 ActivityComponent 에 생성된 모듈이라고 하자 그리고 HiltActivity.kt 라는 액티비티에서 ShipModule 의 인스턴스를 요청하는 상황이라고 하자. 그러면 ShipModule 은 HiltActivity.kt 의 onCreate() 에 생성되고 onDestroy() 에 소멸된다. 다른 MainActivity.kt 에서 ShipModule 의 인스턴스를 필요로 할 시 새로운 인스턴스를 만들어 해당 lifecycle동안 살아있는것이다.
SingletonComponent 에 모듈을 생성하면 앱이 처음 시작할 때부터 종료할 때까지 ShipModule 인스턴스는 하나만 생길 것이다.
즉, 메모리 절약관련해서 범위를 효율적으로 지정해야한다.
제공된 객체는 구성요소가 제거될 때까지 메모리에 남아 있기 때문에 결합의 범위를 그 구성요소로 지정하면 많은 비용이 들 수 있습니다. 따라서 애플리케이션에서 범위가 지정된 결합의 사용을 최소화하세요.
Reference
안드로이드 공식 홈페이지
https://software-creator.tistory.com/35
https://velog.io/@cksgodl/AndroidKotlin-Hilt%EA%B0%80-%EB%AD%90%EC%98%88%EC%9A%94
https://hanyeop.tistory.com/220