[Android,Kotlin] Retrofit2 를 이용하여 RecyclerView에 Rest API 호출하기.

youneeo·2024년 1월 19일
0

일단 이글을 작성하는 이유부터 소개하겠다.

  1. 국비지원 캠프에서 공부했을때 가장 어려웠던 부분이였음.
  2. 지원 공고들을 봤을때, Retrofit2 는 거의 필수인점.
  3. 여기에 들어가는 개념과 구조를 이해시키려는 의도.

Retrofit2 통신을 위해서 의존성을 추가해준다.

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")

AndroidManifest.xml

  <uses-permission android:name="android.permission.INTERNET"></uses-permission> // 인터넷 통신을 해야하기 때문에,manifests 파일에 application 범위 위에 설정해준다.

받아올 데이터 (DTO = Data Transfer Object) 작성
방법: open api 사이트를 가면 응답 예시를 보여준다. 직접 입력해서 응답 결과를 보여주는 곳이 있고, 응답예시 주소만 주는곳이 있다.
응답예시를 꼭! 받아야 하는 이유는 응답 예시에 있는 구조를 보고 DTO를 작성해야하기 때문에(경로가 어긋나면 호출이 되지 않음) 꼭 응답 예시를 받아서
변환시켜준다.
응답예시: 영화 진흥 위원회 openapi

JSON 형태로 응답을 받을것이기 때문에 주소창에 입력해주면

이러한 형태로 들어온다. 이걸 그대로 복사하여 변환해주는 플러그인에 붙여넣기 하고 우상단에 Format을 눌러서 보면,

이렇게 응답받는 데이터 클래스의 형태의 구조를 정리해서 보여준다. 이제 밑에 Advanced를 눌러
nullalbe 체크, 그리고 직렬화해줄 라이브러리(gson or moshi 등등) 저는 gson을 쓰므로 gson으로 선택했습니다.

메인 코드를 보기에 앞서, baseurl 과 엔드포인트 (요청주소 @GET("여기에 들어갈 주소")
에 대해서 설명하겠습니다. 요청할 기본주소와, 내가 꺼낼 자료가 있는 디테일한주소 (이걸 엔드포인트라고 부릅니다.)
정리가 잘 되어있는 nexon api 를 보고 간단하게 설명을 하겠습니다.

Spring이나 웹에선 기본 요청 url을 붙여서 쓰는 경우도 있지만, 안드로이드의 Retrofit에선

baseurl / endpoint(get주소) 단위로 나누어서 각각 레트로핏,서비스인터페이스에 나눠서 작성합니다. 이제 코드를 보겠습니다.

MainActivity.kt

package com.android.project

import androidx.appcompat.app.AppCompatActivity
import android.os.Bundle
import androidx.recyclerview.widget.LinearLayoutManager
import com.android.project.databinding.ActivityMainBinding
import retrofit2.Call
import retrofit2.Callback
import retrofit2.Response
import retrofit2.Retrofit
import retrofit2.converter.gson.GsonConverterFactory

class MainActivity : AppCompatActivity() {


    private val binding by lazy { ActivityMainBinding.inflate(layoutInflater) }
    private val adapter by lazy { Adapter(dataList) }
    private val dataList = mutableListOf<MovieData.BoxOfficeResult.DailyBoxOffice?>()
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(binding.root)

        binding.rvMovie.adapter = adapter
        binding.rvMovie.layoutManager = LinearLayoutManager(this)

        binding.btnSearch.setOnClickListener { movieRequest() }
    }

    fun movieRequest() {

        //1.Retrofit 객체 초기화
        val retrofit: Retrofit = Retrofit.Builder()
            .baseUrl("http://www.kobis.or.kr/")
            .addConverterFactory(GsonConverterFactory.create())
            .build()
        //2. 서비스 객체 생성
        val apiService: MovieService = retrofit.create(MovieService::class.java)
        //3. Call 객체 생성
        val inputDate = binding.etInput.text.trim().replace(Regex(" "), "")
        val movieCall = apiService.getmovieinfo(
            "ApiKey", // 여기서 APikey는 실제 본인의 APi key값으로 대체합니다. 
            inputDate,
            "10",
            "N",
            "K",
            ""
        )

        if (!dataList.isEmpty()) {
            dataList.clear()
        }
        movieCall.enqueue(object : Callback<MovieData> {
            override fun onResponse(call: Call<MovieData>, response: Response<MovieData>) {
                val data = response.body()

                val movieinfo = data?.boxOfficeResult?.dailyBoxOfficeList

                if (!movieinfo.isNullOrEmpty()) {
                    movieinfo?.let { info ->
                        info.forEach {
                            dataList.add(it)
                        }
                    }
                    adapter.notifyDataSetChanged()
                }

            }

            override fun onFailure(call: Call<MovieData>, t: Throwable) {

                call.cancel()
            }
        })

    }
}

