코틀린_Retrofit2 활용 게시판

소정·2023년 3월 22일
0

Kotlin

목록 보기
9/27

준비

  • Retrofit2 연동을 위한 라이브러리 받기

  • 네트워크 작업을 위한 인터넷퍼미션

헤더 값으로 정보 받기위한 키 발급

네이버 키 발급 받기
https://developers.naver.com/products/service-api/search/search.md


헤더에 키값 숨겨 보내기



activity_main.xml

  • 검색 결과를 리사이클러뷰에 보여주기위한 화면 제작
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:app="http://schemas.android.com/apk/res-auto"
    xmlns:tools="http://schemas.android.com/tools"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:orientation="vertical"
    tools:context=".MainActivity">

    <RelativeLayout
        android:padding="16dp"
        android:layout_width="match_parent"
        android:layout_height="wrap_content">

        <EditText
            android:id="@+id/et"
            android:inputType="text"
            android:layout_toLeftOf="@+id/btn"
            android:layout_marginRight="10dp"
            android:hint="검색어"
            android:layout_width="match_parent"
            android:layout_height="wrap_content"/>

        <Button
            android:id="@+id/btn"
            android:layout_alignParentRight="true"
            android:text="검색"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"/>

    </RelativeLayout>

    <androidx.recyclerview.widget.RecyclerView
        android:id="@+id/recycler"
        app:spanCount="2"
        app:layoutManager="androidx.recyclerview.widget.GridLayoutManager"
        android:layout_width="match_parent"
        android:layout_height="match_parent"/>

</LinearLayout>



MainActivity.java

🧨 화면 주요 내용

  • 바인딩을 이용한 view 연결
  • Retrofit를 이용해 네트워크 연결해 네이버에서 제공하는 쇼핑검색 open API 불러오기
  • 소프트 키보드 없애기
  • 네트워크 응답 받았을 때 리사이클러뷰에 아답터 연결해 보이기

파싱한 API String 으로 잘 불러왔는지 확인한 화면


package com.bsj0420.ex03kotlinopenapinaversearch

import android.content.Context
import androidx.appcompat.app.AppCompatActivity
import android.os.Bundle
import android.view.inputmethod.InputMethodManager
import android.widget.Toast
import androidx.appcompat.app.AlertDialog
import com.bsj0420.ex03kotlinopenapinaversearch.databinding.ActivityMainBinding
import retrofit2.Call
import retrofit2.Callback
import retrofit2.Response
import retrofit2.Retrofit
import retrofit2.converter.gson.GsonConverterFactory
import retrofit2.converter.scalars.ScalarsConverterFactory
import retrofit2.create

class MainActivity : AppCompatActivity() {

    private val binding:ActivityMainBinding by lazy { ActivityMainBinding.inflate(layoutInflater) }

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(binding.root) //화면 연결

        binding.btn.setOnClickListener{ serchData() }
        
    }

    private fun serchData() {

        // 소프트 키보드 없애기
        val imm:InputMethodManager = getSystemService(Context.INPUT_METHOD_SERVICE) as InputMethodManager
        imm.hideSoftInputFromWindow(currentFocus?.windowToken, 0)
        //hideSoftInputFromWindow의 매개변수로 포커스(토큰) 넣어줘야함 , flags : 즉시 0


        //네이버에서 제공하는 쇼핑검색 open API 사용해보기

        //1. Retrofit 생성
        //네트워크작업을 대신 작성해주는 라이브러리 활용 : Retrofit
        val retrofit:Retrofit = Retrofit.Builder()
            .baseUrl("https://openapi.naver.com")
            .addConverterFactory(ScalarsConverterFactory.create()) //스칼라가 먼저여야함
            .addConverterFactory(GsonConverterFactory.create())
            .build()

        //2. Retrofit이 해줄 작업에 대한 요구 명세 (인터페이스 설계 & 추상 메소드로 정의)
        // RetrofitService.kt 인터페이스 생성
        
        //3. RetrofitService 객체 생성
        val retrofitService:RetrofitService = retrofit.create(RetrofitService::class.java)

        //4. 원하는 작업 요청 하여 네트워크 작업 실행하는 객체 실행
//        val call:Call<String> = retrofitService.searchDataByString("Client ID",
//            "Client Secret",
//            binding.et.text.toString());
//
//        //5. 작업 시작
//        call.enqueue( object : Callback<String>{ //인터페이스의 익명 클래스 부르는 것 object
//            override fun onResponse(call: Call<String>, response: Response<String>) {
//                var s:String? = response.body()
//
//                AlertDialog.Builder(this@MainActivity).setMessage(s).show()
//            }
//
//            override fun onFailure(call: Call<String>, t: Throwable) {
//                Toast.makeText(this@MainActivity, "error", Toast.LENGTH_SHORT).show()
//            }
//
//        })

        // 위 4,5번은 확인을 위해 Sring 으로 받았기 떄문에 주석처리
        // json으로 파싱 해보자

        //4. 원하는 작업 요청 하여 네트워크 작업 실행하는 객체 실행 리턴 받기
        val call:Call<NaverSearchApiResponse> = retrofitService.searchDataByJson(binding.et.text.toString())

        //5. 네트워크 작업 시작
        call.enqueue(object : Callback<NaverSearchApiResponse>{
            override fun onResponse(
                call: Call<NaverSearchApiResponse>,
                response: Response<NaverSearchApiResponse>
            ) {
                val naverResponse:NaverSearchApiResponse? = response.body()

                //확인용 토스트
                //Toast.makeText(this@MainActivity, "아이템 개수 : ${naverResponse?.items?.size}", Toast.LENGTH_SHORT).show()

                //응답받은 객체의 items 리스트를 리사이클러뷰에 보이기
                binding.recycler.adapter = MyAdapter(this@MainActivity, naverResponse!!.items) //nullable
                //setAdapter 하면 그게 Notify임 따로 안해도 된다
            }

            override fun onFailure(call: Call<NaverSearchApiResponse>, t: Throwable) {
                Toast.makeText(this@MainActivity, "에러 : " + t.message, Toast.LENGTH_SHORT).show()
            }

        })

    }
}

