본문 바로가기
Android/Kotlin

Android Jetpack Hilt 를 이용하여 의존성 주입하기

by wannagohome97 2024. 1. 22.

Hilt 란?

Hilt 는 Android Jetpack 에서 지원하는 DI ( Dependency Injection ) Library 로 Dagger 를 기반으로 빌드되었다고 합니다.

하지만 Dagger 는 러닝 커브가 매우 높은 편이고(쉽게 말하면 어렵다...)

그걸 Android 쪽에서도 인지하고 만든게 Hilt 입니다. 그래서 Dagger 보단 구현적인 측면에선 많이 쉽습니다(개인적인 의견입니다.)

 

DI 에 대한 개념이 잘 잡히지 않으신다면 우선 아래 링크를 보고 오시면 될 것 같습니다.

 

Android의 종속 항목 삽입  |  Android 개발자  |  Android Developers

Android의 종속 항목 삽입 컬렉션을 사용해 정리하기 내 환경설정을 기준으로 콘텐츠를 저장하고 분류하세요. 종속 항목 삽입(DI)은 프로그래밍에 널리 사용되는 기법으로, Android 개발에 적합합니

developer.android.com

 

아래는 Hilt 를 프로젝트에서 사용할 경우 gradle 에서 세팅해야 할 항목들입니다.

 

Project Level 의 build.gradle 입니다.

buildscript {
    dependencies {
        classpath ("com.google.dagger:hilt-android-gradle-plugin:2.46.1")
    }
}
plugins {
//   id("com.android.application") version "8.2.1" apply false
//   id("com.android.library") version "8.2.1" apply false
    id("org.jetbrains.kotlin.android") version "1.8.10" apply false
}

 

module level 의 build.gradle 입니다.

plugins {
//    id("com.android.application")
//    id("org.jetbrains.kotlin.android")
    id ("org.jetbrains.kotlin.kapt")
    id ("dagger.hilt.android.plugin")
}
dependencies {
// ...
    implementation ("com.google.dagger:hilt-android:2.46.1")
    kapt ("com.google.dagger:hilt-compiler:2.46.1")
// ... 
}

 

위의 두 코드 블럭에서 주석처리되지 않은 부분들을 Gradle 에 추가해주고

 

현재 어플리케이션이 HiltAndroidApp 임을 annotation 을 통해 명시해줍니다.

@HiltAndroidApp
class MyApplication : Application(){

    override fun onCreate() {
        super.onCreate()
    }
}

 

이런식으로 Application 을 상속받는 class 를 생성하여 annotation 을 붙여주고

 

    <application
        android:name=".MyApplication"
// ...
    </application>

 

manifest 에서 이렇게 선언해줍니다.

 

 

오늘 예시로 사용할 소스코드는 TMDB 의 영화 검색 API 를 Hilt 를 이용해서 주입하는 과정입니다.

interface TmdbApi{

    @GET("$ACTION_DISCOVER/$TYPE_SERIES?api_key=${API_KEY}")
    fun getTvData(@QueryMap options: Map<String, String>): Call<BaseData<Series>>

    @GET("$ACTION_DISCOVER/$TYPE_MOVIE?api_key=${API_KEY}")
    fun getMovieData(@QueryMap options: Map<String, String>): Call<BaseData<Movie>>
    
}

 

그리고 이제 의존성을 주입해줄 모듈을 만들어봅시다.

@Module
@InstallIn(SingletonComponent::class)
object NetworkModule {

    private val loggingInterceptor = HttpLoggingInterceptor()

    @Provides
    @Singleton
    fun provideClient(): OkHttpClient{
        return OkHttpClient.Builder()
            .addInterceptor(loggingInterceptor.apply {
                level = HttpLoggingInterceptor.Level.BODY
            }).build()
    }

    @Provides
    @Singleton
    fun provideRetrofit(client: OkHttpClient): Retrofit {
        return Retrofit.Builder()
            .client(client)
            .baseUrl(TmdbApi.BASE_URL)
            .addConverterFactory(JacksonConverterFactory.create())
            .build()
    }

    @Provides
    @Singleton
    fun provideTmdb(retrofit: Retrofit): TmdbApi {
        return retrofit.create(TmdbApi::class.java)
    }
    
}

 

이렇게하면 아래와같이 좌측에 종속성에 관한 아이콘이 생길겁니다.

눌러보면 이 메서드가 어디로부터 종속성을 받아오는것인지 혹은 어디로 종속성을 뿌려주는 것 인지 바로가기로 이동시켜줍니다.(이 부분이 진짜 편합니다)

 

그리고 저 TmdbApi 를 이제 주입받아봅시다.

아래의 경우 종속성을 생성자에서 주입 받았습니다.

@Singleton
class VideoRepository @Inject constructor(
    private val tmdbApi: TmdbApi
) {
    fun getData(
        queries: Map<String, String>
    ): BaseData<*>? {
        val call= tmdbApi.getDiscoveredTvData(
            queries
        )
        val response = call.execute()
        return if (response.isSuccessful){
            response.body()
        }
        else{
            null
        }
    }
}

 

그리고 이 Repository 를 ViewModel 에서 간단하게 주입받아 사용할 수 있습니다.

 

@HiltViewModel
class VideoViewModel @Inject constructor(
    private val repository: VideoRepository
) : ViewModel(){


    private val mVideoData = MutableLiveData<BaseData<*>>()

    val data: LiveData<BaseData<*>>
        get() {
            return mVideoData
        }

    suspend fun setData(options: Map<String, String>){
        mVideoData.postValue(
            repository.getData(
                queries = options
            )
        )
    }
}

 

기존에 ViewModelFactory 를 상속받은 class 의 메서드를 수정해서 사용하던 것 보다 훨씬 코드가 간단해졌죠.

 

위 코드들을 간단하게 테스트 해봅시다

val viewModel = ViewModelProvider(this)[VideoViewModel::class.java]
viewModel.data.observe(this){
    println(it)
}
val options = Options.discoverVideo(
    page = 1,
    networkId = TmdbConfigs.Tving.id
)
CoroutineScope(Dispatchers.IO).launch {
    viewModel.setData(options)
}
BaseData(page=1, results=[Series(backdropPath=/icy4p7Nb33UU2S5eHvlBcE0Dvmi.jpg, firstAirDate=2023-12-15, genreIds=[18, 10765], id=218230, name=이재, 곧 죽습니다, originCountry=[KR], originalLanguage=ko, originalName=이재, 곧 죽습니다, overview=지옥으로 떨어지기 직전의 이재가 죽음이 내린 심판에 의해 12번의 죽음과 삶을 경험하게 되는 판타지 인생 환승 드라마. 이원식 & 꿀찬의 웹툰 "이제 곧 죽습니다" 원작, popularity=200.883, posterPath=/5LwZzaFN0kmpLWuqPm6LnF4iRF2.jpg, voteAverage=8.6, voteCount=91), ... ]

 

제대로 동작 하는군요