(Android) #5 SearchView로 검색바 구현 #2

  • by

검색어를 입력한 후 관련 검색어를 받아야 합니다.

다음은 관련 쿼리 데이터를 받는 쿼리입니다.

https://suggestqueries-clients6.youtube.com/complete/search?client=youtube-reduced&hl=en&gs_ri=youtube-reduced&ds=yt&cp=3&gs_id=100&q=(원하는 검색어)&xhr=t&xssi=t&gl=usBash

json 형식으로 페이지에 표시되는 일반 형식과 달리 txt 파일로 다운로드됩니다.

Retrofit2를 사용하여 데이터를 가져와야 합니다.

먼저 관련 검색어에 대한 Retrofit 객체를 정의했습니다.

object RetrofitSuggestionKeyword {
    var instance: Retrofit? = null
    private const val BASE_URL = "https://suggestqueries.google.com/complete/"
    fun initRetrofit(): Retrofit {
        if(instance == null) { 
            instance = Retrofit
                .Builder()
                .baseUrl(BASE_URL)
                .addConverterFactory(GsonConverterFactory.create()) 
                .build()
        }
        return instance!
!
} }

그런 다음 Retrofit Service interface를 만듭니다.

쿼리를 받으면 txt 파일로 받으므로 DTO 모델 클래스를 어떻게 정의하는지 궁금했습니다.

원시 데이터 그대로 받으면 좋은 것이 아닐까 생각되어, 에러 없이 출력이 되는 것을 확인할 수 있었습니다.

Call 에서 반환 유형을 설정했습니다.

interface RetrofitService {
    @GET("search")
    fun getSuggestionKeyword(@Query("client") client: String,
                             @Query("ds") ds: String,
                             @Query("q") q: String
    ): Call<ResponseBody>
}

activity 내에서 retrofit 객체를 만들고 데이터를 받는 함수를 정의했습니다.

