[Android] Hilt - Retrofit + ViewModel로 이해해보기

DI

Dependency Injection는 의존성 주입으로 객체의 생성과 관심을 분리하는 것이다. 

 

간단하게 설명하면, 어떤 객체가 필요로 하는 의존성을 직접 만들어내지 않고, 외부에서 주입받는 것을 말한다.

A가 B의 기능을 사용해야 한다고 한다면, A는 B의 직접 생성하지 않고, 외부에서 B를 만들어서 A에게 주입시켜준다. 

 

장점

그렇다면, 이렇게 의존성을 주입했을 때 어떠한 부분이 좋아질까? 

A와 B의 결합도가 낮아지고 유연한 구조를 가지게 된다. 이렇게 되면, B의 변경이 발생하게 되면 일반적으로 A에게 영향을 미쳤지만, DI를 적용하게 되어 A에게도 영향이 미치지 않을 가능성이 높아지게 된다.

 

결국 대규모 어플리케이션에서의 유지보수성을 향상시키게 된다.

 

Hilt

Hilt는 안드로이드에서 DI를 쉽게하기 위해 제공하는 라이브러리이다.

 

대표 컨셉과 기능에 대해 설명하겠다.

  1. Component : 주입할 의존성을 관리하는 컴포넌트를 자동으로 생성한다.
    1. 예를 들어 @AndroidEntryPoint와 같은 어노테이션을 사용하여, 액티비티/프래그먼트/서비스 등 안드로이드 구성요소를 주입가능한 컴포넌트로 지정하게 된다.
  2. Modules : 의존성 객체를 생성하고 제공하는 방법을 정의하는 모듈을 사용한다. @InstallIn 어노테이션을 사용하여 컴포넌트에 모듈을 설치하고, 모듈 내에서 의존성 객체를 바인딩한다.

사용 방법

당연히 사용을 하기 위해서는 implementation을 해주어야 한다.

 

build.gradle(Project)

id 'org.jetbrains.kotlin.android' version '1.8.0' apply false
    id 'com.google.dagger.hilt.android' version '2.44' apply false

build.gradle (module)

plugins {
    ```
    id 'kotlin-kapt'
    id 'com.google.dagger.hilt.android'
}
dependencies {
    implementation "com.google.dagger:hilt-android:2.44"
    kapt "com.google.dagger:hilt-compiler:2.44"
    ```
}

 

Application을 만들어준다.

본 예제에는 HiltApplication.kt로 만들어 주었다.

importandroid.app.Application
importdagger.hilt.android.HiltAndroidApp

@HiltAndroidApp
classHiltApplication : Application() {

}

Activity에 종속성을 주입해준다.

@AndroidEntryPoint
classMainActivity : AppCompatActivity() {
override funonCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_main)
    }
}

 

이번 예제에서는 Retrofit + MVVM를 이용하여 예시를 들어보겠다.

Retrofit을 이용한 통신에서 나는 고양이 사진을 랜덤으로 제공해주는 API를 사용해보겠다.

 

가장 먼저 의존성 주입을 설정해야한다.

Hilt를 사용하여 레포지토리에 의존성을 주입하기 위해 모듈을 생성한다.

이 때 Retrofit에 필요한 HTTP 클라이언트, API 인터페이스(쿼리문)를 정의한다. 이렇게 정의를 하면 의존성을 주입하여 필요한 곳에서 주입받아 사용할 수 있게 된다.

@Module
@InstallIn(SingletonComponent::class)
object ApiModule {
    @Provides
    fun provideBaseUrl() = "https://api.thecatapi.com/v1/images/"

    @Singleton
    @Provides
    fun provideOkHttpClient() = if (BuildConfig.DEBUG) {
        val loggingInterceptor = HttpLoggingInterceptor()
        loggingInterceptor.setLevel(HttpLoggingInterceptor.Level.BODY)
        OkHttpClient.Builder()
            .addInterceptor(loggingInterceptor)
            .build()
    } else {
        OkHttpClient.Builder().build()
    }

    @Singleton
    @Provides
    fun provideRetrofit(okHttpClient: OkHttpClient): Retrofit {
        return Retrofit.Builder()
            .client(okHttpClient)
            .baseUrl(provideBaseUrl())
            .addConverterFactory(GsonConverterFactory.create())
            .build()
    }

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

    @Provides
    @Singleton
    fun provideCatRepository(apiService:ApiService) = CatRepository(apiService)

}

 

레포지토리 인터페이스를 구현한다.

레포지토리 인터페이스를 생성하고, 필요한 작업을 정의하는데, 위의 Module에서 API 관련 쿼리문을 작성해주면 된다. 

interface ApiService {
    @GET("search")
    suspend fun onGetCat (@Query("limit") limit : String ): Response<List<Cat>>
}

 

레포지토리 구현

인터페이스를 생성해주었다면, 구현을 해준다. 이 때 @Inject 어노테이션을 사용하여 의존성을 주입한다.

class CatRepository @Inject constructor(private val apiService: ApiService){
    suspend fun getCat(limit : String) = apiService.onGetCat(limit)
}

 

실제로 사용하기

MVVM 패턴을 사용하기로 했으므로 ViewModel를 작성해주겠다.

@HiltViewModel
class MainViewModel @Inject constructor(private val repository: CatRepository): ViewModel(){
    var catData = MutableLiveData<String>("응답 없음")

    fun getData() = catData

    init {
        loadData()
    }

    private fun loadData() {
        viewModelScope.launch {
            val data = repository.getCat("1")
            when (data.isSuccessful) {
                true -> {
                    if(data.body() != null) catData.postValue(data.body()!![0].url)
                }
                false -> {
                    catData.postValue("응답 없음")
                }
            }
        }
    }
}

@HiltViewModel를 통해 ViewModel 클래스를 정의 해준다. 그리고 생성자를 통해 필요로 하는 의존성을 주입시켜 주었다. 그리고 API 통신을 통해 데이터를 가져와주었다.

 

@AndroidEntryPoint
class MainActivity : AppCompatActivity() {
    private lateinit var binding: ActivityMainBinding
    private val viewModel: MainViewModel by viewModels()

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        binding = DataBindingUtil.setContentView(this, R.layout.activity_main)

        viewModel.getData().observe(this) {
            data -> data?.let {
                Glide.with(this)
                    .load(it)
                    .into(binding.catIv)
        }
        }
    }
}

MainActivity의 경우에는 @AndroidEntryPoint 어노테이션을 사용해주고, by viewModels()를 통해 viewModel를 생성하고 의존성 주입을 자동으로 처리한다. 그리고 viewModel의 observer를 통해 새로운 고양이 사진이 api 통신에 의해 가져와지면  화면이 업데이트 되도록 한다.

후기

안드로이드를 1년 조금 넘게 개발하고 디자인 패턴 수업을 들으면서 안드로이드에서의 DI를 적용해봐야지 라고 생각은 해왔지만, 실제로 프로젝트에 적용한 적은 없었다. 이번에 Hilt에 대해 공부를 해보았으니, 간단하더라도 프로젝트에 적용하여 개발해봐야 겠다는 생각이 들었다.