[Android] RecyclerView 와 비동기 통신(Retrofit2)
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("받아온 데이터 체크: ", "실패했습니다.");
}
});
}
}