본문 바로가기
2023년 이전/Android

Hilt

by JeongUPark 2021. 12. 3.
반응형

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-alpha2.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

 

Hilt 사용법 및 Module (Binds vs Provides) 정리 및 후기

Hilt를 적용하면서 실 사용법과 Module 주입시 Binds 와 Provides 방법중 어떤것을 사용해야할지에 대해 정리하였습니다. 1. Hilt 사용법 build.gradle implementation("com.google.dagger:hilt-android:${Version..

yuar.tistory.com

위 설명의 간추린 내용은 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