RetrofitService.kt (명세서)

  • RetrofitService 객체 생성 할 때 작업 명세서 쓰는 인터페이스

헤더를 매개변수로 받을 땐 어노테이션 @Header
헤더값을 함수 위에 어노테이션으로 받을 땐 @Headers

package com.bsj0420.ex03kotlinopenapinaversearch

import retrofit2.Call
import retrofit2.http.GET
import retrofit2.http.Header
import retrofit2.http.Headers
import retrofit2.http.Query

interface RetrofitService {

    //고정 값 쿼리는 display=100 넣을 수 있음 , 호출할 때마다 값 전달하기 귀찮아서
    @GET("/v1/search/shop.json?display=100") //여기 만 바꿔서 여러 행동 하게 할수 도 있음
    fun searchDataByString(@Header("X-Naver-Client-Id") clientId:String,
                           @Header("X-Naver-Client-Secret") clientSecret:String ,
                           @Query("query") query:String):Call<String> // 주소 뒤에 붙어갈 것
    //fun searchDataByString(@Query("query") query:String, @Query("display") display:Int ) // 주소 뒤에 붙어갈 것
    //리턴은 Call <> 제네릭 안에 는 쿼리에서 받을 것

    //네이버 검색 API를 요청할 때 다음 예와 같이 HTTP 요청 헤더에 클라이언트 아이디와 클라이언트 시크릿을 추가해야 합니다.


    //헤더 값들도 안바뀌는 글자이기 때문에 고정하기
    @Headers("X-Naver-Client-Id: 키아이디","X-Naver-Client-Secret: 키시크릿")
    @GET("/v1/search/shop.json?display=100")
    fun searchDataByJson(@Query("query") query:String):Call<NaverSearchApiResponse>

}



아답터 작업

아답터에서 할 작업 그림으로 도식화

recycler_item.xml

<?xml version="1.0" encoding="utf-8"?>
<androidx.cardview.widget.CardView xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_width="match_parent"
    android:layout_height="wrap_content"
    xmlns:app="http://schemas.android.com/apk/res-auto"
    app:cardElevation="4dp"
    app:cardCornerRadius="8dp"
    android:layout_marginLeft="8dp"
    android:layout_marginRight="8dp"
    android:layout_marginBottom="4dp"
    android:layout_marginTop="4dp">

    <RelativeLayout
        android:layout_width="match_parent"
        android:layout_height="wrap_content">

        <ImageView
            android:id="@+id/iv"
            android:layout_width="match_parent"
            android:layout_height="160dp"
            android:scaleType="centerCrop"
            android:src="@drawable/penguins"
            />

        <TextView
            android:id="@+id/tv_title"
            android:text="Title"
            android:textStyle="bold"
            android:textColor="@color/black"
            android:textSize="18sp"
            android:layout_below="@+id/iv"
            android:layout_marginTop="8dp"
            android:layout_marginLeft="12dp"
            android:padding="6dp"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"/>

        <TextView
            android:id="@+id/tv_low_price"
            android:layout_below="@+id/tv_title"
            android:layout_alignLeft="@id/tv_title"
            android:textColor="@color/purple_700"
            android:text="20000"
            android:textStyle="bold"
            android:padding="4dp"
            android:layout_marginBottom="6dp"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"/>

        <TextView
            android:id="@+id/tv_mall"
            android:text="판매처"
            android:layout_alignParentRight="true"
            android:layout_alignTop="@+id/tv_low_price"
            android:padding="4dp"
            android:layout_marginBottom="6dp"
            android:textColor="@color/black"
            android:layout_marginRight="12dp"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"/>

    </RelativeLayout>

