Hilt는 Dagger 종속 항목 삽입 라이브러리를 기반으로 빌드되어 Dagger를 Android 애플리케이션에 통합하는 표준 방법을 제공합니다
이런 hilt는 Dagger2와 Koin의 단점을 개선해서 나온 사용하기 쉬운 라이브러로 생각됩니다. (특히, 러닝커프(학습곡선) 이/가 어마어마하게 낮은것 같습니다. 비교 대상은 Dagger2입니다.)
Setting
우선 사용을 위한 Setting은 다음과 같습니다.
build.gralde (project) 에 다음을 추가 합니다.
2021.03.13 확인해보니 버전이 2.33-beta 이었습니다. 그리고 현재(2021.03.13) 2.33-beta 메이븐 배포 안되고 있습니다. 쓰면 빌드 안됩니다.
buildscript {
...
dependencies {
...
classpath 'com.google.dagger:hilt-android-gradle-plugin:2.28-alpha'
}
}
그리고 build.gradle (app) 에 다음을 추가합니다.
...
apply plugin: 'kotlin-kapt'
apply plugin: 'dagger.hilt.android.plugin'
android {
...
}
dependencies {
implementation "com.google.dagger:hilt-android:2.28-alpha"
kapt "com.google.dagger:hilt-android-compiler:2.28-alpha"
}
그리고 마지막으로 Hilt는 자바 8 기능을 사용하기 때문에, 프로젝트에서 자바 8을 사용 설정하기 위해 build.gradle (app)에 다음을 추가합니다.
android {
...
compileOptions {
sourceCompatibility JavaVersion.VERSION_1_8
targetCompatibility JavaVersion.VERSION_1_8
}
}
Start
학습 코드는 android developer에서 제공하는 코드를 사용했습니다.
git clone <https://github.com/googlecodelabs/android-hilt>
관련 링크는 아래 클릭하면 나타납니다.
위의 링크의 내용을 따라하면 (위의 git을 받으신 후에 ServiceLocator.kt 을 지우고 관련 code와 method를 지우고 시작하시면 됩니다.)
Adding Hilt to the Project
Hilt를 사용하기 위한 Setting을 위의 Setting 내용대로 Setting 해줍니다.
Hilt in your Application
그 다음
@HiltAndroidApp
class LogApplication : Application() {
...
}
위의 code처럼 Application 클래스에 HiltAndroidApp 어노테이션을 붙여 줍니다.
이러게 하는 이유는 애플리케이션 컨테이너는 앱의 상위 컨테이너이며, 이는 다른 컨테이너가 제공하는 종속성에 액세스 할 수 있음을 의미합니다.
Field injection with Hilt
다음은 종속 삽입을 수행할 프레그 먼트에 다음과 같이 어노테이션을 추가합니다.
@AndroidEntryPoint
class LogsFragment : Fragment() {
...
}
@AndroidEntryPoint로 Android 클래스에 주석을 달면 Android 클래스 수명주기를 따르는 종속성 컨테이너가 생성됩니다. 그래서 Hilt는 LogsFragment의 수명주기에 연결된 종속성 컨테이너를 만들고 LogsFragment에 인스턴스를 주입 할 수 있습니다.
(참고 Hilt는 현재 애플리케이션 (@HiltAndroidApp 사용), Activity, Fragment, View, Service 및 BroadcastReceiver와 같은 Android 유형을 지원합니다.
Hilt는 FragmentActivity (예 : AppCompatActivity)를 확장하는 활동과 Jetpack 라이브러리 Fragment를 확장하는 프래그먼트 만 지원하며 Android 플랫폼의 (현재 사용되지 않는) 프래그먼트는 지원하지 않습니다. )
다음은 @Inject을 통하여 hilt가 다른 유형의 인스턴스를 주입할 수 있도록 합니다.
@AndroidEntryPoint
class LogsFragment : Fragment() {
@Inject lateinit var logger: LoggerLocalDataSource
@Inject lateinit var dateFormatter: DateFormatter
...
}
그리고 위의 LoggerLocalDataSoure와 DateFormatter class를 다음과 같이 수정해줍니다.
class DateFormatter @Inject constructor() { ... }
class LoggerLocalDataSource @Inject constructor(private val logDao: LogDao) {
...
}
위 처럼 클래스의 생성자에 @Inject 추가하여 Hilt에게 삽입하려는 클래스가 유형의 인스턴스로 제공된다는 것을 알려줍니다.
Scoping instances to containers
아래와 같이 Singleton이라는 어노테이션을 달면
@Singleton
class LoggerLocalDataSource @Inject constructor(private val logDao: LogDao) {
...
}
언제나 애플리케이션 컨테이너가 항상 동일한 인스턴스를 제공하도록합니다.
이런 스코프의 내용은 여기서 확인 가능 합니다.
Hilt modules
여기까지 진행 후 app을 실행해보면 정상동작 하지 않을 것 입니다.
그 이유는 위의 LoggerLocalDataSource의 logDao의 존재를 hilt가 알 수 없기 때문입니다. (아래를 관련 error 코드)
[Dagger/MissingBinding] com.example.android.hilt.data.LogDao cannot be provided without an @Provides-annotated method.
그래서 다음과 같은 Module을 만들어 hilt에게 그 존재를 알려 줍니다.
@InstallIn(ApplicationComponent::class)
@Module
object DatabaseModule {
@Provides
@Singleton
fun provideDatabase(@ApplicationContext appContext: Context): AppDatabase {
return Room.databaseBuilder(
appContext,
AppDatabase::class.java,
"logging.db"
).build()
}
@Provides
fun provideLogDao(database: AppDatabase): LogDao {
return database.logDao()
}
}
그럼 위에서 말한 Module은 무엇일까요?
Module은 Hilt에 바인딩을 추가하는 데 사용됩니다. 즉, Hilt에 다른 유형의 인스턴스를 제공하는 방법을 알리는 데 사용됩니다. 그래서 Hilt Module에는 프로젝트에 포함되지 않은 인터페이스 또는 클래스와 같이 생성자 주입이 불가능한 유형에 대한 바인딩이 포함됩니다.
Hilt Module은 @Module 및 @InstallIn으로 주석이 달린 클래스입니다. @Module은 Hilt에게 이것이 모듈임을 알리고 @InstallIn은 Hilt 컴포넌트를 지정하여 바인딩을 사용할 수있는 컨테이너를 Hilt에게 알려줍니다.
Hilt에서 삽입 할 수있는 각 Android 클래스에는 연결된 Hilt 구성 요소가 있습니다. 예를 들어 Application 컨테이너는 ApplicationComponent와 연결되고 Fragment 컨테이너는 FragmentComponent와 연결됩니다.
그럼 위의 DataseMoudle object와 NavigationModule object를 분석해보겠습니다.
우선 아래 코드는 무슨 의미일까요?
@InstallIn(ApplicationComponent::class)
위에서 설명했다 시키 @InstallIn을 통하여 Hilt에게 ApplicationComponent에서 바인딩을 사용할 수 있다고 알려주는 것입니다.
Hilt 모듈인 DataseModule object에서 @Provides로 함수에 주석을 달아 Hilt에 생성자 주입이 불가능한 유형을 제공하는 방법을 알릴 수 있습니다. 그리고 @Provides 주석 함수의 함수 본문은 Hilt가 해당 유형의 인스턴스를 제공해야 할 때마다 실행됩니다. @Provides 주석 함수의 반환 유형은 Hilt에게 바인딩 유형 또는 해당 유형의 인스턴스를 제공하는 방법을 알려줍니다. 함수 매개 변수는 유형의 종속성입니다.
(참고 : 작성날짜 2021.3.12 일 기준 신규 버전인 2.33-beta 에서는 일부 hilt component가 사라졌습니다. 그중에 ApplicationComponent 가 포함되어있습니다. 아래는 2.28-alpha 와 2.33-beta hilt component들의 비교 입니다. 각 버전 링크를 누르면 상세를 볼 수 있습니다.)
그래서
@Provides
fun provideLogDao(database: AppDatabase): LogDao {
return database.logDao()
}
위의 코드를 통하여 Hilt가 LogDao를 제공 해줘야 할때 마다 제공 할 수 있게 되었습니다.
그리고
@Provides
@Singleton
fun provideDatabase(@ApplicationContext appContext: Context): AppDatabase {
return Room.databaseBuilder(
appContext,
AppDatabase::class.java,
"logging.db"
).build()
}
위 코드를 통하여 Hilt가 AppDatabase를 제공 해줘야 할 때마다 제공할 수 있게되었습니다. (그래서 위의
provideLogDao 함수의 database에 인스턴스를 제공 하게 됩니다.) 그리고 Singleton을 달아서 항상 동일 한 AppDatabase 인스턴스를 Hilt로 부터 제공 받을 수 있습니다. (각 Hilt 컨테이너에는 사용자 지정 바인딩에 종속성으로 삽입 할 수있는 기본 바인딩 집합이 함께 제공됩니다. provideDatabase에서 applicationContext의 경우입니다. 액세스하려면 @ApplicationContext로 필드에 주석을 추가해야합니다. 그리고 여기서 그 기본 바인딩 집합을 확인 하실 수 있습니다.)
이제
@AndroidEntryPoint
class MainActivity : AppCompatActivity() { ... }
적용 후 app을 빌드하면 앱이 정상적으로 빌드가 됩니다. 하지만 실행을 하면 앱이 죽습니다. 그리고 그 죽는 log를 보면 MainActivity에서 lateinit property navigator has not been initialized 되어 죽었다고 나타납니다.
그럼 위의 에러를 해결하기 위해 어떻게 해야할까요?
Providing interfaces with @Binds
위에서 말한 에러를 해결하기 위해 MainActivity와 ButtonFragment에 있는 navigator 변수를 initalized 해주어야 합니다. 그리고 hilt를 통하여 이를 처리 하기 위해 아래와 같은 module을 만들어 줍니다.
@InstallIn(ActivityComponent::class)
@Module
abstract class NavigationModule {
@Binds
abstract fun bindNavigator(impl: AppNavigatorImpl): AppNavigator
}
위에 코드의 내용을 보면 우선 AppNavigator는 인터페이스이기 때문에 생성자 주입을 사용할 수 없습니다. 인터페이스에 사용할 구현을 Hilt에 알리려면 Hilt module 내부의 함수에 @Binds 주석을 사용할 수 있습니다.
@Binds는 추상 함수에 주석을 달아야합니다 (추상적이므로 코드가 포함되어 있지 않으며 클래스도 추상이어야합니다). 추상 함수의 반환 유형은 구현을 제공하려는 인터페이스입니다 (예 : AppNavigator). 구현은 인터페이스 구현 유형 (예 : AppNavigatorImpl)과 함께 고유 한 매개 변수를 추가하여 지정됩니다.
그리고 위의 AppNavigatorImpl을
class AppNavigatorImpl @Inject constructor(
private val activity: FragmentActivity
) : AppNavigator {
...
}
생성자 주입을 적용하여 hilt에서 인스턴스를 제공 받을 수 있도록 합니다.
그리고 이제 MainActivity와 ButtonFragment의 변수 navigator를 다음과 같이 변경해 줍니다.
@Inject lateinit var navigator: AppNavigator
그리고 참고로 Hilt Module은 비 정적 및 추상 바인딩 메서드를 모두 포함 할 수 없으므로 동일한 클래스에 @Binds 및 @Provides 주석을 배치 할 수 없습니다. 그 이유는
위에서 언급한 DatabaseModule 모듈은 ApplicationComponent에 설치되므로 애플리케이션 컨테이너에서 바인딩을 사용할 수 있습니다. 새로운 내비게이션 정보 (예 : AppNavigator)에는 Activity의 특정 정보가 필요합니다 (AppNavigatorImpl에는 Activity가 종속성으로 있음) 따라서 활동에 대한 정보를 사용할 수있는 애플리케이션 컨테이너 대신 엑티비티 컨테이너에 설치해야합니다.
이제 마지막으로 다음과 같이 MainAcitivty와 ButtonFragment에 @AndroidEntryPoint 처리 해주고
@AndroidEntryPoint
class MainActivity : AppCompatActivity() { ... }
@AndroidEntryPoint
class ButtonsFragment : Fragment() { ... }
앱을 빌드 실행 하면
정상적으로 잘 작동 하는 것을 확인 하실 수 있습니다.
그리고 @Binds와 @Provides의 비교에 대한 이해를 돕기위한 참고는 아래에 잘 되어 있습니다.
위 설명의 간추린 내용은 Binds가 파일수가 적고 생성도 간결해서 좋은데 생성시 사용되는 구현체가 @Inject constructor()를 사용할 수 없다면 쓸 수 없다는 것입니다.
'2023년 이전 > Android' 카테고리의 다른 글
onActivityResult / onRequestPermissionsResult deprecated (0) | 2021.12.03 |
---|---|
Hilt를 사용한 ViewModel (0) | 2021.12.03 |
EditText의 유지 (0) | 2021.12.03 |
SafetyNet 보안 관련 (0) | 2021.12.03 |
In-App Reviews (0) | 2021.12.03 |