24.01.24 Retrofit으로 이미지 검색

KSang·2024년 1월 24일
0

TIL

목록 보기
39/101

카카오 API 사용준비

https://developers.kakao.com/product/search
일단 카카오에서 제공하는 API를 사용할건데

imageimage

위 링크에서 시작하기를 들어간다음 애플리케이션을 추가해 주고

REST API키를 받아준다.

image

다시 링크로 들어가서 문서 버튼을 누르면 이렇게 지원하는 기능을 보여준다.

https://developers.kakao.com/docs/latest/ko/daum-search/dev-guide#search-image

image

이런 식으로 되어 있는데 데이터 클래스를 만들때 참조할것이다

Retrofit

Gradle

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등을 추가해준다

Retrofit 객체 생성

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인터페이스를 구축해주면 된다.

KakaoSearchService.kt

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
다시 위에서 언급한 링크로 가면

image

요청부분이 이런식으로 설명된걸 볼수 있는데

어노테이션으로 메서드를 맞춰서 적어주면된다.

어떤느낌이냐면 정보를 받아올때 URL이

https://dapi.kakao.com/v2/search/image?query=[검색어]&sort=recency&page=1&size=80

이런식으로 구성되는데 여기서 변경가능한걸 정의 해준거다.

searchImage를 사용할때 query부분에 사용자가 적은 검색어를 넣어주고 page에 숫자를 적으면 다른페이지의 문서데이터를 보여주는등

BASE_URL과 달리 고정 되지 않은부분을 변경시킬 수 있게 이런식으로 만들어준것

ImageModel

API 주소 설정하는 것까진 알겠다

이젠 데이터를 받아와야하는데

받아올때 어떻게 받아올지 틀이 필요하다

이 데이터를 받아오는 틀은 문서에서 요구하는 사항과 같아야 한다.

문서를 다시 내려 보면

image

응답부분이 이런식으로 되어있다.

여기서 요청하는대로 모델을 만들어주면 된다

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,

SearchModel

마지막으로 데이터를 받고 북마크를 추가하는등의 기능을 넣어

내가 원하는대로 데이터를 저장할것이니 새로운 모델을 만들어준다

@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

image

이번에 리사이클러뷰에 쓸 아이템인데 UI에 맞춰서 필요한 데이터들을 지정해 줬다.

Repository

이제 서버로부터 데이터를 받아올 레포지토리를 만들어 줄 거다.

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에 추가해주고 리턴한다

Activity

이제 레트로핏으로 서버에서 데이터를 받아오는 로직을 완성했으니

데이터를 보내고 (검색어), 받아서 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 했을때 보내준다.

SearchFragment.kt

    private fun searchQuery() {
        arguments?.getString("searchQuery")?.let { query->
            viewModel.searchItems(query)
        }
    }

이런식으로 값을 받았으니 뷰모델을 불러준다.

SearchViewModel

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)으로 백그라운드 스레드가 완료됬을때 다시 메인스레드로 돌아와 스레드를 전환해준다

SearchFragment.kt

다시 프래그먼트로 돌아와서

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에 값을 넣어주면 된다.

image

(색 설정을 따로 안했을 뿐이지 내보관함 프래그먼트가 아니다!!, 자세히 보면 이미지 검색 프래그먼트를 눌렀다)
(오류가 있는데 데이터에서 제목을 설정하는 부분을 찾지 못했다. 웹문서 검색에서 찾아야 하나)

💡 리스트 어댑터의 레이아웃 매니저를 설정할때
일반 GridLayoutManger를 사용하면 1행에 이미지 2개가 고정이다
완성본처럼 이미지 크기에 맞춰 이런식으로 구성하려면 StaggeredGridLayoutManager를 이용하면 된다

0개의 댓글