MovieService.kt

package com.android.project

import retrofit2.Call
import retrofit2.http.GET
import retrofit2.http.Query

interface MovieService {
    @GET("kobisopenapi/webservice/rest/boxoffice/searchDailyBoxOfficeList.json") // 아까 말했던 요청주소를 넣습니다.
    //추상메소드명안에 요청 파라미터들을 정의해서 넣어줍니다.
    fun getmovieinfo(
        @Query("key") key: String,
        @Query("targetDt") targetDt: String,  // 조회하고자 하는 날짜를 yyyymmdd 형식으로 입력합니다.
        @Query("itemPerPage") itemPerPage: String, // 결과 ROW 의 개수를 지정합니다.(default : “10”, 최대 : “10“)
        @Query("multiMovieYn") multiMovieYn: String, // “Y” : 다양성 영화 “N” : 상업영화 (default : 전체)
        @Query("repNationCd") repNationCd: String, // “K: : 한국영화 “F” : 외국영화 (default : 전체)
        @Query("wideAreaCd") wideAreaCd: String // 상영지역별로 조회할 수 있으며, 지역코드는 공통코드 조회 서비스에서 “0105000000” 로서 조회된 지역코드 입니다. (default : 전체)
    ): Call<MovieData> // 콜객체에 요청할 데이터클래스를 담아 작성해줍니다.(DTO 클래스명)
}

MovieData.kt

package com.android.project


import com.google.gson.annotations.SerializedName

data class MovieData(
    @SerializedName("boxOfficeResult")
    val boxOfficeResult: BoxOfficeResult?
) {
    data class BoxOfficeResult(
        @SerializedName("boxofficeType")
        val boxofficeType: String?,
        @SerializedName("dailyBoxOfficeList")
        val dailyBoxOfficeList: List<DailyBoxOffice?>?,
        @SerializedName("showRange")
        val showRange: String?
    ) {
        data class DailyBoxOffice(
            @SerializedName("audiAcc")
            val audiAcc: String?,
            @SerializedName("audiChange")
            val audiChange: String?,
            @SerializedName("audiCnt")
            val audiCnt: String?,
            @SerializedName("audiInten")
            val audiInten: String?,
            @SerializedName("movieCd")
            val movieCd: String?,
            @SerializedName("movieNm")
            val movieNm: String?,
            @SerializedName("openDt")
            val openDt: String?,
            @SerializedName("rank")
            val rank: String?,
            @SerializedName("rankInten")
            val rankInten: String?,
            @SerializedName("rankOldAndNew")
            val rankOldAndNew: String?,
            @SerializedName("rnum")
            val rnum: String?,
            @SerializedName("salesAcc")
            val salesAcc: String?,
            @SerializedName("salesAmt")
            val salesAmt: String?,
            @SerializedName("salesChange")
            val salesChange: String?,
            @SerializedName("salesInten")
            val salesInten: String?,
            @SerializedName("salesShare")
            val salesShare: String?,
            @SerializedName("scrnCnt")
            val scrnCnt: String?,
            @SerializedName("showCnt")
            val showCnt: String?
        )
    }
}

Adapter

package com.android.project

import android.view.LayoutInflater
import android.view.ViewGroup
import androidx.recyclerview.widget.RecyclerView
import com.android.project.databinding.ActivitiyMainItemBinding

class Adapter(val items: MutableList<MovieData.BoxOfficeResult.DailyBoxOffice?>):RecyclerView.Adapter<Adapter.ViewHolder>() {
    override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): Adapter.ViewHolder {
        val binding = ActivitiyMainItemBinding.inflate(LayoutInflater.from(parent.context),parent,false)
        return ViewHolder(binding)
    }

    override fun onBindViewHolder(holder: Adapter.ViewHolder, position: Int) {
        holder.bind(items[position])
    }

    override fun getItemCount(): Int {
        return items.size
    }
    inner class ViewHolder(val binding: ActivitiyMainItemBinding) : RecyclerView.ViewHolder(binding.root){
        fun bind(item: MovieData.BoxOfficeResult.DailyBoxOffice?){

            binding.tvMovieNm.text = "영화이름:${item?.movieNm}🎬"
            binding.tvOpenDt.text = "개봉일:${item?.openDt}일🍿"
            binding.tvSalesAmt.text = "매출액:${item?.salesAmt}원⭐"
            binding.tvAudiChange.text = "전일대비:${item?.audiChange}%"
            binding.tvSalesAcc.text = "누적매출:${item?.salesAcc}원⭐"
            binding.tvAudiAcc.text = "누적관객수:${item?.audiAcc}명👏"

        }
    }
}

