Hilt

Hilt

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

관련 링크는 아래 클릭하면 나타납니다.

위의 링크의 내용을 따라하면 (위의 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의 비교에 대한 이해를 돕기위한 참고는 아래에 잘 되어 있습니다.

https://yuar.tistory.com/84

위 설명의 간추린 내용은 Binds가 파일수가 적고 생성도 간결해서 좋은데 생성시 사용되는 구현체가 @Inject constructor()를 사용할 수 없다면 쓸 수 없다는 것입니다.

from http://jeongupark-study-house.tistory.com/200 by ccl(A) rewrite - 2021-12-03 23:02:07