RecyclerView.Adapter를 사용했다.import com.google.gson.annotations.SerializedName
//내가 사용할 데이터들. 필요한 데이터만 가지고 왔다.
data class KakaoImageData (
@SerializedName("thumbnail_url")
val thumbnailUrl: String,
@SerializedName("display_sitename")
val siteName: String,
val datetime: String
)
//kakaoAPI에서 documents는 응답결과를 반환해준다.
data class ImageResponse(
val documents: List<KakaoImageData>
)
@SerializedName을 사용한 변수를 고치면서 순간적으로 같이 고쳤나본데, datetime은 SerializedName을 안걸어줬기 때문에 꼭 카카오에서 제공하는 변수명을 사용해야 데이터가 제대로 들어온다. 값을 제대로 datetime으로 맞춰주고 해결했다.ㅎimport android.view.LayoutInflater
import android.view.ViewGroup
import androidx.recyclerview.widget.RecyclerView
import com.bumptech.glide.Glide
import com.example.myimgsearch.databinding.LayoutItemBinding
import java.time.OffsetDateTime
import java.time.format.DateTimeFormatter
class SearchAdapter(val kakaoItemData : MutableList<KakaoImageData>) : RecyclerView.Adapter<SearchAdapter.Holder>() {
inner class Holder(binding : LayoutItemBinding) : RecyclerView.ViewHolder(binding.root) {
val ivItemImg = binding.ivItemImg
val tvSiteName = binding.tvSiteName
val tvdateTime = binding.tvDateTime
fun bind(data : KakaoImageData) {
Glide.with(itemView.context).load(data.thumbnailUrl).into(ivItemImg)
tvSiteName.text = data.siteName
tvdateTime.text = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss").format(OffsetDateTime.parse(data.datetime))
}
}
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): Holder {
val binding = LayoutItemBinding.inflate(LayoutInflater.from(parent.context),parent,false)
return Holder(binding)
}
override fun getItemCount(): Int = kakaoItemData.size
override fun onBindViewHolder(holder: Holder, position: Int) {
val item = kakaoItemData[position]
holder.bind(item)
}
}
[트러블슈팅] 수준별 수업 때 튜터님 코드를 많이 참고하고 있었는데, Glide를 통해 이미지를 바인딩하고 있었다. 검색해보니까 이미지 로딩 프레임워크로 빠르고 유능해서 많이 사용한다고 하길래 나도 적용해 보기로 했다.
=> Glide를 사용하기 위해서는 build.gradle(module)에 의존성을 추가해줘야한다.
implementation ("com.github.bumptech.glide:glide:4.12.0")
annotationProcessor ("com.github.bumptech.glide:compiler:4.12.0")
의존성을 추가해 준 후, Gradle을 임포트하면, .load()를 사용하여 이미지 정보를 가지고 있는 개개체를 파라미터로 받을 수 있고, into()를 사용하여 이미지를 넣을 파라미터를 설정할 수 있다.
+이외에도 load()가 반환한 이미지처리에 로딩중,에러,null일 때 보여줄 수 있는 이미지를 설정할 수 있다.([Android | Kotlin] Glide 참조)
[트러블슈팅] 날짜와 시간이 "yyyy-MM-dd HH:mm:ss” 포멧으로 나오는 것이 요구 사항 이였다. 날짜 포맷을 검색해서 맞춰보려고 했는데, LocalDateTime을 활용하는 방법이 있길래 적용했다가 오류를 맞고 결국 AI의 도움을 받아서 공식을 완성했다.
=> 찾아보니까 LocalDateTime은 시간대 정보를 포함하지 않고 OffsetDateTime는 포함하는 차이가 있다고 하는데, 카카오에서 제공하는 datetime의 형식이 시간대정보를 포함하고 있기 때문에 OffsetDateTime을 사용하는게 맞는 것 같다.
import retrofit2.Call
import retrofit2.http.GET
import retrofit2.http.Header
import retrofit2.http.Query
interface KakaoApi {
@GET("v2/search/image")
fun getImgData(
@Header("Authorization") apiKey: String = "KakaoAK ${BuildConfig.KAKAOAPI}",
@Query("query") query: String
) : Call<ImageResponse>
}
kakao 이미지 검색하기에서 Header의 Authorization와, 쿼리파라미터에서 query가 필수사용이길래 2개를 걸어줬다. Header의 경우는 여기서 처리하지않고, RetrofitInstance에서 .addHeader로 걸어주는 방법이 있던데, 내가 사용하는 RetrofitInstance에서는 적용하기가 어지러워서 여기서 처리하도록 만들었다 ... .... .
: Call<ImageResponse>로 ImageDTO에 있는 ImageResponse 데이터 클래스를 참조하도록 작성한다. 참고로 비동기식 요청을 처리할 때에는 Call, 동기식 요청을 처리할 때에는 Response를 사용한다.
[트러블슈팅] @Header에서 apikey값을 그냥 @Header("Authorization") apiKey: String = BuildConfig.KAKAOAPI 라고만 적었더니, AccessDeniedError","message":"cannot find Authorization : KakaoAK header" 오류가 발생했다.
=> 알고보니까 카카오API에서는 KakaoAK ${REST_API_KEY}라고 저 KakaoAK (한칸띄어쓰기필수)를 포함하는 값만을 키값으로 인식한다고 했다. 맞는 모양으로 고쳐줬다..
import okhttp3.OkHttpClient
import okhttp3.logging.HttpLoggingInterceptor
import retrofit2.Retrofit
import retrofit2.converter.gson.GsonConverterFactory
import java.util.concurrent.TimeUnit
object RetrofitInstance {
private const val BASE_URL = "https://dapi.kakao.com"
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 retrofit = Retrofit.Builder()
.baseUrl(BASE_URL)
.addConverterFactory(GsonConverterFactory.create())
.client(createOkHttpClient())
.build()
val api : KakaoApi = retrofit.create(KakaoApi::class.java)
}
import android.content.Context
import android.os.Bundle
import android.util.Log
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import android.view.inputmethod.InputMethodManager
import androidx.fragment.app.Fragment
import androidx.recyclerview.widget.GridLayoutManager
import com.example.myimgsearch.databinding.FragmentSearchBinding
import retrofit2.Call
import retrofit2.Callback
import retrofit2.Response
class SearchFragment : Fragment() {
private var _binding: FragmentSearchBinding? = null
private val binding get() = _binding!!
private lateinit var adapter: SearchAdapter
private val dataList = mutableListOf<KakaoImageData>()
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? {
_binding = FragmentSearchBinding.inflate(inflater, container, false)
return binding.root
}
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
adapter = SearchAdapter(dataList)
binding.rvImgData.adapter = adapter
binding.rvImgData.layoutManager = GridLayoutManager(context, 2)
binding.btnSearch.setOnClickListener {
val searchWord = binding.etSearch.text.toString()
getSearchImg(searchWord)
hideKeyboard()
}
}
private fun getSearchImg(searchWord : String) {
RetrofitInstance.api.getImgData(query = searchWord).enqueue(object : Callback<ImageResponse>{
override fun onResponse(call: Call<ImageResponse>, response: Response<ImageResponse>) {
val body = response.body()
//dataList.addAll(body.documents)를 null체크한 모습
body?.let { //it:ImageResponse
dataList.addAll(it.documents)
}
adapter.notifyDataSetChanged()
Log.d("api검사", "$dataList")
}
override fun onFailure(call: Call<ImageResponse>, t: Throwable) {
Log.d("api검사","네트워크오류/데이터변환오류.")
}
})
}
private fun hideKeyboard() {
val inputMethodManager = requireContext().getSystemService(Context.INPUT_METHOD_SERVICE) as InputMethodManager
inputMethodManager.hideSoftInputFromWindow(view?.windowToken,0)
}
override fun onDestroy() {
super.onDestroy()
_binding = null
}
}
GridLayoutManager(context, 2) : 요구하는 레이아웃모양이 그리드모양이라 처음 사용해봤다. 딱히 사용법이 렵지는 않아서 뚝딱 걸어줬다. context 옆에 원하는 열 수 만큼 숫자를 걸어주면 된다.
[트러블슈팅] getSearchImg()메서드는 만드는 과정 자체가 트러블 슈팅이었다. 저번에 강의를 보고 공부할 때 레트로핏 응답과 요청을 분리해서 생각해버려서.. 하나의 메서드로 만드는 것까지 하루를 꼬박 투자했다... ... .
=> [비동기식요청] RetrofitInstance에 있는 api는 KakaoApi를 참조하기 때문에, api를 통해 getImgData()를 가져온다. 이때! Header에 해당하는 apiKey는 직접 기본값을 입력해줬기 때문에, query 값에 대해서만 query=searchWord로 설정해준다. 만약 키값을 프래그먼트에서 직접 적용하고 싶다면 여기서도 "KakaoAK ${BuildConfig.KAKAOAPI}"라고 원하는 모양을 정확히 입력해줄 수 있도록 한다. (->getImgData("KakaoAK ${BuildConfig.KAKAOAPI}", searchWord))
그리고 여기서는 Call의 enqueue()를 사용하여 비동기식 요청을 한다. 콜백을 사용하여 백그라운드에서 실행되고, 정상적으로 응답이 오면 onResponse가 오류가 생기면 onFaliure가 호출된다. 해당 메서드안에 응답객체를 사용한 처리를 작성하면 된다.
+검색결과를 최대 80개까지 표시하도록하는 요구가 있었는데, 카카오 이미지 검색 restAPI의 기본사이즈 값이 80이여서 80개가 제대로 나오는지 확인만 했다. 잘 나와서 확인로그는 삭제했다.
=>[응답객체 사용하기] onResponse에 response 객체를 사용하여 HTTP응답을 통한 정보에 접근한다. 원래는 onResponse안에서 한번 더 확인하기 위해 if (response.isSuccessful) {}를 사용하지만, 지금은 사용하지 않았다. 여기서 response.body()는 요청사항 그대로 KakaoApi에서 선언한 getImgData의 반환 값인 ImageResponse이다. 나는 가져온 값들을 dataList에 addAll로 담아주었다. 이렇게 데이터가 담기면, 리사이클러뷰가 바로 알아차릴 수 있도록! add해주자마자 그 밑에 adapter.notifyDataSetChanged()를 통해서 바로! 변경사항을 전달해주어야한다.
검색 버튼을 누르면 키보드는 숨김 처리 되도록 구현하는 것이 조건이여서 hideKeyboard()를 만들었다. 검색해보니까 여러 방법들이 나왔는데, 키보드를 숨기는 조건은 여러가지지만, 대체로 똑같은 모양을 사용하고 있어서 뚝딱 걸었다.