이 글은 DI란 무엇인가에 대한 3번째 글로, Dagger에 대한 간단한 개념과 Hilt에 대한 개념을 설명하는 글입니다. Depedency와 Depedency Injection에 대한 개념은 DI란 무멋인가 - 1에 나와있고, 안드로이드에서 DI를 수동으로 적용하는 방법은 DI란 무엇인가 - 2에 나와있습니다. 만약 Depedency, Depedency Injection 등에 대한 개념에 대해 모르신다면 이전 글을 먼저 읽으시는 것을 추천드립니다.
Hilt는 안드로이드 DI(의존성 주입) 라이브러리입니다. Hilt는 유명한 DI 라이브러리인 Dagger를 기반으로 만들어졌습니다. Hilt는 수동으로 의존성을 주입할 경우 생겨났던 보일러 플레이트 코드들을 줄여주고 자동으로 의존성을 주입해줍니다.
참고로 안드로이드 많이 사용되는 DI 라이브러리는 Dagger, Koin, Hilt가 있습니다. 이 라이브러리들을 비교한 내용은 DI Fragmework 선택지에서 확인할 수 있습니다.
보일러 플레이트 코드
- 어떤 일을 하기 위해서 꼭 작성해야 하는 코드로 클래스의 getter, setter와 같은 메서드 코드를 의미합니다.
- 즉, 코드를 길어지게 하고 개발자에게 노동을 강요하는 코드라고 생각하면 됩니다.
Hilt의 목점과 사용 이점은 아래와 같습니다.
Hilt는 Dagger를 기반으로 만든 라이브러리이기에 Hilt와 관련된 글들을 보면 용어가 Dagger와 많이 겹치는 것 같습니다. 따라서 Hilt에 대해 알아보기 전에 Dagger의 5가지 필수 개념을 알아보겠습니다.
Inject
의존성 주입을 요청합니다. Inject 어노테이션으로 주입을 요청하면 연결된 Component가 Module로부터 객체를 생성하여 넘겨줍니다.
Component
연결된 Module을 이용하여 의존성 객체를 생성하고, Inject로 요청받은 인스턴스에 생성한 객체를 주입합니다. 의존성을 요청받고 주입하는 Dagger의 주된 역할을 수행합니다.
Subcomponent
Component는 계층관계를 만들 수 있습니다. Subcomponent는 Inner Class 방식의 하위계층 Component 입니다. Sub의 Sub도 가능합니다. Subcomponent는 Dagger의 중요한 컨셉인 그래프를 형성합니다. Inject로 주입을 요청받으면 Subcomponent에서 먼저 의존성을 검색하고, 없으면 부모로 올라가면서 검색합니다.
Module
Component에 연결되어 의존성 객체를 생성합니다. 생성 후 Scope에 따라 관리도 합니다.
Scope
생성된 객체의 Lifecycle 범위입니다. 안드로이드에서 주로 PerActivity, PerFragment 등으로 화면의 생명주기와 맞추어 사용합니다. Module에서 Scope을 보고 객체를 관리합니다.
Dagger에 대한 기본적인 개념은 Dagger2 시작하기 글에서 참고한 부분입니다.
Hilt를 사용하기 위해서는 아래와 같이 의존성을 설정해야 합니다.
buildscript {
...
dependencies {
...
classpath 'com.google.dagger:hilt-android-gradle-plugin:2.38.1'
}
}
plugins {
id 'kotlin-kapt'
id 'dagger.hilt.android.plugin'
}
android {
...
}
dependencies {
implementation "com.google.dagger:hilt-android:2.38.1"
kapt "com.google.dagger:hilt-compiler:2.38.1"
}
이제 Hilt 라이브러리의 주요 어노테이션의 개념과 사용법을 예제와 함께 알아보도록 하겠습니다. 예제에서 사용된 코드는 Codelab - Android Hilt에 나와있는 소스코드를 사용하였습니다.
들어가기전에 간단하게 용어를 정리하겠습니다. Hilt에는 Dagger와 마찬가지로 컴포넌트
라는 개념이 있는데 이는 안드로이드 컴포넌트
와는 다른 개념이며 컨테이너
역할을 수행하는 클래스라고 생각하면 됩니다. 안드로이드 컴포넌트는 안드로이드 클래스
라고 해당 글에서 작성하였습니다. 참고로 컨테이너
는 종속 항목들을 가지고 이를 필요로 하는 곳에 주입해주는 클래스로 이에 대한 자세한 내용은 'DI란 무엇인가 - 2' 글에 나와있습니다.
따라서 컴포넌트
는 컨테이너이므로 각 컴포넌트 안에 있는 타입들(객체들)을 요청하는 곳(안드로이드 클래스)으로 주입할 수 있는 집합 장소라고 생각하면 됩니다.
@HiltAndroidApp
어노테이션은 Hilt 라이브러리를 사용하는 앱이라면 무조건 선언해야 하는 어노테이션으로, Application
안드로이드 클래스에 선언해야합니다. 최상위 컴포넌트(컨테이너)에 해당하며 Hilt 코드를 생성시켜줍니다.
/** 생명주기가 app에 해당하는 컴포넌트(컨테이너) 생성
*
* 의존성 주입을 사용할 수 있는 앱의 기본 클래스들(액티비티, 프래그먼트 등..)을
* 포함한 Hilt 코드 생성을 발생(트리거)
*
* 최상위 컨테이너로 다른 컨테이너들은 해당 컨테이너가 제공하는 종속 항목에 접근할 수 있음
* 즉, 계층적 접근이 가능하다
*/
@HiltAndroidApp
class LogApplication : Application() {
override fun onCreate() {
super.onCreate()
}
}
해당 어노테이션이 붙은 안드로이드 클래스는 해당 클래스의 생명주기를 따르는 컴포넌트(컨테이너)를 만듭니다. @AndroidEntryPoint
를 사용할 수 있는 안드로이드 클래스는 아래와 같습니다.
만약 안드로이드 클래스에 @AndroidEntryPoint
어노테이션을 달았다면, 이 클래스에 의존하는 안드로이드 클래스에도 어노테이션을 달아야 합니다. 즉, Fragment에 어노테이션을 달았다면 Fragment를 호스팅하는 Activity 클래스에도 반드시 어노테이션을 달아야 합니다.
예제 앱은 하나의 액티비티(MainActivity)와 두 개의 프래그먼트(ButtonsFragment, LogsFragment)로 구성된 싱글 액티비티 구조입니다. 두 개의 프래그먼트 모두 의존성을 주입받고 있어서 @AndroidEntryPoint
어노테이션을 달았습니다. 따라서 이를 호스팅하는 액티비티에도 @AndroidEntryPoint
어노테이션을 아래와 같이 달아줍니다.
// MainActivity 가 호스트하고 있는 LogsFragment, ButtonsFragment 가
// Hilt Component 이기에 MainActivity 또한 Component 로 설정
// 액티비티의 생명주기에 해당하는 의존성 컨테이너(컴포넌트) 생성
@AndroidEntryPoint
class MainActivity : AppCompatActivity() {
...
}
// 프래그먼트 생명주기에 해당하는 의존성 컨테이너(컴포넌트)를 생성
@AndroidEntryPoint
class LogsFragment : Fragment() {
...
}
// 프래그먼트 생명주기에 해당하는 의존성 컨테이너(컴포넌트)를 생성
@AndroidEntryPoint
class ButtonsFragment : Fragment() {
...
}
@AndroidEntryPoint
, @HiltViewModel
과 같은 어노테이션으로 생성된 컴포넌트는 연결된 안드로이드 클래스의 생명주기를 따른다고 위에서 언급하였습니다. 즉, 각 컴포넌트들은 해당 안드로이드 클래스의 생성 시점부터 파괴되기 전까지 의존성 주입이 가능합니다. 아래는 생성되는 컴포넌트와 생명주기를 나타내는 표입니다.
만약 Activity에 @AndroidEntryPoint
어노테이션을 설정하여 액티비티 컴포넌트를 생성하였다고 가정하면 이 컴포넌트는 Activity가 생성되는(onCreate()
) 시점에 함께 생성되고, Activity가 파괴되는(onDestroy()
) 시점에 함께 파괴됩니다. 안드로이드 클래스와 생성되는 컴포넌트의 생명주기에 대한 내용은 Dagger Hilt로 안드로이드 의존성 주입 시작하기에 자세히 나와있습니다.
생성된 컴포넌트들은 계층 구조를 가지고 있습니다. 이에 따라, 하위 컴포넌트는 상위 컴포넌트에서 제공하는 종속 항목을 제공받을 수 있습니다. 즉, Car라는 클래스를 액티비티 컴포넌트에서 제공한다면, 그 하위 컴포넌트인 프래그먼트에서 이를 제공받을 수 있습니다.
참고로 SingletonComponent
와 ActivityComponent
에서는 기본적으로 제공하는 타입이 있습니다. SingletonComponent
는 @ApplicationContext
를 제공하고 ActivityComponent
는 @ActivityContext
를 제공합니다. 만약 Context가 필요하다고 하면 이를 사용해서 접근할 수 있습니다.
의존성을 주입하기 위해서는 @Inject
어노테이션을 사용해야 합니다. 이 어노테이션을 필드에 설정하면 Field injection
이 되고, 이 어노테이션을 생성자에 설정하면 Constructor injection
이 됩니다.
필드 주입을 수행하면 Hilt가 자동으로 의존성을 주입해줍니다. 다만 해당 필드에 접근자로 private은 사용할 수 없습니다. 다음은 LogsFragment에서 DateFormatter 인스턴스를 주입받는 코드입니다.
// 프래그먼트 생명주기에 해당하는 의존성 컨테이너(컴포넌트)를 생성
@AndroidEntryPoint
class LogsFragment : Fragment() {
// @Inject 어노테이션을 사용하여 의존성 주입
// 해당 어노테이션을 사용하면 Hilt가 자동으로 의존성을 주입해준다.
@Inject
lateinit var dateFormatter: DateFormatter
}
필드 주입을 선언해서 Hilt가 자동으로 해당 타입의 객체를 주입(생성)해준다고 해도 해당 타입을 제공하는 방법을 Hilt는 알고 있어야 합니다. Constructor injection
을 통해서 Hilt에게 이 방법을 알려줄 수 있습니다.
생성자 주입은 두 가지의 역할이 있습니다. 첫 번째는 위에서 언급하였듯이 Hilt에게 해당 타입을 제공하는 방법을 알려주는 역할을 수행합니다. 따라서 위의 코드에서 필드 주입을 통해 주입하려는 DateFormatter 타입은 아래와 같이 코드를 작성하면 됩니다.
// Hilt가 주입하기를 원하는 클래스의 생성자에 @Inject 어노테이션을 달면
// Hilt가 해당 타입을 주입하는 방법을 알게되고
// 자동으로 해당 타입을 필요로 하는 곳에(의존하는 곳에) Hilt가 주입해준다.
class DateFormatter @Inject constructor() {
@SuppressLint("SimpleDateFormat")
private val formatter = SimpleDateFormat("d MMM yyyy HH:mm:ss")
fun formatDate(timestamp: Long): String {
return formatter.format(Date(timestamp))
}
}
생성자 주입의 두 번째 역할은 필드 주입과 마찬가지로 종속 항목을 주입받도록 Hilt에게 요청합니다. 생성자 안에 타입을 선언한다면 Hilt는 이를 종속 항목으로 파악하고 해당 타입을 자동으로 주입해줍니다.
즉, 정리하자면 현재 클래스를 종속 항목으로 주입하거나, 현재 클래스에서 종속 항목을 주입받으려면 생성자 주입을 사용하면 됩니다. 다만, 인터페이스를 구현하는 클래스나 외부 라이브러리(Room, Retrofit) 객체들은 @Inject
어노테이션을 사용해서 주입할 수 없습니다. 이들은 다른 어노테이션을 사용해야하는데 아래에서 자세히 알아보도록 하겠습니다.
Hilt에는 주입되는 객체의 scope(범위)를 지정하는 어노테이션이 있습니다. 이 어노테이션을 사용하는 이유는 단순합니다. 객체의 범위를 scoped 어노테이션을 통해 연결된 컴포넌트로 지정하는 것입니다. 이 어노테이션을 사용하면 해당 타입은 scoped 어노테이션으로 지정된 컴포넌트(컨테이너)에서 모두 같은 인스턴스로 제공됩니다. 그림을 통해 쉽게 알아보도록 하겠습니다.
기본적으로 scope 어노테이션을 선언하지 않는다면 unscoped(default) 입니다. 기본적으로 선언하지 않으면, 첫 번째 사진처럼 의존성 객체가 요청되어 주입될 때마다 새로운 객체가 주입됩니다. 반면, 만약 @Singleton
어노테이션을 사용해서 해당 타입의 Scope를 SingletonComponent
로 설정한다면 앱 내내 같은 객체가 의존성으로 주입됩니다. 하지만 scope 어노테이션은 비용이 꽤나 들기에 사용을 최소한으로 해야합니다.
// Singleton Scope를 사용하여 앱이 실행되어 있는 동안에 같은 LoggerLocalDataSource 객체가 전달
// 생성자 주입을 통해 해당 클래스를 제공하는 방법을 Hitl에게 알리고
// 파라미터로 선언된 LogDao를 종속 항목으로 Hilt에게 제공받는다.
@Singleton
class LoggerLocalDataSource @Inject constructor(
private val logDao: LogDao
)
이미 언급했지만 Hilt에서는 인터페이스나 외부 라이브러리 클래스는 생성자 삽입
을 사용할 수 없습니다. 즉, 클래스의 생성자에 @Inject
어노테이션을 설정해서 해당 타입을 종속 항목으로 제공하는 방법을 Hilt 알려줄 수가 없습니다. 이를 위해서 @Module
이라는 어노테이션이 존재합니다.
@Module
어노테이션을 사용하면 Hilt 모듈이 만들어지고 이 모듈은 인터페이스나 외부 라이브러리의 클래스와 같이 생성자 삽입을 사용할 수 없는 타입을 Hilt에서 주입할 수 있게 만들어줍니다. @Module
어노테이션은 @InstallIn
어노테이션과 무조건 함께 쓰이는데, 이 어노테이션은 어떤 Hilt 컴포넌트에서 종속 항목을 주입해줄지 지정하는 역할을 수행합니다. 지정할 수 있는 컴포넌트로는 안드로이드 클래스 - 컴포넌트 - scope 어노테이션
에서 보았던 것들이 있습니다.
예제 코드에서는 Room 라이브러리를 사용해서 데이터베이스에 로그를 저장합니다. 그런데 Room 라이브러리는 외부 라이브러리이기에 생성자 삽입을 사용해서 의존성을 주입할 수 없습니다. 따라서 Hilt Module을 만들어서 의존성을 제공해야 하는데 코드를 보도록 하겠습니다.
/*
LogDao를 종속 항목으로 요청하는 LoggerLocalDataSource 클래스가
application container로 scoped를 지정했기에(@Singleton)
LogDao을 제공하는 Module의 컨테이너를 SingletonComponent로 지정
@Provides 어노테이션을 사용하는 모듈은 object 키워드를 사용해서 객체 선언
*/
@Module
@InstallIn(SingletonComponent::class)
object DatabaseModule {
// @Provides 어노테이션을 사용하여 Hilt에 생성자 삽입을 할 수 없는
// 타입을 제공하는 방법을 알려줌(RoomDatabase는 외부 라이브러리이므로 생성자 삽입 불가능)
// Hilt에서 항상 동일한 데이터베이스를 제공하기 위해서 @Singleton 어노테이션 사용
@Provides
@Singleton
fun provideDatabase(@ApplicationContext appContext: Context): AppDatabase {
return Room.databaseBuilder(
appContext,
AppDatabase::class.java,
"logging.db"
).build()
}
// @Provides 어노테이션을 사용하여 Hilt에 생성자 삽입을 할 수 없는 타입을
// 제공하는 방법을 알려줌(LogDao는 인터페이스이므로 생성자 삽입 불가능)
// 리턴타입은 제공하려는 타입(바인딩 타입)
// 파라미터는 종속항목
@Provides
fun provideLogDao(database: AppDatabase): LogDao {
return database.logDao()
}
}
@Module
과 @InstallIn
어노테이션을 사용해서 모듈을 생성하였습니다. 모듈의 안에는 메서드를 통해서 인터페이스와 외부 라이브러리 클래스들을 종속 항목으로 제공하고 있는데 메서드에 @Provides
라는 어노테이션이 있습니다. 해당 어노테이션을 설정하면 생성자 삽입을 사용할 수 없는 타입을 제공하는 방법을 Hilt에게 알려줄 수 있습니다. 함수의 매개변수는 해당 타입의 종속 항목이고 반환 타입이 인터페이스 또는 외부 라이브러리 클래스에 해당합니다.
위의 코드에서는 Room 데이터베이스 클래스와 LogDao 인터페이스를 @Provides
어노테이션을 사용해서 종속 항목으로 지정하였습니다. 참고로 Room Database에 @Singleton
어노테이션을 지정한 이유는 Room 데이터베이스 객체는 초기화에 많은 비용이 들기에, 초기화를 한번만 진행하고 그 이후에는 항상 같은 객체를 제공하도록 설정하기 위한 것입니다.
@Provides
어노테이션은 인터페이스와 외부 라이브러리를 제공할 때 사용할 수 있고, 단지 인터페이스만을 제공하려고 한다면 @Binds
어노테이션을 사용할 수 있습니다.
해당 어노테이션 또한 Hilt 모듈에서 사용할 수 있는 어노테이션이므로 우선 @Module
과 @InstallIn
어노테이션을 사용해서 Hilt 모듈을 생성해야 합니다. 다만 @Binds
어노테이션은 추상 메서드를 사용해서 인터페이스를 제공하므로, 클래스 또한 추상 클래스로 선언해야 합니다.
우선 인터페이스와 인터페이스를 구현하는 클래스를 선언하고 @Binds
어노테이션을 사용하는 방법을 알아보도록 하겠습니다.
interface AppNavigator {
// Navigate to a given screen.
fun navigateTo(screen: Screens)
}
// 해당 클래스는 생성자 삽입을 사용할 수 있기에
// @Inject 어노테이션 사용
class AppNavigatorImpl @Inject constructor(
private val activity: FragmentActivity
) : AppNavigator {
override fun navigateTo(screen: Screens) {
val fragment = when (screen) {
Screens.BUTTONS -> ButtonsFragment()
Screens.LOGS -> LogsFragment()
}
activity.supportFragmentManager.beginTransaction()
.replace(R.id.main_container, fragment)
.addToBackStack(fragment::class.java.canonicalName)
.commit()
}
}
// MainActivity.kt
@AndroidEntryPoint
class MainActivity : AppCompatActivity() {
// 필드 주입을 통해 AppNavigator 타입의 종속 항목 요청
@Inject
lateinit var navigator: AppNavigator
...
}
AppNavigator 인터페이스와 이를 구현하는 AppNavigatorImpl 클래스입니다. 이 인터페이스는 MainActivity에서 종속 항목으로 요청됩니다.
@Binds
어노테이션 사용// Navigation을 요청하는 곳이 Activity 이므로
// Activity Component로 지정하여 액티비티 컨테이너에서 종속 항목을 주입해주도록 설정
@Module
@InstallIn(ActivityComponent::class)
abstract class NavigationModule {
// interface를 binding(Hilt가 제공하는 타입)으로 추가하기 위해서 @Binds 어노테이션 사용
// 반환 타입은 Hilt에게 종속 항목으로 알릴 interface
// 파라미터는 interface를 구현하는 객체
@Binds
abstract fun bindNavigator(impl: AppNavigatorImpl): AppNavigator
}
@Module
과 @InstallIn
어노테이션을 사용해서 Hilt 모듈을 생성하였고, 모듈 클래스는 위에서 언급했듯이 추상 클래스로 선언하였습니다. 메서드에는 @Binds
어노테이션을 선언했고 반환 타입은 제공하려는 인터페이스, 파라미터는 인터페이스를 구현하는 객체를 넣습니다.
정리하자면 다음과 같습니다.
@Module
: 모듈을 생성하는 어노테이션이고@InstallIn
: 어떤 컴포넌트에서 모듈안에 선언된 타입들을 주입해 줄지를 결정하는 어노테이션@Provide
: 인터페이스와 외부 라이브러리를 제공하는 방법을 Hilt에게 알려주는 어노테이션@Binds
: 인터페이스를 제공하는 방법을 Hilt에게 알려주는 어노테이션잠깐 MainActivity.kt에서 Navigation 인터페이스를 종속 항목으로 요청했던 코드를 다시 보도록 하겠습니다.
// MainActivity.kt
@AndroidEntryPoint
class MainActivity : AppCompatActivity() {
// 필드 주입을 통해 AppNavigator 타입의 종속 항목 요청
@Inject
lateinit var navigator: AppNavigator
...
}
이 코드를 보면 약간 궁금한 점이 생기시는 분이 계실수도 있습니다. 만약 AppNavigator 인터페이스를 구현하는 클래스가 두 개라면 Hilt는 어떻게 이들을 구별할 수 있을까요?? @Qualifer
어노테이션을 사용하면 이들을 구별하는 방법을 Hilt에게 알려줄 수 있습니다. 예시 코드와 함께 알아보도록 하곘습니다.
Codelab 예제 코드에는 로그를 기록하고 확인하고 지우는 Data Source가 있습니다. 하나는 Memory를 통해 로그를 남기고, 하나는 Local Database를 통해 로그를 남깁니다. 따라서 이 둘의 기능의 틀은 똑같기에 인터페이스를 선언하고 이 두 클래스가 이를 구현하도록 만들었습니다.
// Logger data source를 위한 공통 인터페이스
interface LoggerDataSource {
fun addLog(msg: String)
fun getAllLogs(callback: (List<Log>) -> Unit)
fun removeLogs()
}
// LoggerDataSource 인터페이스를 구현하는 또다른 클래스
// ActivityScope를 사용하여 ActivityComponent 에서 같은 LoggerInMemoryDataSource 객체를 획득할 수 있다.
@ActivityScoped
class LoggerInMemoryDataSource @Inject constructor(
): LoggerDataSource {
...
}
/ LoggerDataSource 인터페이스를 구현하는 클래스
// Singleton Scope를 사용하여 앱이 실행되어 있는 동안에 같은 LoggerLocalDataSource 객체가 전달
@Singleton
class LoggerLocalDataSource @Inject constructor(
private val logDao: LogDao
): LoggerDataSource {
...
}
LoggerDataSource 인터페이스를 선언하고 이를 구현하는 LoggerInMemoryDataSource 클래스와 LoggerLocalDataSource 클래스를 선언하였습니다. 이제 LoggerDataSource 인터페이스를 제공하기 위해서 Hilt 모듈을 생성하고 @Provide
또는 @Binds
어노테이션을 사용해서 이를 제공하는 방법을 Hilt에게 알려줘야 합니다. 아래는 @Binds
어노테이션을 사용한 코드입니다.
// LoggerDataSource 인터페이스를 구현하는 LoggerInMemoryDataSource와
// LoggerLocalDataSource는 서로 다른 컴포넌트로 scope가 지정되었기에
// 같은 모듈에 선언할 수 없다.
@InstallIn(SingletonComponent::class)
@Module
abstract class LoggingDatabaseModule {
@Singleton
@Binds
abstract fun bindDatabaseLogger(impl: LoggerLocalDataSource): LoggerDataSource
}
@InstallIn(ActivityComponent::class)
@Module
abstract class LoggingInMemoryModule {
@ActivityScoped
@Binds
abstract fun bindInMemoryLogger(impl: LoggerInMemoryDataSource): LoggerDataSource
}
// LoggerDataSource를 요청하는 ButtonsFragment
// 프래그먼트 생명주기에 해당하는 의존성 컨테이너(컴포넌트)를 생성
@AndroidEntryPoint
class ButtonsFragment : Fragment() {
@Inject
lateinit var logger: LoggerDataSource
이제 Module을 선언해서 인터페이스를 제공하는 방법을 Hilt에게 알려주었습니다. 그리고
ButtonsFragment에서 LoggerDataSource 인터페이스타입을 Hilt에게 주입해달라고 요청하였습니다. 하지만 이와 같이 사용한다면 error: [Dagger/DuplicateBindings] com.example.android.hilt.data.LoggerDataSource is bound multiple times
와 같은 에러가 발생합니다. 그 이유는 Hilt가 LoggerDataSource를 구현하는 클래스 중에서 어떠한 타입을 주입해주어야 할지 모르기 때문입니다. 따라서 어떤 타입을 제공해야할지 Hilt에게 알려줘야 하는데, 이를 위해서 @Qualifier
어노테이션을 사용하는 것입니다.
@InstallIn(SingletonComponent::class)
@Module
abstract class LoggingDatabaseModule {
// Qualifier 지정
@DatabaseLogger
@Singleton
@Binds
abstract fun bindDatabaseLogger(impl: LoggerLocalDataSource): LoggerDataSource
}
@InstallIn(ActivityComponent::class)
@Module
abstract class LoggingInMemoryModule {
// Qualifier 지정
@InMemoryLogger
@ActivityScoped
@Binds
abstract fun bindInMemoryLogger(impl: LoggerInMemoryDataSource): LoggerDataSource
}
// Qualifier(한정자) 정의
// LoggerDataSource 인터페이스를 구현하는 두 클래스(LoggerInMemoryDataSource, LoggerLocalDataSource)를
// Hilt는 구별하지 못하기에 이를 Hilt에게 알려줘서 구별하도록 하기위해서 사용한다.
@Qualifier
annotation class InMemoryLogger
// Qualifier(한정자) 정의
@Qualifier
annotation class DatabaseLogger
@AndroidEntryPoint
class ButtonsFragment : Fragment() {
// Field Injection
// LoggerInMemoryDataSource를 삽입하는 지점에도 Qualifier 선언
// 이제 Hilt는 logger가 LoggerInMemoryDataSource 타입임을 안다
@InMemoryLogger
@Inject
lateinit var logger: LoggerDataSource
InMemoryLogger와 DatabaseLogger 어노테이션 클래스를 선언하며 @Qualifier
어노테이션을 지정하였습니다. 각 한정자는 타입을 식별하는데 사용되기에 구현별로 선언하였고, 선언한 어노테이션들을 메서드에 지정하였습니다. 이제 종속 항목을 요청하는 곳에서도 필요한 구현체에 맞는 어노테이션을 지정하면, Hilt는 이를 구별하여 적절한 타입을 주입해줍니다.
Hilt는 여러 어노테이션을 사용하면 자동으로 컴포넌트가 생성되고 종속 항목들을 주입해주기에 익숙해진다면 편리하게 사용할 수 있는 라이브러입니다.
계속해서 공부하며 추가로 알게되는 부분은 새로 글을 작성하거나 해당 글에 추가하겠습니다.
* Hilt test, @EntryPoint
만약 이 글을 읽으며 Hilt에 대해 알아보려고 하신다면, 참조에 달아놓은 글들도 꼭 읽어보시기 바랍니다.
참조
안드로이드 developer - Depedency injection with hilt
Depedency Injection on Android with Hilt
Depedency Injection: Dagger-Hilt vs Koin
DI 기본개념부터 사용법까지, Dagger2 시작하기
Hilt 개념 설명 및 사용법
Hilt for DroidKnights
틀린 부분은 댓글로 남겨주시면 수정하겠습니다..!!
너무 잘읽었습니다!!! 감사합니다.
이제 글 작성은 안하시는 것 같지만, 글을 정주행 하면서 읽은건 처음이에요.
이런 정리글 적어주셔서 정말 갑사합니당 ㅎㅎ