# Hilt란?
공식문서에 정리되어있는 Hilt의 정의이다
Hilt는 프로젝트의 모든 Android 클래스에 컨테이너를 제공하고 수명 주기를 자동으로 관리함으로써, 애플리케이션에서 DI를 사용하는 표준 방법을 제공합니다.
그럼 DI란 뭘까?
DI는 Dependency Injection의 약자로, 의존성 주입을 뜻한다
특정 한 객체가 다른 객체를 필요로 할 때 이 의존성을 제공하는 기술이 DI이다
객체가 다른 객체를 필요로 하면, 외부에서 해당하는 객체를 생성하여 필요한 객체에 넘겨주게 된다
DI는 아래와 같은 장점이 있다.
- 코드 재사용성 향상
- 결합도 감소
- 테스트 용이성
- 의존성을 가짜 객체나 Mock 객체로 대체하여 테스트 수행 가능
- 코드의 유연성 및 확장성, 가독성 향상
- 새로운 기능을 추가하거나, 기능을 수정할 때 미치는 영향 감소
- 의존성 추적 용이성
- 클래스가 사용하는 종속성의 명확한 파악 가능
Android Developers에 있는 공식 코드 예제를 보면서 이해해보자
class Car {
private val engine = Engine()
fun start() {
engine.start()
}
}
fun main(args: Array) {
val car = Car()
car.start()
}
첫번째 코드
class Car(private val engine: Engine) {
fun start() {
engine.start()
}
}
fun main(args: Array) {
val engine = Engine()
val car = Car(engine)
car.start()
}
두번째 코드
비슷해보이지만 첫번째 코드는 DI를 사용하지 않은 코드고, 두 번째 코드는 DI를 사용해 의존성을 주입한 예제이다
첫 번째 코드를 보면 Car 클래스가 자체적으로 Engine을 생성해 사용하고 있다.
이 코드는 둘 간의 의존성이 높아 결합도가 높아지고, Car 클래스가 Engine을 직접 인스턴스화 해서, Engine의 서브 클래스들을 사용할 수 없다
반면 두 번째 코드는 main함수에서 Engine 인스턴스를 생성해, Car 클래스에 파라미터로 넘겨주고 있다
그렇기에 Car 클래스의 재사용성이 높아지고, Engine의 구현이 수정돼도 Car 클래스에는 영향을 미치지 않는다!!
이제 Hilt의 특징을 살펴보자
Hilt는 아래와 같은 특징이 있다.
- AndroidX와 통합
- 안드로이드 앱의 생명주기와 관련된 의존성 주입 지원
- 안드로이드 컴포넌트와 자동 통합
- Application, Activity, Fragment, ViewModel과 자동으로 통합
- Daager의 간소화
- Dagger의 복잡성을 간소화 하고 Dagger 컴포넌트 및 모듈의 생성, 관리를 자동화
- 코드 간소화 및 테스트 용이성
- DI 관련 코드를 간소화 하고, 테스트에서 필요한 의존성을 쉽게 대체 가능
# Hilt를 프로젝트에 적용해보기!
1. 의존성 추가
plugins {
...
alias(libs.plugins.ksp) //hilt
alias(libs.plugins.hilt) //hilt
}
dependencies {
...
//hilt
implementation(libs.hilt.android)
ksp(libs.hilt.android.compiler)
}
build.gradle(app)
plugins와 dependencies 부분에 추가해주기
plugins {
...
// hilt
alias(libs.plugins.hilt) apply false
alias(libs.plugins.ksp) apply false
}
build.gradle(project)
[versions]
...
# Annotation Processor
ksp = "1.9.21-1.0.16"
# Hilt
hilt = "2.50"
[libraries]
...
# Hilt
hilt-android = { module = "com.google.dagger:hilt-android", version.ref = "hilt" }
hilt-android-compiler = { module = "com.google.dagger:hilt-android-compiler", version.ref = "hilt" }
[plugins]
...
# Hilt
hilt = { id = "com.google.dagger.hilt.android", version.ref = "hilt" } # Hilt
ksp = { id = "com.google.devtools.ksp", version.ref = "ksp" } # Hilt
libs.versions.toml
이렇게 작성해주면 Hilt 세팅?은 끝난다
2. Application 클래스 작성
// Application 클래스 작성
// Hilt 라이브러리는 Application와 ApplicationContext에 접근하기 위해 ApplicationClass가 필요!
@HiltAndroidApp
class StandardApplication : Application()
StandardApplication.kt
보통 Application 클래스의 이름은 앱 이름 뒤에 Application을 붙여주는 식으로 많이 명명한다.
@HiltAndroidApp 어노테이션에 의해 Singleton Component를 생성하게 된다.
따라서 앱이 살아있는 동안 의존성(Dependency)을 제공하는 역할을 하는 애플리케이션(Application) 레벨의 Component가 된다
<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools">
<uses-permission android:name="android.permission.INTERNET" />
<application
android:allowBackup="true"
android:dataExtractionRules="@xml/data_extraction_rules"
android:fullBackupContent="@xml/backup_rules"
// 이코드 추가해주기!!
android:name=".presentation.StandardApplication"
android:icon="@mipmap/ic_launcher"
android:label="@string/app_name"
android:roundIcon="@mipmap/ic_launcher_round"
android:supportsRtl="true"
android:theme="@style/Theme.Standardfour_recyclerview"
tools:targetApi="31">
<activity
android:name=".presentation.githubmain.SearchMainActivity"
android:exported="true">
<intent-filter>
<action android:name="android.intent.action.MAIN" />
<category android:name="android.intent.category.LAUNCHER" />
</intent-filter>
</activity>
</application>
</manifest>
Application 클래스를 생성한 뒤에는 AndroidManifest 파일에 application 태그에 name 설정을 꼭 해주어야 오류가 생기지 않는다!
android:name=".presentation.StandardApplication" 이 코드를 꼭 추가해줘야한다
3. AppModule 클래스 생성
internal class HttpRequestInterceptor : Interceptor {
override fun intercept(chain: Interceptor.Chain): Response {
//캐싱된 token값을 가져온다면?
//val token : String? = runBlocking { userDataStore.getToken() }
return chain.proceed(
chain.request()
.newBuilder()
.run {
//Token 필요할 시 여기에 넣기! 이렇게 하면 call할 때마다 @Header 추가 안해줘도됨.
//this.addHeader("Token", token)
this.addHeader("Token", $Token값 넣기)
}
.build()
)
}
}
HttpRequestInterceptor.kt
@Header를 매번 추가하지 않도록하기위해서 HttpRequestInterceptor 클래스를 하나 생성해줬다
private const val BASE_URL = "https://api.github.com/"
@Module
@InstallIn(SingletonComponent::class)
object RetrofitClient {
//@Provides : room, retrofit과 같은 외부 라이브러리에서 제공되는 클래스여서 프로젝트 내에서 소유할 수 없는 경우 or builder 패턴을 통해 인스턴스를 생성해야 하는 경우 사용
//@Provides 어노테이션이 달린 함수는 return값이 있어야함 (return 유형은 함수가 어떤 유형의 인스턴스를 제공하는지 Hilt에 알려줘야함)
@Provides
@Singleton
// 네트워크 요청을 위한 httpClient 구성
fun providesOkHttpClient() : OkHttpClient{
val loggingInterceptor = HttpLoggingInterceptor()
loggingInterceptor.level = HttpLoggingInterceptor.Level.BODY
return OkHttpClient.Builder()
.addInterceptor(HttpRequestInterceptor()) // 만들어준 HttpRequestInterceptor() 넣어줌
.addNetworkInterceptor(loggingInterceptor)
.build()
}
@Provides
@Singleton
// retrofit 객체 초기화 및 생성
fun provideRetrofit(okHttpClient: OkHttpClient): Retrofit {
return Retrofit.Builder()
.baseUrl(BASE_URL)
.client(okHttpClient)
// Gson으로 컨버팅
.addConverterFactory(GsonConverterFactory.create())
.build()
}
@Provides
@Singleton
// Api Service 객체(SearchRemoteDataSource) 생성
fun provideSearchRemoteDataSource(retrofit: Retrofit): SearchRemoteDataSource {
return retrofit.create(SearchRemoteDataSource::class.java)
}
}
RetrofitClient.kt
Module임을 알리는 @Module 어노테이션을 붙여준다
앱 전체에서 사용할 모듈이기 때문에, SingletonComponent 로 설정해주었다.
그리고 retrofit 관련설정을하는 함수들을 생성해주는데,
retrofit은 외부 라이브러리이기 때문에 @Provides라는 어노테이션을 붙여준다
@Provides는 클래스가 외부 라이브러리에서 제공되거나, 클래스를 직접 소유하지 않은 경우, 또는 빌더패턴으로 인스턴스를 생성해야할 경우 작성해준다. 또한 @Provides는 return값으로 함수가 어떤 유형의 인스턴스를 제공하는지 Hilt에 알려주어야 한다
4. RepositoryModule 클래스 생성
@Module
@InstallIn(SingletonComponent::class) // 앱 전체에서 사용할 모듈이기 때문에 SingletonComponent로 설정
abstract class SearchRepositoryModule {
@Binds
@Singleton
abstract fun bindSearchRepository(searchRepositoryImpl: SearchRepositoryImpl) : SearchRepository
}
SearchRepositoryModule.kt
SearchRepositoryModule 역시 Module임을 알리는 @Module 어노테이션을 붙여주고,
앱 전체에서 사용할 모듈이기 때문에 SingletonComponent로 설정해주었다
인터페이스인 SearchRepository를 @Binds 어노테이션을 통해 Hilt가 의존성 객체를 생성할 수 있도록 하였다
@Binds 어노테이션은 인터페이스에 대해 종속성을 삽입할 때 사용하는 어노테이션이다
예를 들어 한 인터페이스를 두 개의 레포지토리가 상속받는 경우, Hilt는 어느 레포지토리를 선택해야 할지 혼란을 겪게 된다. 이럴 때 @Binds로 상속받은 인터페이스를 구분지을 수 있게 된다.
매개변수에는 구현체인 SearchRepositoryImpl을 넣어준다
주의할 점은 모듈이 추상클래스이면, 함수도 추상함수여야한다
5. 의존성 주입하기
안드로이드 앱 아키텍쳐(AAC) 구조에 따르면, 의존성 주입이 단방향으로 이루어진다.
- AppModule의 의존성을 Repository에 주입
- Repository 객체는 ViewModel에 주입
- 각 View에 ViewModel 객체를 주입
5-1. AppModule에서 만든 Retrofit 의존성을 Repository에 주입
// 레포지토리 구현부 (override해서 데이터구현부 실제구현)
// SearchRepository 상속받음
// 보통 데이터를 local, remote 영역으로 나눠서써줌 (지금예제에선 remote - remoteDataSource에 해당)
// @Inject constructor 추가 (remoteDataSource 파라미터로 Hilt주입시켜줌)
@Singleton
class SearchRepositoryImpl @Inject constructor (private val remoteDataSource : SearchRemoteDataSource) : SearchRepository {
// SearchRepository에서 구현한 getGitHubUserList함수를 오버라이딩
override suspend fun getGitHubUserList(q: String) =
remoteDataSource.getGitHubUser(name = q).toEntity()
}
SearchRepositoryImpl.kt
RepositoryImpl에 @Singleton을 붙여서 의존성 주입 가능한 스코프로 지정해준다
그리고 @Inject constructor() 를 붙여서, remoteDataSource에 Hilt를 주입해준다 (constructor 안에 있는 객체들은 Hilt가 주입하게 된다)
이제 주입받은 remoteDataSource를 이용해 알맞은 코드를 짜주면된다
5-2. Repository 의존성을 ViewModel에 주입
// ViewModel에서는 직접적으로 데이터에 접근하지 않고, Repository를 통해서 데이터접근해서 데이터호출 (private val searchRepository: SearchRepository)
// hilt 주입해줬기 때문에 Factory 안써줘도됨
@HiltViewModel
class SearchViewModel @Inject constructor(private val searchRepository: SearchRepository) : ViewModel(){
// LiveData 사용
private val _getGitHubUserList : MutableLiveData<List<GitHubUser>> = MutableLiveData()
val getGitHubUserList : LiveData<List<GitHubUser>> get() = _getGitHubUserList
fun getGitHubUserList(query : String) = viewModelScope.launch {
// "cindy" 검색값 붙잡고있음
_getGitHubUserList.value = searchRepository.getGitHubUserList(q = query).items
}
// 좋아요 상태값 관찰할 라이브데이터 세팅
private val _favoriteUserList : MutableLiveData<List<GitHubUser>> = MutableLiveData()
val favoriteUserList : LiveData<List<GitHubUser>> get() = _favoriteUserList
// 좋아요한 값만 가져오는 함수
fun setFavoriteItem(item : GitHubUser){
// gitHubUserList변수에 라이브데이터에 세팅된 값이 담김(cindy 검색값이 담김)
//toMutableList 수정가능 한 List로 변경
val gitHubUserList = _getGitHubUserList.value!!.toMutableList()
//매칭된 아이템의 index를 반환
val position = gitHubUserList.indexOfFirst {
it.id == item.id
}
// 좋아요 상태값 넣어줌
_getGitHubUserList.value =
//livedata에서 받아온 list를 index으로 sorting해서 data class copy함 (data class의 객체를 복사)
gitHubUserList.also {
it[position] = item.copy(
//bool 값을 반대값 세팅 (isFavorite이 ture이면 false로, false이면 true로)
isFavorite = !item.isFavorite
)
}
// 좋아요한 목록만(좋아요가 true인값만) 필터링해서 넣어줌!!
_favoriteUserList.value = gitHubUserList.filter {
it.isFavorite
}
}
}
SearchViewModel.kt
주입받을 대상이 ViewModel이기 때문에 @HiltViewModel을 어노테이션 붙여서 의존성 주입 가능한 스코프로 지정해준뒤,
아까와 마찬가지로 @Inject constructor()를 붙여줘서 Hilt를 주입해준다
원래는 ViewModelFactory를 사용해서 구현해줬었지만, Hilt를 사용해서 의존성을 주입해주면 ViewModelFactory를 사용할 필요가 없다
5-3. ViewModel 의존성을 View에 주입
Activity나 Fragment에서는 @AndroidEntryPoint를 사용해서 View를 의존성 주입 가능한 스코프로 지정해줘야한다
그리고 viewModel을 사용할 Activity나 Fragment에서는 viewModel을 선언해줘야할텐데 그때 viewModels()를 써줘야하기때문에 viewModels()를 불러오기위해 build.gradle(app)의 dependencies에 아래와 같이 의존성을 추가해준다
dependencies {
...
// viewModels
implementation (libs.androidx.activity.ktx)
implementation (libs.androidx.fragment.ktx)
}
build.gradle(app)
@AndroidEntryPoint
class GitHubSearchUsersFragment : Fragment() {
...
// searchViewModel 선언
private val searchViewModel: SearchViewModel by viewModels()
...
// 좋아요 상태값 observe해서, 좋아요가 ture인 값들만 observe
searchViewModel.favoriteUserList.observe(viewLifecycleOwner){
// sharedViewModel.setFavoriteList에 그 넘어온값(좋아요가 ture)인값을 넣어줌
sharedViewModel.setFavoriteList(it)
}
}
GitHubSearchUsersFragment.kt
@AndroidEntryPoint 어노테이션을 붙여주고,
private val searchViewModel: SearchViewModel by viewModels() 이런식으로 viewModel을 선언한다음, 필요한 부분에 불러와서 써주면된다
@AndroidEntryPoint
class GitHubFavorateUsersFragment : Fragment() {
...
}
GitHubFavorateUsersFragment.kt
@AndroidEntryPoint
class SearchMainActivity : AppCompatActivity() {
...
}
SearchMainActivity.kt
이런식으로 모든 Activity,Fragment에 @AndroidEntryPoint 어노테이션을 달아주면 끝이다!
# 참고자료
https://velog.io/@cksgodl/AndroidKotlin-Hilt%EA%B0%80-%EB%AD%90%EC%98%88%EC%9A%94
[Android/Kotlin] Hilt가 뭐예요?
Hilt가 뭘까요? Android Developer Hilt공식 홈페이지의 내용을 나만의 언어로 정리해 보자. > Hilt는 프로젝트의 모든 Android 클래스에 컨테이너를 제공하고 수명 주기를 자동으로 관리함으로써 애플리케
velog.io
https://velog.io/@mi-fasol/Kotlin-Android-DI
[Kotlin] Android DI와 Hilt
전에 스프링을 공부할 때 DI에 대해 작성한 적이 있었는데, 이번엔 안드로이드의 DI에 대한 이야기를 하려 한다. DI DI는 Dependency Injection의 약자로 의존성 주입을 뜻한다. 특정 한 객체가 다른 객체
velog.io
의존성 주입이란 무엇이며 왜 필요한가?
목표 의존성 주입이 무엇인지 이해한다. 의존성 주입이 왜 필요한지 이해한다. 의존성 주입이란? 의존성 주입이란 클래스간 의존성을 클래스 외부에서 주입하는 것을 뜻한다. 의존성 주입 그 자
kotlinworld.com
Dagger Hilt 내부코드 분석
Dagger Hilt가 생성하는 코드를 분석하여, Dagger Hilt가 안드로이드 앱에 의존성을 주입하는 과정을 이해해봅시다.
fornewid.medium.com
Dagger Hilt 내부코드 분석: Advanced
Dagger Hilt가 생성하는 코드를 분석하여, Dagger Hilt가 안드로이드 앱에 의존성을 주입하는 과정을 이해해봅시다.
fornewid.medium.com
https://jminie.tistory.com/182
안드로이드 [Kotlin] - 프로젝트에 의존성 주입(DI) 적용해보기 - Hilt
의존성 주입, 안드로이드에서의 의존성 주입, 그리고 안드로이드 의존성 주입 라이브러리인 Hilt에 대해서 지난번 포스팅에서 다뤘다. https://jminie.tistory.com/180 안드로이드 [Kotlin] - 의존성 주입(DI)
jminie.tistory.com
[공식문서]
https://developer.android.com/training/dependency-injection/hilt-android?hl=ko
Hilt를 사용한 종속 항목 삽입 | Android Developers
이 페이지는 Cloud Translation API를 통해 번역되었습니다. Hilt를 사용한 종속 항목 삽입 컬렉션을 사용해 정리하기 내 환경설정을 기준으로 콘텐츠를 저장하고 분류하세요. Hilt는 프로젝트에서 종속
developer.android.com
'개발 노트 > Kotlin' 카테고리의 다른 글
[Android/Kotlin] collect과 collectLatest의 차이점? [Flow] (0) | 2024.05.27 |
---|---|
[Android/Kotlin] Flow 란? (StateFlow, SharedFlow) (1) | 2024.05.26 |
JSON 보기편하게 변환 해주는 사이트 (0) | 2024.05.03 |
[Android/Kotlin] Google Map API 사용해서 구글 지도맵 만들기 (0) | 2024.05.01 |
[Android/Kotlin] 사용자 위치 얻기 (0) | 2024.05.01 |