와챠의 우당탕탕 코딩 일기장
[Android/Kotlin] Hilt 사용해보기 본문
data binding을 배우고나서,,,
ViewModel 생성 시 ViewModelFactory를 사용했는데,
이 ViewModelFactory의 코드가 참 복잡했다.
뭐를 생성하고 생성자에 넣고 또 생성자에 이거 넣어주고,,, 참 복잡하고 귀찮다!!
그래서 이런 의존성을 바탕으로 객체를 주입해주는 라이브러리인 Hilt를 사용해 이 코드를 수정해보려고 한다.
먼저 기존 코드를 보자
이건 ViewModel 생성 시 ViewModelFactory에서 ViewModel을 생성해주는 코드이다.
class HomeFragment: Fragment() {
private lateinit var binding: FragmentHomeBinding
// ViewModel 생성을 ViewModelFactory 클래스에 위임하는 부분
private val viewModel: HomeViewModel by viewModels { MyViewModelFactory(requireContext()) }
// 생략
}
ViewModelFactory의 코드는 아래와 같다.
class MyViewModelFactory(private val context: Context): ViewModelProvider.Factory {
override fun <T : ViewModel> create(modelClass: Class<T>): T {
return when {
// home
modelClass.isAssignableFrom(HomeViewModel::class.java) -> {
val repository = HomeRepository(HomeRemoteDataSource((AssetLoader(context))))
HomeViewModel(repository) as T
}
else -> {
throw IllegalArgumentException("Failed to create ViewModel: ${modelClass.name}")
}
}
}
}
repository를 생성하는 부분을 보면... 아래와 같은데
HomeViewModel(HomeRepository(HomeRemoteDataSource((AssetLoader(context)))))
HomeViewModel을 생성하기 위해 HomeRepository를 생성해서 넣어주고,
HomeRepositoty를 생성하기 위해 HomeRemoteDataSource를 생성해서 넣어주고,
HomeRemoteDataSource를 생성하기 위해 AssetLoader를 생성해서 넣어주고...
AseetLoader를 생성할 땐 또 context를 전달해주는 레전드 의존성을 가지고 있는 코드이다.
이런 복잡한 의존성을 Hilt를 사용해 간단하게 표현하고, 쉽게 생성해보려고 한다!!
시작하기 전에... Hilt란??
Hilt는 프로젝트에서 종속 항목 수동 삽입을 실행하는 상용구를 줄이는 Android용 종속 항목 삽입 라이브러리입니다.
-Android Developer
쉽게 말해 위처럼 복잡한 생성 방법을 가진 코드들을 Hilt 어노테이션을 사용해 자동으로 생성하도록 하는 라이브러리라는 것이다.
만들고자 하는 것:
Dog API를 사용해 fetch 버튼 누를 때마다 강아지 사진 보이기
전체 코드는 여기 GitHub에서 볼 수 있다.
목차는 아래와 같다. 생략된 것도 있음
<목차>
1. Hilt 라이브러리 및 기타 필요한 라이브러리 추가
2. @HiltAndroidApp 사용해 Application 상속한 클래스 생성하기
3. Manifest에 MyApplication 클래스 추가하기 + 인터넷 권한 추가
4. 서버 통신을 위한 RetrofitModuel 객체 생성
5. 받을 JSON 형식을 data class로 생성하기
7. DogService.kt 생성
7-1. RetrofitServiceModule 생성
8. DogDataSorce interfase 생성
9. DogDataSource를 상속받은 객체 생성
9-1. DataSourceModule 생성
10. DogRepository interface 생성
11. DogRepository를 상속받은 객체 생성
11-1. RepositoryModule 생성
12. ViewModel 생성
15. MainActivity에 데이터 요청하고 받아서 ImageView에 보이는 코드 짜기~~
1. Hilt 라이브러리 및 기타 필요한 라이브러리 추가 - app 단위 build.gradle에 아래 코드 추가
plugins {
id 'com.android.application'
id 'org.jetbrains.kotlin.android'
// 추가
id 'kotlin-kapt'
id 'dagger.hilt.android.plugin'
}
...
dependencies {
...
// Hilt
implementation "com.google.dagger:hilt-android:2.42"
kapt "com.google.dagger:hilt-android-compiler:2.42"
//Retrofit2
implementation "com.squareup.retrofit2:retrofit:2.9.0"
implementation "com.google.code.gson:gson:2.9.0"
implementation "com.squareup.retrofit2:converter-gson:2.9.0"
//okhttp3
implementation 'com.squareup.okhttp3:okhttp:5.0.0-alpha.2'
implementation 'com.squareup.okhttp3:logging-interceptor:4.9.1'
// ViewModel
implementation "androidx.fragment:fragment-ktx:1.5.2"
implementation 'androidx.activity:activity-ktx:1.5.1'
implementation "androidx.lifecycle:lifecycle-viewmodel-ktx:2.5.1"
//coroutine
implementation "org.jetbrains.kotlinx:kotlinx-coroutines-android:1.6.1"
implementation 'org.jetbrains.kotlinx:kotlinx-coroutines-core:1.6.1'
implementation 'androidx.lifecycle:lifecycle-livedata-ktx:2.5.1'
// Glide
implementation 'com.github.bumptech.glide:glide:4.12.0'
kapt 'com.github.bumptech.glide:compiler:4.12.0'
}
project 단위의 build.gradle에도 아래 코드 추가
plugins {
id 'com.android.application' version '7.2.1' apply false
id 'com.android.library' version '7.2.1' apply false
id 'org.jetbrains.kotlin.android' version '1.7.10' apply false
// 추가
id 'com.google.dagger.hilt.android' version '2.42' apply false
}
2. @HiltAndroidApp 사용해 Application 상속한 클래스 생성하기
@HiltAndroidApp : 애플리케이션 단위의 컴포넌트
package com.example.testhilt
import android.app.Application
import dagger.hilt.android.HiltAndroidApp
@HiltAndroidApp
class MyApplication: Application() {
override fun onCreate() {
super.onCreate()
}
}
3. Manifest에 MyApplication 클래스 추가하기 + 인터넷 권한 추가
<uses-permission android:name="android.permission.INTERNET"/>
<uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" />
<application
android:name=".MyApplication"
...
</application>
4. 서버 통신을 위한 RetrofitModuel 객체 생성
di>RetrofitModule.kt
package com.example.testhilt.di
import dagger.Module
import dagger.Provides
import dagger.hilt.InstallIn
import dagger.hilt.components.SingletonComponent
import okhttp3.Interceptor
import okhttp3.OkHttpClient
import retrofit2.Retrofit
import retrofit2.converter.gson.GsonConverterFactory
import java.util.concurrent.TimeUnit
import javax.inject.Inject
import javax.inject.Named
import javax.inject.Singleton
@Module
@InstallIn(SingletonComponent::class)
object RetrofitModule {
private const val BASE_URL = "https://dog.ceo/"
@Singleton
@Provides
fun providesOkHttpClient(): OkHttpClient =
OkHttpClient.Builder()
.connectTimeout(10, TimeUnit.SECONDS)
.writeTimeout(10, TimeUnit.SECONDS)
.readTimeout(10, TimeUnit.SECONDS)
.build()
@Provides
@Singleton
fun providesRetrofit(okHttpClient: OkHttpClient): Retrofit =
Retrofit.Builder()
.baseUrl(BASE_URL)
.client(okHttpClient)
.addConverterFactory(GsonConverterFactory.create())
.build()
}
@Module: Hilt 모듈
@Provides: 인스턴스 삽입
- 함수 반환 유형을 통해 이 함수가 어떤 유형의 인스턴스를 제공하는지 Hilt에 알려줌
- 함수 매개변수는 해당 유형의 종속 항목을 Hilt에 알려줌
- Hilt는 해당 유형의 인스턴스를 제공해야 할 때마다 함수 본문을 실행함.
@InstallIn: 모듈을 사용하거나 설치할 Android 클래스를 Hilt에 알림
- 서버 접근을 위한 객체는 항상 같아야 메모리 소모가 적기에 싱글톤 모듈 사용함
컴포넌트 구성 요소는 아래와 같음
컴포넌트에 모듈을 설치하면 이 컴포넌트 계층 구조에서 그 아래에 있는 하위 구성요소의 다른 결합의 종속 항목으로 설치된 모듈의 결합에 액세스할 수 있다...< 중요한듯
5. 받을 JSON 형식을 data class로 생성하기
data>remote>model>response>DogResponse.kt
package com.example.testhilt.data.remote.model.response
/*
{
"message": "https://images.dog.ceo/breeds/mexicanhairless/n02113978_147.jpg",
"status": "success"
}
*/
data class DogResponse(
val message: String,
val status: String
)
6. BaseResponse.kt 생성
data>remote>BaseResponse.kt
response 통일되게 받기 위한 데이터 클래스.....인데......여기선 딱히 필요 없음...
우리가 받은 json이 'message', 'success'이라서..
원래는 BaseResponse<DogResponse> 사용해서 받으려 했는데 'message', 'success'이게 똑같아서 BaseResponse로 json이 받아짐ㅋㅋㅋ 그냥 앞으로 나오는 BaseResponse<DogResponse>은 DogResponse이렇게 생각해도 될듯
수정할 시간이 없어서 패스
package com.example.testhilt.data.remote.model
data class BaseResponse<T>(
val data:T,
val status:String,
val message:String,
val success:Boolean
)
7. DogService.kt 생성
data>remote>service>DogService.kt
서버에 데이터 요청하고 response 받는 코드
package com.example.testhilt.data.remote.service
import com.example.testhilt.data.remote.model.BaseResponse
import com.example.testhilt.data.remote.model.response.DogResponse
import retrofit2.http.GET
interface DogService {
@GET("api/breeds/image/random")
suspend fun getDog(): BaseResponse<DogResponse>
}
7-1. RetrofitServiceModule 생성
위에서 생성한 DogService를 Hilt가 생성하게 만들어주는 코드임!!
provideDogService함수는 @Provides를 사용해 DogService를 생성해 반환해주는 함수이다.
참고로 매개변수를 보면 retrofit이 필요한데,
4번을 보면 우리가 @Provides를 사용해 Retrofit을 반환하는 providesRetrofit 함수 또한 만들어둔 것을 볼 수 있다.
providesRetrofit 함수의 반환값인 Retrofit이 provideDogService의 매개변수에 들어가는 것이다.
package com.example.testhilt.di
import com.example.testhilt.data.remote.service.DogService
import dagger.Module
import dagger.Provides
import dagger.hilt.InstallIn
import dagger.hilt.components.SingletonComponent
import retrofit2.Retrofit
import javax.inject.Singleton
@Module
@InstallIn(SingletonComponent::class)
object RetrofitServiceModule {
@Provides
@Singleton
fun provideDogService(retrofit: Retrofit): DogService =
retrofit.create(DogService::class.java)
}
8. DogDataSorce interfase 생성
data>remote>datasource>DogDataSource.kt
package com.example.testhilt.data.remote.datasource
import com.example.testhilt.data.remote.model.BaseResponse
import com.example.testhilt.data.remote.model.response.DogResponse
interface DogDataSource {
suspend fun getDog(): BaseResponse<DogResponse>
}
9. DogDataSource를 상속받은 객체 생성
data>remote>service>DogDataSourceImpl.kt
@Inject를 사용해서 @Provides로 생성한 DogService를 주입시킨다
덜덜
package com.example.testhilt.data.remote.datasource
import com.example.testhilt.data.remote.model.BaseResponse
import com.example.testhilt.data.remote.model.response.DogResponse
import com.example.testhilt.data.remote.service.DogService
import javax.inject.Inject
class DogDataSourceImpl @Inject constructor(private val dogService: DogService) : DogDataSource {
override suspend fun getDog(): BaseResponse<DogResponse> = dogService.getDog()
}
9-1. DataSourceModule 생성
@Binds는 인터페이스의 인스턴스를 제공해야 할 때 사용할 구현법을 Hilt에 알려주는 어노테이션이다.
- 함수 반환 유형을 통해 해당 함수가 어떤 인터페이스의 인스턴스를 제공하는지 Hilt에 알려준다.
- 함수 매개변수는 제공할 구현을 Hilt에 알려준다.
DogDataSource가 interface였기 때문에... Hilt야 DogSource interface는 이렇게 만들어라~해주기 위해 @Binds 어노테이션을 사용해준다.
package com.example.testhilt.di
import com.example.testhilt.data.remote.datasource.DogDataSource
import com.example.testhilt.data.remote.datasource.DogDataSourceImpl
import dagger.Binds
import dagger.Module
import dagger.hilt.InstallIn
import dagger.hilt.components.SingletonComponent
import javax.inject.Singleton
@Module
@InstallIn(SingletonComponent::class)
interface DataSourceModule {
@Binds
@Singleton
fun bindsDogDataSource(dataSourceImpl: DogDataSourceImpl): DogDataSource
}
10. DogRepository interface 생성
data>remote>repository>DogRepository.kt
package com.example.testhilt.data.remote.repository
import com.example.testhilt.data.remote.model.BaseResponse
import com.example.testhilt.data.remote.model.response.DogResponse
interface DogRepository {
suspend fun getDog(): BaseResponse<DogResponse>
}
11. DogRepository를 상속받은 객체 생성
data>remote>repository>DogRepositoryImpl.kt
@Inject를 사용해 DogdateSource 주입시키기
package com.example.testhilt.data.remote.repository
import com.example.testhilt.data.remote.datasource.DogDataSource
import com.example.testhilt.data.remote.model.BaseResponse
import com.example.testhilt.data.remote.model.response.DogResponse
import javax.inject.Inject
class DogRepositoryImpl @Inject constructor(
private val dataSource: DogDataSource
) : DogRepository {
override suspend fun getDog(): BaseResponse<DogResponse> = dataSource.getDog()
}
11-1. RepositoryModule 생성
package com.example.testhilt.di
import com.example.testhilt.data.remote.repository.DogRepository
import com.example.testhilt.data.remote.repository.DogRepositoryImpl
import dagger.Binds
import dagger.Module
import dagger.hilt.InstallIn
import dagger.hilt.components.SingletonComponent
import javax.inject.Singleton
@Module
@InstallIn(SingletonComponent::class)
interface RepositoryModule {
@Binds
@Singleton
fun bindsDogRepository(repository: DogRepositoryImpl): DogRepository
}
12. ViewModel 생성
@HiltViewModel 추가해주면 된다.
주입할 것들은 @Inject!!
package com.example.testhilt.ui.main.viewmodel
import android.util.Log
import androidx.lifecycle.LiveData
import androidx.lifecycle.MutableLiveData
import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope
import com.example.testhilt.data.remote.model.response.DogResponse
import com.example.testhilt.data.remote.repository.DogRepository
import dagger.hilt.android.lifecycle.HiltViewModel
import kotlinx.coroutines.launch
import javax.inject.Inject
@HiltViewModel
class MainViewModel @Inject constructor(
private val repository: DogRepository
) : ViewModel() {
private val _response = MutableLiveData<DogResponse>()
val response: LiveData<DogResponse> = _response
init {
fetchDogResponse()
}
private fun fetchDogResponse() = viewModelScope.launch {
kotlin.runCatching {
repository.getDog()
}.onSuccess {
_response.value = DogResponse(it.message, it.status)
}.onFailure {
Log.d("mmm get dog api fail", "${it.message}")
}
}
fun clickBtnFetch() {
fetchDogResponse()
}
}
13. BaseActivity 생성
package com.example.testhilt.ui.base
import android.os.Bundle
import androidx.annotation.LayoutRes
import androidx.appcompat.app.AppCompatActivity
import androidx.databinding.DataBindingUtil
import androidx.databinding.ViewDataBinding
abstract class BaseActivity<T : ViewDataBinding>(@LayoutRes val layoutRes: Int) :
AppCompatActivity() {
protected lateinit var binding: T
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
binding = DataBindingUtil.setContentView(this, layoutRes)
binding.lifecycleOwner = this@BaseActivity
}
}
14. ImageBindingAdapters.kt 생성
package com.example.testhilt.ui.util
import android.widget.ImageView
import androidx.databinding.BindingAdapter
import com.bumptech.glide.Glide
@BindingAdapter("imageUrl")
fun loadImage(view: ImageView, imageUrl: String?) {
if (!imageUrl.isNullOrEmpty()) {
Glide.with(view)
.load(imageUrl)
.into(view)
}
}
15. activity_main.xml에 fetch 버튼과 이미지 보일 공간 만들기
<?xml version="1.0" encoding="utf-8"?>
<layout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools">
<data>
</data>
<androidx.constraintlayout.widget.ConstraintLayout
android:layout_width="match_parent"
android:layout_height="match_parent"
tools:context=".ui.main.MainActivity">
<Button
android:id="@+id/btn_fetch"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:backgroundTint="#ffddee"
android:text="fetch!"
android:textColor="@color/black"
android:textSize="24sp"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent" />
<ImageView
android:id="@+id/tv_dog"
android:layout_width="0dp"
android:layout_height="0dp"
android:contentDescription="강아지 사진 보일 곳"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@id/btn_fetch" />
</androidx.constraintlayout.widget.ConstraintLayout>
</layout>
15. MainActivity에 데이터 요청하고 받아서 ImageView에 보이는 코드 짜기~~
Activity나 Fragment에는 @AndroidEntryPoint 어노테이션을 사용한다.
@AndroidEntryPoint는 프로젝트의 각 Android 클래스에 관한 개별 Hilt 컴포넌트를 생성한다.
package com.example.testhilt.ui.main
import android.os.Bundle
import androidx.activity.viewModels
import com.example.testhilt.R
import com.example.testhilt.data.remote.datasource.DogDataSourceImpl
import com.example.testhilt.data.remote.datasource.DogDataSourceImpl_Factory
import com.example.testhilt.data.remote.repository.DogRepository
import com.example.testhilt.data.remote.repository.DogRepositoryImpl
import com.example.testhilt.data.remote.service.DogService
import com.example.testhilt.databinding.ActivityMainBinding
import com.example.testhilt.ui.base.BaseActivity
import com.example.testhilt.ui.main.viewmodel.MainViewModel
import dagger.hilt.android.AndroidEntryPoint
@AndroidEntryPoint
class MainActivity : BaseActivity<ActivityMainBinding>(R.layout.activity_main) {
private val viewModel: MainViewModel by viewModels()
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
binding.vm = viewModel
// DogRepositoryImpl(DogDataSourceImpl(DogService()))
}
}
원래라면 주석에 있는 것처럼 DogRepository나 ViewModel을 생성했어야 하는데,
Hilt를 사용하니까 그럴 필요가 없어짐.!! 대박 간단,,,지금 눈물흐르는 중