1. 개요
기존 코드에서는 프래그먼트에서 레트로핏을 통해 api를 받아오는 작업을 수행하였다.
하지만 이는 완전한 데이터 분리와는 먼 방식이며
viewmodel에서 해당 방식을 적용하는게 맞다.
하지만 리포지토리를 만든다면 data layer를 분리해줌으로 써 한 번 더 데이터 층을 나눌 수 있다.
2. retrofit
1. 인터페이스
interface RetrofitInterface {
@GET("v2/search/image")
suspend fun searchImage(
@Query("query") query: String,
@Query("sort") sort:String,
@Query("size") size: Int
): ResultImgModel
@GET("v2/search/vclip")
suspend fun searchVideo(
@Query("query") query: String?,
@Query("sort") sort:String,
@Query("size") size: Int
) : ResultVideoModel
}
2. 클라이언트
package com.example.assignmnet_img.search.retrofit
import com.example.assignmnet_img.search.SearchFragment
import com.example.assignmnet_img.unit.Unit.API_KEY
import com.example.assignmnet_img.unit.Unit.BASE_URL
import okhttp3.OkHttpClient
import okhttp3.Request
import retrofit2.Retrofit
import retrofit2.converter.gson.GsonConverterFactory
object RetrofitClient {
val api : RetrofitInterface get() = instance.create(RetrofitInterface::class.java)
private val instance: Retrofit
get() {
val httpClient = OkHttpClient.Builder().addInterceptor { chain ->
val request: Request = chain.request()
.newBuilder()
.addHeader("Authorization", "KakaoAK ${API_KEY}")
.build()
chain.proceed(request)
}.build()
return Retrofit.Builder()
.baseUrl(BASE_URL)
.addConverterFactory(GsonConverterFactory.create())
.client(httpClient)
.build()
}
}
3. repository
interface
package com.example.assignmnet_img.search.viewmdoel.repsitory
import com.example.assignmnet_img.search.dataclass.SearchModel
interface SearchRepository {
suspend fun getSearchedImages(text: String): List<SearchModel>
suspend fun getSearchVideos(text: String): List<SearchModel>
}
데이터
package com.example.assignmnet_img.search.viewmdoel.repsitory
import com.example.assignmnet_img.search.dataclass.SearchModel
import com.example.assignmnet_img.search.retrofit.RetrofitClient
class SearchRepositoryImpl(
private val client: RetrofitClient
) : SearchRepository {
//데이터 영역
override suspend fun getSearchedImages(text: String): List<SearchModel> {
val responseImages = client.api.searchImage(text, "recency", 20)
val responseList = responseImages.documents
val resultList = responseList.map { document ->
SearchModel(
Url = document.image_url,
label = "[IMG]",
title = document.display_sitename,
datetime = document.datetime
)
}
return resultList
}
override suspend fun getSearchVideos(text: String): List<SearchModel> {
val responseVideos = client.api.searchVideo(text, "recency", 20)
val responseList = responseVideos.documents
val resultList = responseList.map { document ->
SearchModel(
Url = document.thumbnail,
label = "[Video]",
title = document.title,
datetime = document.datetime,
)
}
return resultList
}
}
변경된 뷰 모델
package com.example.assignmnet_img.search.viewmdoel
import android.content.Context
import android.widget.EditText
import androidx.lifecycle.LiveData
import androidx.lifecycle.MutableLiveData
import androidx.lifecycle.ViewModel
import androidx.lifecycle.ViewModelProvider
import androidx.lifecycle.viewModelScope
import com.example.assignmnet_img.bookmark.provider.SharedProvider
import com.example.assignmnet_img.bookmark.provider.SharedProviderImpl
import com.example.assignmnet_img.search.dataclass.SearchModel
import com.example.assignmnet_img.search.retrofit.RetrofitClient
import com.example.assignmnet_img.search.viewmdoel.repsitory.SearchRepository
import com.example.assignmnet_img.search.viewmdoel.repsitory.SearchRepositoryImpl
import kotlinx.coroutines.launch
class SearchViewModel(
private val contextProvider: SharedProvider,
private val repository: SearchRepository
) : ViewModel() {
private val _searchList: MutableLiveData<List<SearchModel>> = MutableLiveData()
val searchList: LiveData<List<SearchModel>> get() = _searchList
val searchText: MutableLiveData<String> = MutableLiveData()
private fun findItem(item: SearchModel?): SearchModel? {
val currentList = searchList.value.orEmpty()
if (item != null) {
return currentList.find {
it.datetime == item.datetime && //혹시 모를 고유 아이디 부여가 꼬일 시 방어 기제로 상세한 비교
it.Url == item.Url &&
it.title == item.title
}
}
return null
}
private fun clearList() {
val currentList = searchList.value.orEmpty().toMutableList()
currentList.clear()
_searchList.value = currentList
}
private fun findIndex(item: SearchModel?): Int {
val currentList = searchList.value.orEmpty().toMutableList()
val findItem = findItem(item)
return currentList.indexOf(findItem)
}
private fun sortList(baseList: MutableList<SearchModel>): List<SearchModel> {
val sortedList = baseList.sortedByDescending { it.datetime }
return sortedList
}
suspend fun doSearch(keyword: String) {
clearList()
viewModelScope.launch {
val resultImagesList = repository.getSearchedImages(keyword)
val resultVideosList = repository.getSearchVideos(keyword)
val currentList = searchList.value.orEmpty().toMutableList().apply {
addAll(resultImagesList)
addAll(resultVideosList)
}
val sortedList = sortList(currentList)
_searchList.value = sortedList
}
}
fun updateItem(item: SearchModel?) {
if (item == null) return
val currentList = searchList.value.orEmpty().toMutableList()
val index = findIndex(item)
currentList[index] = item
_searchList.value = currentList
}
fun compareUpdateItem(item: SearchModel?) {
if (item == null) return
if (findItem(item) == null) return
else updateItem(item)
}
fun updateText(text: String) {
searchText.value = text
}
fun saveSearchText(text: String) {
val sharedPrf = contextProvider.getSharedPreferences("name_search_text")
val edit = sharedPrf.edit().apply {
putString("key_search_text", text)
apply()
}
}
fun loadSearchText(view: EditText) {
val sharedPrf = contextProvider.getSharedPreferences("name_search_text")
val text = sharedPrf.getString("key_search_text", "")
view.setText(text)
}
}
class SearchViewModelFactory(
private val context: Context
) : ViewModelProvider.Factory {
override fun <T : ViewModel> create(modelClass: Class<T>): T {
if (modelClass.isAssignableFrom(SearchViewModel::class.java)) {
return SearchViewModel(
SharedProviderImpl(context),
SearchRepositoryImpl(RetrofitClient)
) as T
} else {
throw IllegalArgumentException("Not found ViewModel class.")
}
}
}
1. retorfit은 비동기 작동을한다.
2. 비동기 작업을 원활히 하기 위해서는 코루틴을 사용하게된다. -> suspend fun
3. suspend fun을 사용함으로써 retrofit의 원활한 사용이 가능해진다!
리포지토리를 사용하면서 주의할 점은 리포지토리는 비지니스 모델을 분리하기 위함이므로 라이브 데이터를 직접 사용하지 않는다. 즉 리포지토리 안에 라이브 데이터를 선언해서 사용할 수는 있으나, 이는 데이터를 관리하는 뷰모델에서 조정하는 것이 맞다.
또한 인터페이스를 통해 구현되는 메소드들은 합칠 수 있으면 합쳐주어 간소화 시키는 것 역시 중요하다.
한 눈에 보기 편한 코드포맷을 짜도록 노력해봐야한다.
따라서 레트로핏의 인터페이스에서는 콜이 아닌 직접적인 클래스를 사용하기 위해 suspend fun을 사용해주었으며
suspend fun을 사용함으로써 비동기 작업을 좀 더 직관적으로 꾸릴 수 있게 되었다.
또한 리포지토리를 사용함으로써 데이터 레이아웃 층을 나누었으며 여기서 더 나아가 퍼사드 패턴을 사용하여 도메인층까지 분리가 되었다.
이에 대한 개념은 좀 더 이해가 필요할 것이다.
참고 사이트
[Android] MVVM 패턴 Retrofit 가이드 Kotlin
Android MVVM 패턴 Retrofit 가이드
velog.io
https://nuritech.tistory.com/16
[Kotlin] Coroutine suspend function 은 대체 뭐야?
목차 suspend 는 무엇인가. 사전을 찾아보면, '중지하다' 라는 뜻의 단어다. 그렇다면, coroutine 에서의 suspend keyword 는 무엇을 의미할까? a function that could be started, paused, and resume. 시작하고, 멈추고,
nuritech.tistory.com
https://refactoring.guru/ko/design-patterns/facade
퍼사드 패턴
/ 디자인 패턴들 / 구조 패턴 퍼사드 패턴 다음 이름으로도 불립니다: Facade 의도 퍼사드 패턴은 라이브러리에 대한, 프레임워크에 대한 또는 다른 클래스들의 복잡한 집합에 대한 단순화된 인터
refactoring.guru
'개발노트 > Kotlin' 카테고리의 다른 글
12주차 3일 무한스크롤, 자바 라이브러리의 null예외처리 (1) | 2023.09.27 |
---|---|
12주차 1일 horizontal recyclerview (0) | 2023.09.25 |
11주차 3일 sharedprfernce (0) | 2023.09.20 |
11주차 2일 키 리스너 (0) | 2023.09.19 |
11주차 1일 api숨기 (0) | 2023.09.18 |