</androidx.cardview.widget.CardView>

🧨 화면 주요 내용

  • API Response 데이터들 받아오는 item class 생성

  • 데이터 클래스에는 class앞에 data 키워드 필수!

  • 데이터 받아올 때 String으로 받는 게 안전하다
    (그래야 빈 값이 올 때 오류가 안난다)

  • 같은 클래스 안에 data 클래스 2개 다 만듦!

package com.bsj0420.ex03kotlinopenapinaversearch

//데이터 클래스 - 생성자 필수, {} 삭제 가능
//데이터 받아올 때 String으로 받는 게 안전하다
data class NaverSearchApiResponse(var total:Int, var display:Int, var items:MutableList<ShoppingItem>)

//아이템 1개의 클래스 (같은 class에 만들어도 됨)
data class ShoppingItem(
    var title:String,
    var link: String,
    var image: String,
    var lprice: String, //읽어올 Integer값이 빈값으로 오면 error 날 수 있기에 타입을 그냥 String으로 받자
    var hprice: String,
    var mallName: String,
)

Adapter.kt

🧨 화면 주요 내용

  • binding을 활용한 view불러오기
    - 뷰홀더는 아이템뷰 하나짜리 받도록 되어 있음 그래서 ViewHolder 매개변수로 binding.root 를 줘야됨
  • 제목글씨중에 html 태그문이 포함되어 있는걸 안보이도록 제거 : HtmlCompat.FROM_HTML_MODE_COMPACT
  • 선택한 아이템 상세 링크 핸드폰 디바이스의 인터넷 앱으로 실행
package com.bsj0420.ex03kotlinopenapinaversearch

import android.content.Context
import android.content.Intent
import android.net.Uri
import android.view.LayoutInflater
import android.view.ViewGroup
import androidx.core.text.HtmlCompat
import androidx.recyclerview.widget.RecyclerView.Adapter
import androidx.recyclerview.widget.RecyclerView.ViewHolder
import com.bsj0420.ex03kotlinopenapinaversearch.databinding.RecyclerItemBinding
import com.bumptech.glide.Glide

class MyAdapter(var context: Context, var items:MutableList<ShoppingItem>):Adapter<MyAdapter.VH>() {

    //뷰홀더는 아이템뷰 하나짜리 받도록 되어 있음 그래서 binding.root 를 줘야됨
    inner class VH(var binding: RecyclerItemBinding) : ViewHolder(binding.root){
    }

    //inflate
    override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): VH {
        return VH(RecyclerItemBinding.inflate(LayoutInflater.from(context), parent, false))
    }

    override fun getItemCount(): Int = items.size

    //hoder에 연결
    override fun onBindViewHolder(holder: VH, position: Int) {
        val item:ShoppingItem = items[position] //배열처럼 쓰는걸 권장

        //제목글씨중에 html 태그문이 포함되어 있는걸 안보이도록 제거 : HtmlCompat.FROM_HTML_MODE_COMPACT
        var title:String = HtmlCompat.fromHtml(item.title, HtmlCompat.FROM_HTML_MODE_COMPACT).toString()
        holder.binding.tvTitle.text = title
        
        holder.binding.tvLowPrice.text = "${item.lprice} 원"
        holder.binding.tvMall.text = item.mallName

        Glide.with(context).load(item.image).into(holder.binding.iv)

        //링크 열기
        holder.binding.root.setOnClickListener { 
            //핸드폰 디바이스의 인터넷 앱을 실행
            val intent = Intent(Intent.ACTION_VIEW) //인터넷을 보여주는 것
            intent.data = Uri.parse(item.link)
            context.startActivity(intent)
        }
    }

}
profile
보조기억장치

0개의 댓글