API 2개를 같이 사용하는 법을 다뤄봤다.
카카오 API를 이용해서 검색어를 받아와서 이미지와 동영상 두가지를 한꺼번에 받아오려고 한다.
API2개사용
data class CombinedSearchItem(
val searchItem: Any, // 이미지 또는 동영상
val itemType: SearchItemType
)
enum class SearchItemType {
IMAGE,
VIDEO
}
받아올 아이템이 이미지와 영상, 이렇게 두가지가 있으니 두가지를 받아올 searchItem과 받아오는게 사진이라면 IMAGE, 동영상이라면 VIDEO로 넣어줄 열거형 클래스인 enum클래스도 하나 같이 만들어준다.
공식 홈페이지에서 나오는 요청과 응답에 맞춰 오타없이 받아와야한다.
//이미지 검색
data class SearchResponse(
@SerializedName("meta")
val searchMeta: SearchMeta,
@SerializedName("documents")
val searchDocument: MutableList<SearchDocument>?
)
data class SearchMeta(
val is_end: Boolean,
val pageable_count: Int,
val total_count: Int
)
data class SearchDocument(
val collection: String,
val datetime: String,
val display_sitename: String,
val doc_url: String,
val height: Int,
val image_url: String,
val thumbnail_url: String,
val width: Int,
var isLike: Boolean = false
)
//비디오 검색
data class SearchResponseVideo(
@SerializedName("meta")
val searchMeta: SearchMetaVideo,
@SerializedName("documents")
val searchDocument: MutableList<SearchDocumentVideo>?
)
data class SearchMetaVideo(
val is_end: Boolean,
val pageable_count: Int,
val total_count: Int
)
data class SearchDocumentVideo(
val title:String,
val url:String,
val datetime:String,
val play_time:Int,
val thumbnail:String,
val author:String,
var isLike: Boolean = false
)
두개 모두 형식에 맞게 가져와준다.
interface NetWorkInterface {
@GET(Contract.REQUEST_ADDRESS)
suspend fun getImageResults(
@Header("Authorization") authHeader: String,
@QueryMap param: HashMap<String, String>
): SearchResponse
@GET(Contract.REQUEST_ADDRESS_VIDEO)
suspend fun getVideoResults(
@Header("Authorization") authHeader: String,
@QueryMap param: HashMap<String, String>
): SearchResponseVideo
}