activity_main.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"
    xmlns:tools="http://schemas.android.com/tools"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    tools:context=".MainActivity">

    <EditText
        android:id="@+id/et_input"
        android:layout_width="match_parent"
        android:layout_height="50dp"
        android:gravity="center"
        android:hint="영화일자:검색예시 20240101"
        android:background="@drawable/edit_outline"
        app:layout_constraintBottom_toBottomOf="parent"
        app:layout_constraintEnd_toEndOf="parent"
        app:layout_constraintHorizontal_bias="0.0"
        app:layout_constraintStart_toStartOf="parent"
        app:layout_constraintTop_toTopOf="parent"
        app:layout_constraintVertical_bias="0.007" />

    <Button
        android:id="@+id/btn_search"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:layout_marginStart="161dp"
        android:layout_marginTop="5dp"
        android:layout_marginEnd="162dp"
        android:text="조회"
        android:textStyle="bold"
        app:layout_constraintEnd_toEndOf="parent"
        app:layout_constraintStart_toStartOf="parent"
        app:layout_constraintTop_toBottomOf="@+id/et_input" />

    <androidx.recyclerview.widget.RecyclerView
        android:id="@+id/rv_movie"
        android:layout_width="409dp"
        android:layout_height="624dp"
        android:layout_marginStart="1dp"
        android:layout_marginTop="1dp"
        android:layout_marginEnd="1dp"
        app:layout_constraintEnd_toEndOf="parent"
        app:layout_constraintStart_toStartOf="parent"
        app:layout_constraintTop_toBottomOf="@+id/btn_search" />

</androidx.constraintlayout.widget.ConstraintLayout>

activity_main_item.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"
    xmlns:tools="http://schemas.android.com/tools"
    android:layout_width="match_parent"
    android:layout_height="wrap_content">

    <androidx.constraintlayout.widget.ConstraintLayout
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        tools:layout_editor_absoluteX="0dp"
        tools:layout_editor_absoluteY="0dp">

        <TextView
            android:id="@+id/tv_movieNm"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:layout_marginStart="11dp"
            android:layout_marginTop="12dp"
            android:text="영화이름:"
            app:layout_constraintStart_toStartOf="parent"
            app:layout_constraintTop_toTopOf="parent" />

        <TextView
            android:id="@+id/tv_openDt"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:layout_marginStart="12dp"
            android:layout_marginTop="9dp"
            android:text="개봉일:"
            app:layout_constraintStart_toStartOf="parent"
            app:layout_constraintTop_toBottomOf="@+id/tv_movieNm" />

        <TextView
            android:id="@+id/tv_salesAmt"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:layout_marginStart="11dp"
            android:layout_marginTop="10dp"
            android:text="매출액:"
            app:layout_constraintStart_toStartOf="parent"
            app:layout_constraintTop_toBottomOf="@+id/tv_openDt" />

        <TextView
            android:id="@+id/tv_audiChange"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:layout_marginStart="11dp"
            android:layout_marginTop="7dp"
            android:text="전일대비:"
            app:layout_constraintStart_toStartOf="parent"
            app:layout_constraintTop_toBottomOf="@+id/tv_salesAmt" />

        <TextView
            android:id="@+id/tv_salesAcc"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:layout_marginStart="11dp"
            android:layout_marginTop="8dp"
            android:text="누적 매출:"
            app:layout_constraintStart_toStartOf="parent"
            app:layout_constraintTop_toBottomOf="@+id/tv_audiChange" />

        <TextView
            android:id="@+id/tv_audiAcc"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:layout_marginStart="11dp"
            android:layout_marginTop="8dp"
            android:text="누적 관객수:"
            app:layout_constraintStart_toStartOf="parent"
            app:layout_constraintTop_toBottomOf="@+id/tv_salesAcc" />
    </androidx.constraintlayout.widget.ConstraintLayout>
</androidx.constraintlayout.widget.ConstraintLayout>

실행결과

profile
정돈된 공간에서 생각하기를 좋아합니다.

0개의 댓글