https://developers.kakao.com/product/search
일단 카카오에서 제공하는 API를 사용할건데
위 링크에서 시작하기를 들어간다음 애플리케이션을 추가해 주고
REST API키를 받아준다.
다시 링크로 들어가서 문서 버튼을 누르면 이렇게 지원하는 기능을 보여준다.
https://developers.kakao.com/docs/latest/ko/daum-search/dev-guide#search-image
이런 식으로 되어 있는데 데이터 클래스를 만들때 참조할것이다
Retrofit을 이용하기 위해선 gradle에 라이브러리를 추가해야하는데
//by viewModels를 사용하기 위한 의존성
implementation ("androidx.activity:activity-ktx:1.7.2")
implementation ("androidx.fragment:fragment-ktx:1.6.1")
//retrofit
implementation ("com.google.code.gson:gson:2.10.1")
implementation ("com.squareup.retrofit2:retrofit:2.9.0")
implementation ("com.squareup.retrofit2:converter-gson:2.9.0")
implementation ("com.squareup.okhttp3:okhttp:4.10.0")
implementation ("com.squareup.okhttp3:logging-interceptor:4.10.0")
// coil
implementation("io.coil-kt:coil:1.4.0")
이런식으로 viewModels와 이미지를 불러올 coil 레트로핏, Gson등을 추가해준다
object KakaoApi {
private const val KAKAO_BASE_URL = "https://dapi.kakao.com/v2/search/"
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 kakaoRetrofit = Retrofit.Builder().baseUrl(KAKAO_BASE_URL).addConverterFactory(GsonConverterFactory.create()).client(
createOkHttpClient()
).build()
val kakaoNetwork: KakaoSearchService = kakaoRetrofit.create(KakaoSearchService::class.java)
}
서치부분만 쓸 생각이니 BASE_URL을 "https://dapi.kakao.com/v2/search/"로 설정해 줬다.
https://dapi.kakao.com/ 까지만 하고 인터페이스의 @Get에서 v2/search/를 추가해도 상관없다
베이스 URL을 만드어 줬으니 그 뒤에 올 URL을 만들어줄
val kakaoNetwork: KakaoSearchService = kakaoRetrofit.create(KakaoSearchService::class.java)의 KakaoSearchService인터페이스를 구축해주면 된다.
interface KakaoSearchService {
@GET("image") //카카오 이미지
suspend fun searchImage(
@Header("Authorization") apiKey: String,
@Query("query") query: String, //검색을 원하는 질의어
@Query("sort") sort: String, // 정렬 방식 accuracy(정확도순) 또는 recency(최신순)
@Query("page") page: Int, // 결과 페이지 번호, 1~50 사이의 값, 기본 값 1
@Query("size") size: Int// 한 페이지에 보여질 문서 수, 1~80 사이의 값, 기본 값 80
) : Response<ImageModel>
}
https://developers.kakao.com/docs/latest/ko/daum-search/dev-guide#search-image
다시 위에서 언급한 링크로 가면
요청부분이 이런식으로 설명된걸 볼수 있는데
어노테이션으로 메서드를 맞춰서 적어주면된다.
어떤느낌이냐면 정보를 받아올때 URL이
https://dapi.kakao.com/v2/search/image?query=[검색어]&sort=recency&page=1&size=80
이런식으로 구성되는데 여기서 변경가능한걸 정의 해준거다.
searchImage를 사용할때 query부분에 사용자가 적은 검색어를 넣어주고 page에 숫자를 적으면 다른페이지의 문서데이터를 보여주는등
BASE_URL과 달리 고정 되지 않은부분을 변경시킬 수 있게 이런식으로 만들어준것
API 주소 설정하는 것까진 알겠다
이젠 데이터를 받아와야하는데
받아올때 어떻게 받아올지 틀이 필요하다
이 데이터를 받아오는 틀은 문서에서 요구하는 사항과 같아야 한다.
문서를 다시 내려 보면
응답부분이 이런식으로 되어있다.
여기서 요청하는대로 모델을 만들어주면 된다
data class ImageModel(
val meta: Meta,
val documents: List<Document>
)
data class Meta(
val total_count: Int,
val pageable_count: Int,
val is_end: Boolean
)
data class Document(
val collection: String,
val thumbnail_url: String?,
val image_url: String,
val width: Int,
val height: Int,
val display_sitename: String,
val doc_url: String,
val datetime: String
)
이름이 햇갈리는데 내 스타일 대로 바꾸고싶다 하면 어노테이션을 이용해서 바꿔줄 수도 있다
Json에선 snake case를 써서 나타냈는데, 코틀린에선 carmel case로 나타내니
@SerialzedName으로 명시해주고 변경해줘야한다.
...
@SerializedName("thumnail_uri")
val thumnailUri: String,
마지막으로 데이터를 받고 북마크를 추가하는등의 기능을 넣어
내가 원하는대로 데이터를 저장할것이니 새로운 모델을 만들어준다
@Parcelize
data class SearchModel(
val image: Uri,
val title: String,
val type: String = "Image",
val site: String,
val date: String,
val doc_url: String,
var bookMark: Boolean = false
): Parcelable
이번에 리사이클러뷰에 쓸 아이템인데 UI에 맞춰서 필요한 데이터들을 지정해 줬다.
이제 서버로부터 데이터를 받아올 레포지토리를 만들어 줄 거다.
class ApiRepository {
private val searchList: MutableList<SearchModel> = mutableListOf()
private suspend fun searchImage(
query: String,
sort: String,
page: Int
): Response<ImageModel> {
return KakaoApi.kakaoNetwork.searchImage(
apiKey = "KakaoAK ${REST_API_KEY}",
query = query,
sort = sort,
page = page
)
}
suspend fun searchData(query: String, sort: String, page: Int): MutableList<SearchModel> {
val imageApi = searchImage(query, sort, page)
searchList.clear()
if (imageApi.isSuccessful) {
imageApi.body()?.documents?.map { document ->
SearchModel(
image = Uri.parse(document.image_url),
title = document.collection,
site = document.display_sitename,
date = dateTimeFormatter(document.datetime),
doc_url = document.doc_url
)
}?.let { searchList.addAll(it) }
}
return searchList
}
fun dateTimeFormatter(date: String): String {
val originalFormatter = DateTimeFormatter.ofPattern("yyyy-MM-dd'T'HH:mm:ss.SSSXXX")
val targetFormatter = DateTimeFormatter.ofPattern("yy-MM-dd HH-mm-ss")
val dateTime = LocalDateTime.parse(date, originalFormatter)
return dateTime.format(targetFormatter)
}
}
레포지토리에서 searchData를 사용하게 되면 searchImage에서 입력받은 값을 토대로 Json 데이터를 코틀린 객체로 return한다
KakaoApi에서 정의한
private val kakaoRetrofit = Retrofit.Builder().baseUrl(KAKAO_BASE_URL).addConverterFactory(GsonConverterFactory.create()).client(
createOkHttpClient()
).build()
로 데이터를 코틀린 객체로 받을 수 있다.
이렇게 반환된 데이터를 SearchModel형식으로 변형시킨뒤 searchList에 추가해주고 리턴한다
이제 레트로핏으로 서버에서 데이터를 받아오는 로직을 완성했으니
데이터를 보내고 (검색어), 받아서 UI에 구성하면 된다.
액티비티에선 검색창에 검색어를 프래그먼트로 보내주는 역할이 필요하다
private fun setSearch() = with(binding){
searchView.setOnQueryTextListener(object : SearchView.OnQueryTextListener,
androidx.appcompat.widget.SearchView.OnQueryTextListener {
override fun onQueryTextSubmit(query: String?): Boolean {
submitQuery(query)
return true
}
override fun onQueryTextChange(newText: String?): Boolean {
return false // 검색어 추천등 추가 하면 좋을 듯
}
})
}
private fun submitQuery(query: String?): Boolean {
return if (query == null) {
false
}else {
val bundle = Bundle().apply {
putString("searchQuery", query)
}
navController.navigate(R.id.nav_search_img, bundle)
true
}
}
전체 적인 UI는 바텀네비게이터를 사용해 액티비티 1 - 프래그먼트 2 로 구성했는데
검색한 목록을 띄워줄 프래그먼트 nav_search_img로 사용자가 submit 했을때 보내준다.
private fun searchQuery() {
arguments?.getString("searchQuery")?.let { query->
viewModel.searchItems(query)
}
}
이런식으로 값을 받았으니 뷰모델을 불러준다.
class SearchViewModel(private val repository: ApiRepository) : ViewModel() {
private val _itemList: MutableLiveData<MutableList<SearchModel>> = MutableLiveData()
val itemList: LiveData<MutableList<SearchModel>> get() = _itemList
private val _page: MutableLiveData<Int> = MutableLiveData(1)
val page: LiveData<Int> get() = _page
fun searchItems(query: String){
//launch는 기본적으로 메인 스레드 Dispatchers.Main에서 일어남
viewModelScope.launch(Dispatchers.IO) {
//데이터 불러오는 오래걸리는건 백그라운드 스레드에서 이뤄져야해서 명시해주기
val search = repository.searchData(query, "recency", 1)
withContext(Dispatchers.Main) {
_itemList.value = search.toMutableList()
}
}
}
}
class SearchViewModelFactory(
private val repository: ApiRepository
):ViewModelProvider.Factory {
override fun <T : ViewModel> create(modelClass: Class<T>): T {
if(modelClass.isAssignableFrom(SearchViewModel::class.java)) {
return SearchViewModel(repository) as T
}else throw IllegalArgumentException("Not found ViewModel class.")
}
}
파라미터로 ApiRepository를 받는다.
프래그먼트에서 query를 받았으니 레포지토리의 searchData를 이용해 데이터를 불러와야하는데
코루틴을 사용하고 launch(Dispatchers.IO)로 명시해줘서 백그라운드 스레드에서 이루어지게 해야한다
명시하지않으면 launch(Dispatchers.Main)으로 인지하게 되는데 데이터를 불러오는 작업은 백그라운드 스레드에서 이뤄져야 데이터를 원활하게 불러올 수 있기 때문에 꼭 명시해줘야 한다.
그런 다음 데이터를 불러왔으면 UI를 업데이트 해야될것이니
withContext(Dispatchers.Main)으로 백그라운드 스레드가 완료됬을때 다시 메인스레드로 돌아와 스레드를 전환해준다
다시 프래그먼트로 돌아와서
private fun setAdapter() {
_adapter = SearchAdapter()
binding.rcSearchGrid.adapter = adapter
binding.rcSearchGrid.layoutManager = StaggeredGridLayoutManager(2, StaggeredGridLayoutManager.VERTICAL)
viewModel.itemList.observe(viewLifecycleOwner){
adapter?.submitList(it)
}
}
itemList를 옵저빙 하고 리스트 어댑터의 submitList에 값을 넣어주면 된다.
(색 설정을 따로 안했을 뿐이지 내보관함 프래그먼트가 아니다!!, 자세히 보면 이미지 검색 프래그먼트를 눌렀다)
(오류가 있는데 데이터에서 제목을 설정하는 부분을 찾지 못했다. 웹문서 검색에서 찾아야 하나)
💡 리스트 어댑터의 레이아웃 매니저를 설정할때
일반 GridLayoutManger를 사용하면 1행에 이미지 2개가 고정이다
완성본처럼 이미지 크기에 맞춰 이런식으로 구성하려면 StaggeredGridLayoutManager를 이용하면 된다