[Android / Kotlin] Hilt 의존성 작성하기에 이어 Hilt 의존성을 주입 하는 부분도 정리해보고자 한다.
Hilt를 접하기 전 낯설기도 하고 어려워 보여 겁을 먹었는데 예상대로 복잡하고 어렵기는 하지만..🤯 내용을 정리하며 조금은 이해할 수 있게 된 것 같다. 백프로 이해할 수 있게 틈틈이 정리한 내용을 보며 의존성을 주입하고 어노테이션을 추가하는 부분을 익숙해지게 만들어야할 것 같다 🥹
앱 모듈의 의존성을 ImageSearchRepository에 주입하고 ImageSearchRepository 인스턴스는 ViewModel에 주입한다. 그리고, 각 Fragment에 ViewModel 인스턴스를 주입하면 된다.
ImageSearchRepositoryImpl에 @Singleton을 붙여서 의존성 주입 가능한 스코프로 지정해주고 @Inject constructor를 추가해주면 이 안의 객체들은 Hilt가 주입하게 된다.
➡️ 다시 정리해보면 ! 클래스 생성자에서 @Inject를 사용하여 의존성을 주입 받고 있다. 주입되는 의존성으로는 KaKaoSearchApi와 SharedPreferences가 있다.
@Singleton
class ImageSearchRepositoryImpl @Inject constructor(
private val api: KaKaoSearchApi,
private val sharedPreferences: SharedPreferences
) : ImageSearchRepository {
// PagingSource도 Repository로부터 Retrofit 객체를 전달 받도록 수정한다.
override fun searchImagePaging(query: String, sort: String): Flow<PagingData<SearchModel>> {
val pagingSourceFactory = { ImageSearchPagingSource(api, query, sort) }
return Pager(
config = PagingConfig(
pageSize = MAX_SIZE_IMAGE,
enablePlaceholders = false,
maxSize = MAX_SIZE_IMAGE * 3
),
pagingSourceFactory = pagingSourceFactory
).flow
}
/*
* 이미지 검색 화면에서 이미지를 클릭하면 보관함에 저장하기 위한 함수
* id를 비교해서 존재하지 않을 경우에만 아이템을 추가한다.
*/
override suspend fun saveStorageItem(searchModel: SearchModel) {
val favoriteItems = getPrefsStorageItems().toMutableList()
val findItem = favoriteItems.find { it.id == searchModel.id }
if (findItem == null) {
favoriteItems.add(searchModel)
savePrefsStorageItems(favoriteItems)
}
}
/*
* 보관함에 저장된 이미지를 삭제하기 위한 함수
* 해당 아이템이 보관함에 존재하면 아이템을 삭제한다.
*/
override suspend fun removeStorageItem(searchModel: SearchModel) {
val favoriteItems = getPrefsStorageItems().toMutableList()
favoriteItems.removeAll { it.id == searchModel.id }
savePrefsStorageItems(favoriteItems)
}
// 보관함에 저장되어 있는 아이템을 리스트 목록으로 가져온다.
override suspend fun getStorageItems(): List<SearchModel> {
return getPrefsStorageItems()
}
private fun getPrefsStorageItems(): List<SearchModel> {
val jsonString = sharedPreferences.getString(Constants.STORAGE_ITEMS, "")
return if (jsonString.isNullOrEmpty()) {
emptyList()
} else {
// Gson()을 사용하여 Json 문자열을 SearchModel 객체로 변환
Gson().fromJson(jsonString, object : TypeToken<List<SearchModel>>() {}.type)
}
}
// SearchModel 객체 아이템을 Json 문자열로 변환한 후 저장
private fun savePrefsStorageItems(items: List<SearchModel>) {
val jsonString = Gson().toJson(items)
sharedPreferences.edit().putString(Constants.STORAGE_ITEMS, jsonString).apply()
}
//검색 키워드 저장
override suspend fun saveSearchData(searchWord: String) {
sharedPreferences.edit {
putString(Constants.SEARCH_WORD, searchWord)
}
}
// 검색 키워드 불러오기
override suspend fun loadSearchData(): String? =
sharedPreferences.getString(Constants.SEARCH_WORD, "")
}
class ImageSearchPagingSource(
private val api: KaKaoSearchApi, // 의존성 주입
private val query: String,
private val sort: String
) : PagingSource<Int, SearchModel>() {
override val keyReuseSupported: Boolean = true
override fun getRefreshKey(state: PagingState<Int, SearchModel>): Int? {
return state.anchorPosition?.let { anchorPosition ->
state.closestPageToPosition(anchorPosition)?.prevKey?.plus(1)
?: state.closestPageToPosition(anchorPosition)?.nextKey?.minus(1)
}
}
override suspend fun load(params: LoadParams<Int>): LoadResult<Int, SearchModel> {
return try {
val pageNumber = params.key ?: STARTING_PAGE_INDEX
val response = api.searchImage(query, sort, pageNumber, params.loadSize)
val endOfPaginationReached = response.metaData?.isEnd!!
val data = response.documents?.map { imageDocument ->
SearchModel(
thumbnailUrl = imageDocument.thumbnailUrl,
siteName = imageDocument.displaySiteName,
datetime = imageDocument.dateTime,
itemType = SearchListType.IMAGE
)
} ?: emptyList()
val prevKey = if (pageNumber == STARTING_PAGE_INDEX) null else pageNumber - 1
val nextKey = if (endOfPaginationReached) {
null
} else {
val calculatedNextKey = pageNumber + 1
calculatedNextKey
}
LoadResult.Page(
data = data,
prevKey = prevKey,
nextKey = nextKey
)
} catch (exception: IOException) {
LoadResult.Error(exception)
}
}
}
@HiltViewModel을 붙여서 SearchViewModel를 의존성 주입 가능한 스코프로 만들어주고 @Inject constructor를 이용해서 모듈에서 만들어준 Repository 객체를 주입한다.
🫨 다음의 코드를 정리해보면, SearchViewModel은 ImageSearchRepository에 대한 의존성을 갖고 있다. Dagger Hilt는 @HiltViewModel를 통해 의존성을 주입하게 되며 ViewModel을 생성할 때 Dagger Hilt가 알아서 필요한 의존성을 해결해준다.
@HiltViewModel // ViewModel에 대한 의존성 주입을 단순화 하는데 사용된다.
class SearchViewModel @Inject constructor(
private val imageSearchRepository: ImageSearchRepository
) : ViewModel() {
...
}
Fragment에 @AndroidEntryPoint를 붙여서 의존성 주입 가능한 스코프로 만들어주고 by viewModels로 ViewModel을 생성한다.
ViewModel이 Delegate에 의해서 만들어지기 때문에 ViewModelFactory가 필요없게 된다.
@AndroidEntryPoint // Hilt가 해당 클래스에 대해 의존성 주입을 처리하도록 지시한다.
class SearchListFragment : Fragment() {
companion object {
fun newInstance() = SearchListFragment()
}
private var _binding: FragmentSearchBinding? = null
private val binding get() = _binding!!
private val viewModel: SearchViewModel by viewModels ()
...
}
@AndroidEntryPoint // Dagger Hilt를 사용하여 의존성을 주입
class MainActivity : AppCompatActivity() {
private val binding by lazy {
ActivityMainBinding.inflate(layoutInflater)
}
...
}