private fun getSuggestionKeyword(newText: String){
    val retrofit = RetrofitSuggestionKeyword.initRetrofit()
    retrofit.create(RetrofitService::class.java).getSuggestionKeyword("firefox","yt",newText)
        .enqueue(object : Callback<ResponseBody> {
            override fun onResponse(call: Call<ResponseBody>, response: Response<ResponseBody>) {
                Log.d("검색어 결과","${response.body()?.string()!
!
}") } override fun onFailure(call: Call<ResponseBody>, t: Throwable) { Log.d("실패!
","!
") } }) }

인수 newText 는 검색어를 의미합니다.

다음은 검색어가 “쿠키”일 때 onResponse의 로그 기록입니다.

D/검색어 결과: ("cookie",("cookie","cookie \uB17C\uB780","cookie \uAC00\uC0AC","cookie \uB178\uB798\uBC29","cookie \uB274\uC9C4\uC2A4","cookie run kingdom","cookie inst","cookie \uC548\uBB34","cookie reaction","cookie remix"),(),{"google:suggestsubtypes":((512,433),(512),(512),(512),(512,433),(512,433),(512),(512),(512,433),(512,433,131))})

유니코드가 되어 있으므로 이것을 변환해 주는 함수를 가져왔습니다.

private fun convertStringUnicodeToKorean(data: String): String {
    val sb = StringBuilder() // 단일 쓰레드이므로 StringBuilder 선언
    var i = 0
    /**
     * \uXXXX 로 된 아스키코드 변경
     * i+2 to i+6 을 16진수의 int 계산 후 char 타입으로 변환
     */
    while (i < data.length) {
        if (data(i) == '\\' && data(i + 1) == 'u') {
            val word = data.substring(i + 2, i + 6).toInt(16).toChar()
            sb.append(word)
            i += 5
        } else {
            sb.append(data(i))
        }
        i++
    }
    return sb.toString()
}

변환 후 쿼리의 결과입니다.

))}'
("(cookie)",(("cookie newjeans",0,(229,433)),
("cookie haerin",0,(229)),("cookie swirl c",0,(512,433,131)),
("cookie 논란",0,(512,203)),("cookie 가사",0,(512,203)),
("cookie 노래방",0,(512,203)),("cookie 뉴진스",0,(512,203)),
("cookie inst",0,(512,203)),("cookie run kingdom",0,(512,203)),
("cookie 안무",0,(512,203))),{"j":"100","k":1,"q":"vHT1uSYBNMbgvuN3NhuF0iqyhKU"}

json 형식과 달리 각 키워드에 () 표시가 붙어 있고 여러 숫자가 추가되었습니다.

내가 필요한 정보는 단지 키워드이기 때문에 그것을 변환하는 함수를 만들었습니다.

val suggestionKeywords = ArrayList<String>() // 전역변수로 설정

/**
문자열 정보가 이상하게 들어와 알맞게 나눠주고 리스트에 추가
 **/
private fun addSubstringToSuggestionKeyword(splitList: List<String>){
    for (index in splitList.indices){
        if (splitList(index).isNotEmpty()){
            if (splitList(index)(splitList(index).length-1) == ')')
                suggestionKeywords.add(splitList(index).substring(1, splitList(index).length-2))
            else
                suggestionKeywords.add(splitList(index).substring(1, splitList(index).length-1))
        }
    }
}

suggestionKeywords는 연관 쿼리를 저장하는 목록입니다.

이 목록은 관련 검색어를 재활용 보기에 표시합니다.

완전한 코드입니다.

private fun getSuggestionKeyword(newText: String){
    val retrofit = RetrofitSuggestionKeyword.initRetrofit()
    retrofit.create(RetrofitService::class.java).getSuggestionKeyword("firefox","yt",newText)
        .enqueue(object : Callback<ResponseBody> {
            override fun onResponse(call: Call<ResponseBody>, response: Response<ResponseBody>) {
                suggestionKeywords.clear()
                val responseString = convertStringUnicodeToKorean(response.body()?.string()!
!
) val splitBracketList = responseString.split('(') val splitCommaList = splitBracketList(2).split(',') if (splitCommaList(0) !
= "))" && splitCommaList(0) !
= '"'.toString()){ addSubstringToSuggestionKeyword(splitCommaList) } searchSuggestionKeywordAdapter.submitList(suggestionKeywords.toMutableList()) } override fun onFailure(call: Call<ResponseBody>, t: Throwable) { Log.d("실패!
","!
1") } }) } /** 문자열 정보가 이상하게 들어와 알맞게 나눠주고 리스트에 추가 **/ private fun addSubstringToSuggestionKeyword(splitList: List<String>){ for (index in splitList.indices){ if (splitList(index).isNotEmpty()){ if (splitList(index)(splitList(index).length-1) == ')') suggestionKeywords.add(splitList(index).substring(1, splitList(index).length-2)) else suggestionKeywords.add(splitList(index).substring(1, splitList(index).length-1)) } } } private fun convertStringUnicodeToKorean(data: String): String { val sb = StringBuilder() // 단일 쓰레드이므로 StringBuilder 선언 var i = 0 /** * \uXXXX 로 된 아스키코드 변경 * i+2 to i+6 을 16진수의 int 계산 후 char 타입으로 변환 */ while (i < data.length) { if (data(i) == '\\' && data(i + 1) == 'u') { val word = data.substring(i + 2, i + 6).toInt(16).toChar() sb.append(word) i += 5 } else { sb.append(data(i)) } i++ } return sb.toString() }

getSuggestionKeyword(newText: String) 함수가 호출되면,

newText 관련 쿼리를 suggestionKeywords 목록으로 나누어 저장하고,

재활용 보기 어댑터로 보내 항목을 업데이트해 줍니다.

재활용 어댑터를 ListAdapter 형식으로 만들었으므로 submitList를 사용합니다.

RecyclerView.Adapter로 만든 사람은 adapter.notifyDataSetChanged() 메서드를 사용할 수 있습니다.

호출이 될 때마다 관련 검색어가 바뀌어야 하기 때문에, suggestionKeywords.clear() 를 최초로 작성했습니다.

이제 마지막 게시물에 만든 searchView의 queryListener에 해당하는 함수를 넣습니다.

searchView.setOnQueryTextListener(object: SearchView.OnQueryTextListener{
    override fun onQueryTextSubmit(query: String?): Boolean {
        /..
        return false
    }
    
    override fun onQueryTextChange(newText: String?): Boolean {
        if (newText !
= ""){ getSuggestionKeyword(newText!
!
) } if (newText == ""){ suggestionKeywords.clear() searchSuggestionKeywordAdapter.submitList(suggestionKeywords.toMutableList()) } return false } })

searchView에 검색어가 비어 있으면 관련 검색어 창도 비어 있어야 하므로 suggestionKeywords를 비운 후,

재활용 보기를 업데이트합니다.

검색어가 바뀔 때마다, 상기에서 작성한 getSuggestionKeyword 함수에 인수를 건네주어 호출합니다.


완성!

해당 재활용 뷰의 항목 xml 파일.

<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:app="http://schemas.android.com/apk/res-auto"
    android:layout_width="match_parent"
    android:layout_height="40dp">
    <ImageView
        android:id="@+id/search_icon"
        android:layout_width="25dp"
        android:layout_height="25dp"
        android:src="http://joh9911-programming-note./m/@drawable/ic_baseline_search_24_black"
        android:layout_margin="10dp"
        app:layout_constraintStart_toStartOf="parent"
        app:layout_constraintTop_toTopOf="parent"
        />
    <TextView
        android:id="@+id/suggestion_keyword"
        android:layout_width="0dp"
        android:layout_height="match_parent"
        android:text="연관 검색어"
        android:textSize="15dp"
        android:textColor="@color/black"
        android:gravity="center_vertical"
        android:paddingLeft="25dp"
        app:layout_constraintStart_toEndOf="@id/search_icon"
        app:layout_constraintEnd_toStartOf="@id/move_icon"/>
    <ImageView
        android:id="@+id/move_icon"
        android:layout_width="25dp"
        android:layout_height="25dp"
        android:src="@drawable/ic_baseline_arrow_outward_24"
        android:layout_margin="10dp"
        app:layout_constraintEnd_toEndOf="parent"
        app:layout_constraintTop_toTopOf="parent"
        app:layout_constraintBottom_toBottomOf="parent"/>

</androidx.constraintlayout.widget.ConstraintLayout>