Android

[Android] RecyclerView 와 비동기 통신(Retrofit2)

jane.dev 2021. 11. 2. 18:31
반응형

2021.11.01 - [Android] - [Android] ListView로 동적 리스트 추가

 

[Android] ListView로 동적 리스트 추가

ListView 세부정보가 포함되지 않는 뷰그룹, 데이터 목록을 수직 스크롤로 제공 ListView에서 나라 이름을 누르면 해당 이미지를 보여주는 예제 activity_main.xml ListView 위젯 정의

wheneveryouwantsz.tistory.com

 

RecyclerView
ListView보다 유연하고 성능이 뛰어난 접근방식으로
대량의 데이터 세트를 효율적으로 표시

 

세팅

AndroidManifest.xml

앱의 올바른 동작을 위해 사용자가 부여하는 시스템 권한(앱이 실행중일 때 사용자에게 인터넷을 사용할 수 있도록 권한이 부여됨)

<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
    package="org.ict.movieprj">
    <!-- 추가 -->
    <uses-permission android:name="android.permission.INTERNET" />
    <application
        android:usesCleartextTraffic="true"
        android:allowBackup="true"
        android:icon="@mipmap/ic_launcher"
        android:label="@string/app_name"
        android:roundIcon="@mipmap/ic_launcher_round"
        android:supportsRtl="true"
        android:theme="@style/Theme.MoviePrj">
        <activity android:name=".MainActivity">
            <intent-filter>
                <action android:name="android.intent.action.MAIN" />

                <category android:name="android.intent.category.LAUNCHER" />
            </intent-filter>
        </activity>
    </application>

</manifest>

android:name <permission> 요소를 사용해 애플리케이션에서 정의한 권한의 이름

android.permission.INTERNET 애플리케이션에서 네트워크 소켓을 열 수 있도록 허용

android:usesCleartextTraffic 앱이 일반 텍스트  HTTP와 같은 일반 텍스트 네트워크 트래픽을 사용하는지 여부(API 레벨 27이하를 타겟팅하는 앱에서는 기본값이 true)

 

Gradle Scripts > build.gradle(Module: 프로젝트명.app)

android에서 API 서버와 통신하기 위한 방법 

dependencies 내부에 작성해 의존성 추가

retrofit2:retrofit

 

retrofit2:converter-gson

 

→ Sync now 로 적용

 

영화진흥위원회의 OPEN API를 통해 일별 박스오피스 순위 받아오기

http://www.kobis.or.kr/kobisopenapi/

 

http://www.kobis.or.kr/kobisopenapi/

 

www.kobis.or.kr

 

회원가입 후 키 발급/관리 탭에서 키 발급 받기

 

OPEN API탭에서 응답 예시 JSON URL 뒤에 key(발급받은 키 값) 와 targetDt(조회하고자하는 날짜) 를 붙이기

 

브라우저에서는 아래와 같이 결과로 볼 수 있음 → 복사

 

VO 객체를 생성하기 위해서 아래 주소로 접속

https://www.jsonschema2pojo.org

 

jsonschema2pojo