동영상을 예르들면 여기서 /search/vclip 이 @GET()안에 들어가야한다.
1개의 API를 가져올때와 같은 형식으로 사용해준다.
object NetWorkClient {
private fun createOkHttpClient(): OkHttpClient {
val interceptor = HttpLoggingInterceptor()
if (BuildConfig.DEBUG)
interceptor.level = HttpLoggingInterceptor.Level.BODY
else
interceptor.level = HttpLoggingInterceptor.Level.NONE
return OkHttpClient.Builder()
.connectTimeout(20, TimeUnit.SECONDS)
.readTimeout(20, TimeUnit.SECONDS)
.writeTimeout(20, TimeUnit.SECONDS)
.addNetworkInterceptor(interceptor)
.build()
}
private val combinedRetrofit = Retrofit.Builder()
.baseUrl(Contract.SERVICE_URL)
.addConverterFactory(GsonConverterFactory.create())
.client(createOkHttpClient())
.build()
val combinedNetWork: NetWorkInterface = combinedRetrofit.create(NetWorkInterface::class.java)
}
먼저 items를 선언해준다. 젤위에서 보여준 CombinedSearchItem형식으로 만든다.
var items = mutableListOf<CombinedSearchItem>()
다음으로 communityNetWork를 만들어준다.
@SuppressLint("NotifyDataSetChanged")
private fun communicateNetWork(param: HashMap<String, String>) = lifecycleScope.launch() {
val authKey = "KakaoAK ${Contract.API_KEY}"
//기존 검색기록은 삭제
items.clear()
val imageResponseData = NetWorkClient.combinedNetWork.getImageResults(authKey, param)
val videoResponseData = NetWorkClient.combinedNetWork.getVideoResults(authKey, param)
imageResponseData.searchDocument?.let {
items.addAll(it.map { CombinedSearchItem(it, SearchItemType.IMAGE) })
}
videoResponseData.searchDocument?.let {
items.addAll(it.map { CombinedSearchItem(it, SearchItemType.VIDEO) })
}
items.shuffle()
items.sortedByDescending {
when (it.itemType) {
SearchItemType.IMAGE -> (it.searchItem as SearchDocument).datetime
SearchItemType.VIDEO -> (it.searchItem as SearchDocumentVideo).datetime
}
}
searchAdapter = SearchAdapter(items)
binding.searchRecyclerView.adapter = searchAdapter
searchAdapter.notifyDataSetChanged()
searchAdapter.itemClick = object : SearchAdapter.ItemClick {
override fun onClick(item: CombinedSearchItem) {
val selectedThumbnail: String
val selectedSiteName: String
val selectedTime: String
when (item.itemType) {
SearchItemType.IMAGE -> {
val result = item.searchItem as SearchDocument
selectedThumbnail = result.thumbnail_url
selectedSiteName = result.display_sitename
selectedTime = result.datetime
result.isLike = !result.isLike
}
SearchItemType.VIDEO -> {
val result = item.searchItem as SearchDocumentVideo
selectedThumbnail = result.thumbnail
selectedSiteName = result.title
selectedTime = result.datetime
result.isLike = !result.isLike
}
}
val selectedItems = SelectedItem(selectedThumbnail, selectedSiteName, selectedTime)
if (SelectedItem.myLikeList.contains(selectedItems)) {
SelectedItem.myLikeList.remove(selectedItems)
} else {
SelectedItem.myLikeList.add(selectedItems)
}
}
}
}
이미지는 이미지형식으로, 동영상은 동영상 형식으로 가져와줬다.
shuffle()을 이용해서 이미지와 동영상을 모두 섞어줬고, datetime기준으로 정렬시켜줬다. 그후 어뎁터를 업데이트 해서 리사이클러뷰에 가져온 데이터를 뿌려줬다. onClick부분은 기존에 어뎁터에서 선택된 데이터에 대해 클릭이벤트를 만들어서, 클릭된 데이터를 따로 저장하는 로직인데, 바꾼 로직에 맞춰서 바꿔줬다.
다음으로는 setUpImageParameter를 만들어준다.
private fun setUpImageParameter(input: String, page:Int): HashMap<String, String> {
val authKey = "KakaoAK ${Contract.API_KEY}"
return hashMapOf(
"Authorization" to authKey,
"query" to input,
"sort" to "recency",
"page" to page.toString(),
"size" to "20"
)
}
가져오는건 이미지와 동영상, 두개지만, 두개의 타입은 같아서 하나의 함수로 만들어줬다.
위에껀 동영상, 아래는 이미즈를 가져올때 필요한 요청 파라미터이다. 필요한게 Authorization, Query, sort, page, size로 같다.
결과 페이지번호의 최대값과 보여질 문서의수의 최대값이 다르니 여기부분만 신경써서 사용하면 된다.
무한스크롤
자동검색구현
위와같이 사용하면, size를 20으로 설정해뒀기때문에 한페이지에 20개의 데이터가 들어온다. 이미지와 동영상을 받아오니 사진20개, 동영상20개 총 40개의 데이터가 리사이클러뷰에 들어오게 된다.
이때 스크롤을 제일 아래까지 내리면 더이상 새로운 검색된내용을 로딩할수 없게된다. 이때 자동으로 다음페이지로 넘어가서 무한스크롤을 구현하려한다.
searchRecyclerView.addOnScrollListener(object : RecyclerView.OnScrollListener() {
override fun onScrolled(recyclerView: RecyclerView, dx: Int, dy: Int) {
super.onScrolled(recyclerView, dx, dy)
val layoutManager = recyclerView.layoutManager as GridLayoutManager
val visibleItemCount = layoutManager.childCount //화면에보이는 아이템개수
val totalItemCount = layoutManager.itemCount //어뎁터에설정된 전체아이템개수
val firstVisibleItemPosition = layoutManager.findFirstVisibleItemPosition() //현재화면에 첫번째로보이는 아이템의 포지션
val isLastItemVisible = firstVisibleItemPosition + visibleItemCount >= totalItemCount //마지막아이템이 화면에 보이는지
if (isLastItemVisible) {
loadNextPage()
}
}
})
리사이클러뷰 어뎁터의 addOnScrollLister안에서 onScrolled를 오버라이딩시켜준다.
그리고 화면에보이는 아이템의 개수, 전체 아이템 개수를 구해서 마지막 아이템이 화면에 보이는지 확인해서 보인다면 loadNextPage()함수를 불러와준다.
private var currentPage = 1
...
private fun loadNextPage(){
currentPage++
val searchEdit = binding.searchEdit.text.toString()
if (searchEdit.isNotBlank()) {
val searchParam = setUpImageParameter(searchEdit, currentPage)
communicateNetWork(searchParam)
}
}
currentPage를 1 올려주고, 현재 검색된 값을 가져와서 다시한번 검색해주면 된다.