[Kotlin/Android] Hilt를 사용한 종속 항목 삽입
1. 소개
종속 항목 삽입(DI)은 프로그래밍에 널리 사용되는 기법으로, Android 개발에 적합합니다. DI의 원칙을 따르면 훌륭한 앱 아키텍처를 위한 토대를 마련할 수 있습니다. 종속 항목 삽입을 구현하면 다음과 같은 이점을 누릴 수 있습니다. - 코드 재사용 가능 - 리팩터링 편의성 - 테스트 편의성 |
p.s Spring을 해보았다면, 이해하기 훨씬 편하다.
2. 기본 셋팅
1) build.gradle(Project)
// Top-level build file where you can add configuration options common to all sub-projects/modules.
buildscript {
...
dependencies {
...
classpath 'com.google.dagger:hilt-android-gradle-plugin:2.38.1'
}
}
2) build.gradle(app)
plugins {
...
id 'kotlin-android'
id 'kotlin-kapt'
id 'dagger.hilt.android.plugin'
}
android {
...
compileOptions {
sourceCompatibility JavaVersion.VERSION_1_8
targetCompatibility JavaVersion.VERSION_1_8
}
...
}
dependencies {
...
// Hilt
implementation "com.google.dagger:hilt-android:2.38.1"
kapt "com.google.dagger:hilt-android-compiler:2.38.1"
}
3) Application Class 생성 후, @HiltAndroidApp 주석 추가(Hilt를 설정)
import android.app.Application
import dagger.hilt.android.HiltAndroidApp
@HiltAndroidApp
class Application : Application() {
...
}
4) Application Class를 AndroidManifest.xml 에 추가
...
<application
android:name=".Application"
...">
...
</application>
...
5) MainActivity 에 @AndroidEntryPoint 주석 추가 (종속항목 삽입)
import androidx.appcompat.app.AppCompatActivity
import android.os.Bundle
import dagger.hilt.android.AndroidEntryPoint
@AndroidEntryPoint
class MainActivity : AppCompatActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
}
}
3. Hilt 변수 결합하기
1) BobModel Class 를 생성한다. 중요한 부분은 생성자 옆에 @Inject 주석을 추가해서, Hilt 에게 알려야한다. Hilt가 알고 있는 정보를 결합이라고 한다.
import javax.inject.Inject
class BobModel @Inject constructor(){
var mesage : String = ""
}
2) MainActivity Class에 변수를 선언한다.
import androidx.appcompat.app.AppCompatActivity
import android.os.Bundle
import android.util.Log
import dagger.hilt.android.AndroidEntryPoint
import javax.inject.Inject
@AndroidEntryPoint
class MainActivity : AppCompatActivity() {
@Inject lateinit var bobModel: BobModel
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
// 이렇게 생성할 필요가 없다.
// bobModel = BobModel()
// Hilt가 알고 있기 때문에, 바로 사용 가능하다.
bobModel.mesage = "bob!!"
Log.d("sjlim/test", "${bobModel.mesage}")
}
}
3) 출력
D/sjlim/test: bob say : bob!!
4. 인스턴스 만들기 위한 @Singleto
애플리케이션 컨테이너에서 항상 같은 인스턴스를 제공한다.
getInstance() 패턴 형식이라고 이해하면 된다.
1) BobModel Class에 @Singleton 주석을 추가한다.
import javax.inject.Inject
import javax.inject.Singleton
@Singleton
class BobModel @Inject constructor(){
var mesage : String = ""
}
2) MainActivity Class를 수정한다.
import androidx.appcompat.app.AppCompatActivity
import android.os.Bundle
import android.util.Log
import dagger.hilt.android.AndroidEntryPoint
import javax.inject.Inject
@AndroidEntryPoint
class MainActivity : AppCompatActivity() {
@Inject lateinit var bobModel1: BobModel
@Inject lateinit var bobModel2: BobModel
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
bobModel1.mesage = "bob!!"
Log.d("sjlim/test", "bob1 say : ${bobModel1.mesage}")
// BobModel1 과 2는 같은 인스턴스를 참조하고 있으므로, 같은 메세지가 출력된다.
Log.d("sjlim/test", "bob2 say : ${bobModel2.mesage}")
}
}
3) 출력
D/sjlim/test: bob1 say : bob!!
D/sjlim/test: bob2 say : bob!!
5. interface를 사용하기 위한 @Module과 @InstallIn
추가적으로 @ActivityScoped @Binds 를 사용한다.
1) interface 를 생성한다.
interface BobStateListener {
fun happy(msg: String)
}
2) Interface 를 구현한 Impl 을 생성한다.
Hilt 가 알 수 있도록 생성자 옆에 @InstallIn 주석을 추가해야한다.
import android.app.Activity
import android.util.Log
import javax.inject.Inject
class BobStateListenerImpl @Inject constructor(private val activity: Activity) : BobStateListener{
override fun happy(msg: String) {
Log.d("sjlim/test", "$msg")
}
}
2-1) 위 소스에서 activity는 아래 소스에 있는 Activity 범위를 설정했기 때문에, activity 변수는 null이 아니라, 존재하는 값으로 들어온다.
3) interface를 이용하기 위해서는 추상화 클래스를 이용해야한다. 추상화 클래스를 생성 후, @Module과 @InstallIn을 추가한다.
import dagger.Binds
import dagger.Module
import dagger.hilt.InstallIn
import dagger.hilt.android.components.ActivityComponent
import dagger.hilt.android.scopes.ActivityScoped
@InstallIn(ActivityComponent::class)
@Module
abstract class BobModule {
@ActivityScoped
@Binds
abstract fun bindBob(impl: BobStateListenerImpl) : BobStateListener
}
3-1) @InstallIn ( Class ??? ) 안에 Class는 왜 입력했을까?
Installn 에 Class는 구성요소 범위와 표에 맞게 작성해주면 된다.
위 소스 기준으로 본다면, BobStateListener을 Activity에서 범위에서 사용한다고 생각했고, @ActivityScoped 주석을 입력했다. @ActivityScoped 을 사용하려면 Class 를 @InstallIn 에 입력해줘야하는데, 아래 표를 본다면 ActivityComponent와 맵핑이 되어 있어서, ActivityComponent::class를 입력해주었다.
Application | ApplicationComponent | @Singleton |
View Model | ActivityRetainedComponent | @ActivityRetainedScope |
Activity | ActivityComponent | @ActivityScoped |
Fragment | FragmentComponent | @FragmentScoped |
View | ViewComponent | @ViewScoped |
@WithFragmentBindings 주석이 지정된 View | ViewWithFragmentComponent | @ViewScoped |
Service | ServiceComponent | @ServiceScoped |
3-2) @Binds 는 왜 입력했을까?
인터페이스에 사용할 구현을 Hilt에 알리려면 Hilt 모듈 내 함수에 @Binds 주석을 사용하면 됩니다.
4) interface에서 사용할 impl을 구현한다.
import android.app.Activity
import android.util.Log
import javax.inject.Inject
class BobStateListenerImpl @Inject constructor(private val activity: Activity) : BobStateListener{
override fun happy(msg: String) {
Log.d("sjlim/test", "$msg")
}
}
5) 이제 MainActivity에서 아래와 같이 사용 가능하다.
import androidx.appcompat.app.AppCompatActivity
import android.os.Bundle
import dagger.hilt.android.AndroidEntryPoint
import javax.inject.Inject
@AndroidEntryPoint
class MainActivity : AppCompatActivity() {
@Inject lateinit var bobStateListener: BobStateListener
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
bobStateListener.happy("happy!!!")
}
}
6) 출력
D/sjlim/test: happy!!!
6. Module 안에 여러개의 추상화 class를 사용하려면?
1) BobModule.kt 를 아래와 같이 수정한다.
import dagger.Binds
import dagger.Module
import dagger.hilt.InstallIn
import dagger.hilt.android.components.ActivityComponent
import dagger.hilt.android.components.FragmentComponent
import dagger.hilt.android.scopes.ActivityScoped
import javax.inject.Qualifier
@Qualifier
annotation class activityModule
@Qualifier
annotation class fragmentModule
@InstallIn(ActivityComponent::class)
@Module
abstract class BobActivityModule {
@activityModule
@ActivityScoped
@Binds
abstract fun bindBob(impl: BobStateListenerImpl) : BobStateListener
}
@InstallIn(FragmentComponent::class)
@Module
abstract class BobFragmentModule {
@fragmentModule
@ActivityScoped
@Binds
abstract fun bindBob(impl: BobStateListenerImpl) : BobStateListener
}
2) 하나의 BobModule.kt 에 두개의 추상화 클래스를 만들었다.
BobActivityModule 은 Activity 범위로 만들었고, BobFragmentModule는 Fragment 범위로 만들었다.
3) @Qualifier 라인은 무엇인가?
위 소스를 보면, 각 bindBob 메소드에서 BobStateListener 을 반환하고 있는것을 알 수 있다.
Hilt 는 어느것을 사용해야할지 정확하게 판단하도록 명시를 해주기 위해서 네이밍 같은것을 해주는 것이다.
@Qualifier
annotation class activityModule
@Qualifier
annotation class fragmentModule
이렇게 선언한다면, @activityModule 주석을 사용할 수 있다. 그래서 소스 하단 부분에 BobActivityModule 에는
...
abstract class BobActivityModule {
@activityModule
...
abstract fun bindBob(impl: BobStateListenerImpl) : BobStateListener
이렇게 선언해줌으로써, 명확하게 Hilt가 알 수 있도록 한다.
4) @activityModule 과 @fragmentModule 주석을 생성했다면, MainActivity에서 아래와 같이 사용할 변수 위에 사용이 가능하다.
import androidx.appcompat.app.AppCompatActivity
import android.os.Bundle
import dagger.hilt.android.AndroidEntryPoint
import javax.inject.Inject
@AndroidEntryPoint
class MainActivity : AppCompatActivity() {
@activityModule
@Inject lateinit var bobStateListener: BobStateListener
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
bobStateListener.happy("happy!!!")
}
}
5) 만약 @fragmentModule 을 사용하면 어떻게 될까?
위 소스는 Activity 범위를 가지고 있기 때문에, Fragment 범위는 사용할 수 없기 때문에 오류가 발생한다.
7. Context 를 엑세스 하려면 어떻게 해야할까?
1) BobStateListenerImpl 파일에 @ApplicationContext 주석을 이용해서 사용할 수 있다.
import android.app.Activity
import android.content.Context
import android.util.Log
import dagger.hilt.android.qualifiers.ApplicationContext
import javax.inject.Inject
class BobStateListenerImpl @Inject constructor(private val activity: Activity, @ApplicationContext private val context: Context) : BobStateListener{
override fun happy(msg: String) {
Log.d("sjlim/test", "$msg")
}
}