Reference properties For each property present in the 'properties' definition, we add a property to a given Java class according to the JavaBeans spec. A private field is added to the parent class, along with accompanying accessor methods (getter and sette

www.jsonschema2pojo.org

 

붙여넣기 → Preview 클릭

 

(구조)

 

클래스 3개가 생성됨

 

위에서 생성된 3개의 VO 객체를 작성(javax.annotation.Generated 삭제_지원하지 않음)

DailyBoxOffice

public class DailyBoxOffice {

    @SerializedName("rnum")
    @Expose
    private String rnum;
    @SerializedName("rank")
    @Expose
    private String rank;
    @SerializedName("rankInten")
    @Expose
    private String rankInten;
    @SerializedName("rankOldAndNew")
    @Expose
    private String rankOldAndNew;
    @SerializedName("movieCd")
    @Expose
    private String movieCd;
    @SerializedName("movieNm")
    @Expose
    private String movieNm;
    @SerializedName("openDt")
    @Expose
    private String openDt;
    @SerializedName("salesAmt")
    @Expose
    private String salesAmt;
    @SerializedName("salesShare")
    @Expose
    private String salesShare;
    @SerializedName("salesInten")
    @Expose
    private String salesInten;
    @SerializedName("salesChange")
    @Expose
    private String salesChange;
    @SerializedName("salesAcc")
    @Expose
    private String salesAcc;
    @SerializedName("audiCnt")
    @Expose
    private String audiCnt;
    @SerializedName("audiInten")
    @Expose
    private String audiInten;
    @SerializedName("audiChange")
    @Expose
    private String audiChange;
    @SerializedName("audiAcc")
    @Expose
    private String audiAcc;
    @SerializedName("scrnCnt")
    @Expose
    private String scrnCnt;
    @SerializedName("showCnt")
    @Expose
    private String showCnt;

    public String getRnum() {
        return rnum;
    }

    public void setRnum(String rnum) {
        this.rnum = rnum;
    }

    public String getRank() {
        return rank;
    }

    public void setRank(String rank) {
        this.rank = rank;
    }

    public String getRankInten() {
        return rankInten;
    }

    public void setRankInten(String rankInten) {
        this.rankInten = rankInten;
    }

    public String getRankOldAndNew() {
        return rankOldAndNew;
    }

    public void setRankOldAndNew(String rankOldAndNew) {
        this.rankOldAndNew = rankOldAndNew;
    }

    public String getMovieCd() {
        return movieCd;
    }

    public void setMovieCd(String movieCd) {
        this.movieCd = movieCd;
    }

    public String getMovieNm() {
        return movieNm;
    }

    public void setMovieNm(String movieNm) {
        this.movieNm = movieNm;
    }

    public String getOpenDt() {
        return openDt;
    }

    public void setOpenDt(String openDt) {
        this.openDt = openDt;
    }

    public String getSalesAmt() {
        return salesAmt;
    }

    public void setSalesAmt(String salesAmt) {
        this.salesAmt = salesAmt;
    }

    public String getSalesShare() {
        return salesShare;
    }

    public void setSalesShare(String salesShare) {
        this.salesShare = salesShare;
    }

    public String getSalesInten() {
        return salesInten;
    }

    public void setSalesInten(String salesInten) {
        this.salesInten = salesInten;
    }

    public String getSalesChange() {
        return salesChange;
    }

    public void setSalesChange(String salesChange) {
        this.salesChange = salesChange;
    }

    public String getSalesAcc() {
        return salesAcc;
    }

    public void setSalesAcc(String salesAcc) {
        this.salesAcc = salesAcc;
    }

    public String getAudiCnt() {
        return audiCnt;
    }

    public void setAudiCnt(String audiCnt) {
        this.audiCnt = audiCnt;
    }

    public String getAudiInten() {
        return audiInten;
    }

    public void setAudiInten(String audiInten) {
        this.audiInten = audiInten;
    }

    public String getAudiChange() {
        return audiChange;
    }

    public void setAudiChange(String audiChange) {
        this.audiChange = audiChange;
    }

    public String getAudiAcc() {
        return audiAcc;
    }

    public void setAudiAcc(String audiAcc) {
        this.audiAcc = audiAcc;
    }

    public String getScrnCnt() {
        return scrnCnt;
    }

    public void setScrnCnt(String scrnCnt) {
        this.scrnCnt = scrnCnt;
    }

    public String getShowCnt() {
        return showCnt;
    }

    public void setShowCnt(String showCnt) {
        this.showCnt = showCnt;
    }

}

 

BoxOfficeResult

public class BoxOfficeResult {

    @SerializedName("boxofficeType")
    @Expose
    private String boxofficeType;
    @SerializedName("showRange")
    @Expose
    private String showRange;
    @SerializedName("dailyBoxOfficeList")
    @Expose
    // DailyBoxOffice를 멤버로 가짐
    private List<DailyBoxOffice> dailyBoxOfficeList = null;

    public String getBoxofficeType() {
        return boxofficeType;
    }

    public void setBoxofficeType(String boxofficeType) {
        this.boxofficeType = boxofficeType;
    }

    public String getShowRange() {
        return showRange;
    }

    public void setShowRange(String showRange) {
        this.showRange = showRange;
    }

    public List<DailyBoxOffice> getDailyBoxOfficeList() {
        return dailyBoxOfficeList;
    }

    public void setDailyBoxOfficeList(List<DailyBoxOffice> dailyBoxOfficeList) {
        this.dailyBoxOfficeList = dailyBoxOfficeList;
    }

}

 

Example

public class Example {

    @SerializedName("boxOfficeResult")
    @Expose
    // BoxOfficeResult를 멤버로 가짐
    private BoxOfficeResult boxOfficeResult;

    public BoxOfficeResult getBoxOfficeResult() {
        return boxOfficeResult;
    }

    public void setBoxOfficeResult(BoxOfficeResult boxOfficeResult) {
        this.boxOfficeResult = boxOfficeResult;
    }

}

 

RetrofitInterface.java

메서드 구현

// RetrofitInterface에는 비동기 호출에 대한 가변 파라미터 주소, 호출 형식 정의
// -> 어떤 주소와 어떤 방식으로 접근할지에 대한 정의)
// 가변파라미터: key값과 targetDt
public interface RetrofitInterface {

    // 영화진흥위원회 API 서버는 조회권한만 가지고 있기 때문에 @GET("요청주소") 형식으로 작성
    @GET("http://kobis.or.kr/kobisopenapi/webservice/rest/boxoffice/searchDailyBoxOfficeList.json")
    // 요청에 대한 응답은 Call<Result>를 반환하는 Getter를 이용
    // 가변파라미터(현 사이트에서는 key, targetDt)는 @Query("파라미터명")자료변수형으로 지정
    Call<Example> getBoxOffice(@Query("key")String key, @Query("targetDt")String targetDt);
}

 

RetrofitClient.java

위에서 생성된 3개의 VO 객체를 이용해 비동기 통신 진행

public class RetrofitClient {
    //필요 변수들을 선언
    private static RetrofitClient instance = null;
    private static RetrofitInterface retrofitInterface;
    // baseUrl만 상황에 맞춰 변경
    private static String baseUrl = "http://www.kobis.or.kr";

    // 싱글턴 패턴으로 RetrofitClient 생성 및 활용
    private RetrofitClient(){
        Retrofit retrofit = new Retrofit.Builder()
                // baseUrl에는 or.kr이나 .com으로 끝나는 주소만 기입
                .baseUrl(baseUrl)
                // 받아온 데이터를 json에서 자바에 맞게 변환
                .addConverterFactory(GsonConverterFactory.create())
                // 변환된 데이터 저장
                .build();
        // RetrofitInterface 구현
        retrofitInterface = retrofit.create(RetrofitInterface.class);
    }

    public static RetrofitClient getInstance(){
       if(instance == null){
           instance = new RetrofitClient();
       }
       return instance;
    }

    public static RetrofitInterface getRetrofitInterface(){
        return retrofitInterface;
    }
}

 

activity_main.xml

RecyclerView를 구현하면 목록의 각 개별요소는 ViewHolder 객체로 정의되고 ViewHolder가 생성되었을 때에는 연결된 데이터가 없음

ViewHolder가 생성되었을 때 RecyclerView가 ViewHolder를 view의 데이터에 바인딩함

→ RecyclerView는 view를 요청하고 어댑터에서 메서드를 호출해 view를 view의 데이터에 바인딩

<?xml version="1.0" encoding="utf-8"?>
<LinearLayout
    xmlns:tools="http://schemas.android.com/tools"
    xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:gravity="center"
    android:orientation="vertical">

    <androidx.recyclerview.widget.RecyclerView
        android:id="@+id/recyView"
        android:layout_width="match_parent"
        android:layout_height="wrap_content" />

</LinearLayout>

내부에 CardView로 구성

 

card.xml

카드 기반 레이아웃(RecyclerView내부에 들어갈 레이아웃)

데이터를 비슷한 스타일의 컨테이너에 표시하기위해 사용

<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:tools="http://schemas.android.com/tools"
    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"
    android:gravity="center"
    android:orientation="vertical">

    <androidx.cardview.widget.CardView
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:layout_margin="5dp"
        app:cardCornerRadius="10dp"
        app:cardElevation="10dp">
        <LinearLayout
            android:layout_width="match_parent"
            android:layout_height="wrap_content"
            android:orientation="horizontal">
            <LinearLayout
                android:layout_width="0dp"
                android:layout_height="match_parent"
                android:layout_weight="1"
                android:orientation="vertical"
                android:gravity="center"
                android:background="#FFEB3B">
                <TextView
                    android:id="@+id/rankNum"
                    android:layout_width="wrap_content"
                    android:layout_height="wrap_content"
                    android:text="순위"
                    android:textSize="30dp"></TextView>
            </LinearLayout>
            <LinearLayout
                android:layout_width="0dp"
                android:layout_height="match_parent"
                android:layout_weight="3"
                android:orientation="vertical"
                android:gravity="center"
                android:background="#3F51B5" >
                <TextView
                    android:id="@+id/mTitle"
                    android:layout_width="wrap_content"
                    android:layout_height="wrap_content"
                    android:text="제목 텍스트"
                    android:textSize="15dp"></TextView>
                <TextView
                    android:id="@+id/mDate"
                    android:layout_width="wrap_content"
                    android:layout_height="wrap_content"
                    android:text="날짜 텍스트"
                    android:textSize="15dp"></TextView>
            </LinearLayout>
        </LinearLayout>
    </androidx.cardview.widget.CardView>

</LinearLayout>

 

MovieAdapter.java

RecyclerView 를 사용하기 위해 데이터를 ViewHolder 뷰와 연결하는 Adapter정의

// RecyclerView 클래스 내부의 Adapter를 상속
public class MovieAdapter extends RecyclerView.Adapter<MovieAdapter.ViewHolder>{

    // 반복해서 View 로 나타내줄 요소 선언
    List<DailyBoxOffice> items;

    // 생성자에 View 로 나타내줄 요소를 입력해야만 실행되도록 파라미터를 처리
    public MovieAdapter(List<DailyBoxOffice> items){
        this.items = items;
    }

    /* 어댑터 정의시 아래 세 가지 키 메서드 재정의해야함 */
    // 1. RecyclerView는 ViewHolder를 새로 생성할 때마다 이 메서드를 호출
    // -> ViewHolder가 특정 데이터에 바인딩(특정한 형식에 데이터를 넣는 것)된 상태가 아니기 때문에 view의 컨텐츠를 채우지는 않음
    // layou폴더 내부에 있는 RecyclerView의 본체로 활용될 현 프로젝트의 card.xml을 참조
    // 여기서의 ViewHolder는 card.xml을 의미함
    @NonNull
    @Override
    public MovieAdapter.ViewHolder onCreateViewHolder(@NonNull ViewGroup parent, int viewType) {
        View itemView = LayoutInflater.from(parent.getContext()).inflate(R.layout.card, parent, false);
        return new ViewHolder(itemView);
    }

    // 2. ViewHolder를 데이터를 연결할 때 이 메서드를 호출
    // -> 적절한 데이터를 가져와 해당 데이터와 ViewHolder의 레이아웃을 채움
    // 위에서 불러온 card.xml 내부의 카드마다 DailyBoxOffice에 해당하는 영화정보를 붙여줘야함
    @Override
    public void onBindViewHolder(@NonNull ViewHolder holder, int position) {
        // holder에 각각 영화 정보를 바인딩
        DailyBoxOffice item = items.get(position);
        holder.setItem(item);
    }

    // 3. 데이터 세트 크기를 가져올 때 이 메서드를 호출
    // 출력할 영화 개수 체크
    public int getItemCount(){
        return items.size();
    }

    // 클래스의 내부에 ViewHolder 클래스 선언, TextView 3개에 대한 설정을 할 수 있도록 처리
    public static class ViewHolder extends RecyclerView.ViewHolder{
        // card.xml 내부 위젯
        private TextView rankNum, mTitle, mDate;

        // 생성자에 card.xml 내부 위젯을 연결
        public ViewHolder(View itemView){
            super(itemView);
            rankNum = itemView.findViewById(R.id.rankNum);
            mTitle = itemView.findViewById(R.id.mTitle);
            mDate = itemView.findViewById(R.id.mDate);
        }

        // 연결된 위젯의 텍스트 세팅
        public void setItem(DailyBoxOffice item){
            rankNum.setText(item.getRank() + "위");
            mTitle.setText("영화 제목: " + item.getMovieNm());
            mDate.setText("개봉일: " + item.getOpenDt());
        }
    }
}

 

MainActivity.java

public class MainActivity extends AppCompatActivity {

    // 비동기 요청을 담당하는 Retrofit관련 변수 선언
    private RetrofitClient retrofitClient;
    private RetrofitInterface retrofitInterface;

    RecyclerView recyView;
    // RecyclerAdapter 필요
    RecyclerView.Adapter mAdapter;

    // 상수로 본인 key 값을 저장
    final String KEY = "발급받은 키 값 입력";

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);

        // Retrofit 요소들을 연결
        retrofitClient = RetrofitClient.getInstance();
        retrofitInterface = RetrofitClient.getRetrofitInterface();

        // RecyclerView 요소를 먼저 연결
        recyView = (RecyclerView)findViewById(R.id.recyView);
        // RecyclerView 는 레이아웃 설정을 자바에서 받아야 표현됨
        LinearLayoutManager layoutManager = new LinearLayoutManager(getApplicationContext(), LinearLayoutManager.VERTICAL, false);

        recyView.setLayoutManager(layoutManager);

        // 화면이 켜졌을 때 비동기 요청으로 데이터를 받아오고, 해당 데이터를 RecyclerView 내부에 세팅하도록 처리
        retrofitInterface.getBoxOffice(KEY, "조회하고자 하는 날짜 입력").enqueue(new Callback<Example>() {
            @Override
            public void onResponse(Call<Example> call, Response<Example> response) {
                // 비동기 데이터 저장
                Example result = response.body();
                // result 내부의 영화정보를 얻어 MovieAdapter 생성자에 전달
                mAdapter = new MovieAdapter(result.getBoxOfficeResult().getDailyBoxOfficeList());
                // RecyclerView에 MovieAdapter를 전달해서 cardView 양식으로 반복하도록 처리
                recyView.setAdapter(mAdapter);
            }

            @Override
            public void onFailure(Call<Example> call, Throwable t) {
                Log.d("받아온 데이터 체크: ", "실패했습니다.");
            }
        });
    }
}