본문 바로가기
코딩과 알고리즘

크레이의 앱개발 도전기 #5. 리싸이클뷰

※ 이 게시글은 크레이의 IT개발 관련 성장기를 다루고 있습니다. 관련지식이 약간 있어야 이해되실 수 있습니다. 가벼운 마음으로 읽어보시면서 흥미가 생기고 의욕이 생긴다면? 개발자의 자질이 있으신 겁니다 :)

 

홍드로이드님의 구름 에듀 강좌를 참조하여 만들어 보았습니다.

https://edu.goorm.io/learn/lecture/15564/현직개발자가-알려주는-안드로이드-앱-개발/lesson/727134/12-recyclerview 

이번 리싸이클 뷰는 그냥 영상을 보고 따라하는 정도는 문제가 없었으나,
제대로 사용하려면 제대로 이해해야 하기 때문에 그 시간이 많이 소요되었는데요.
크레이의 경험을 공유하고자 합니다.
강좌 내용은 아니기 때문에 따라하시려면 홍드로이드님 영상을 중점적으로 참조해 주세요.


리싸이클뷰? 왜 쓰는 건가요?

리싸이클뷰는 소스가 아주 특색이 있는데요. 일반적인 리스트뷰에 비해 사용법이 복잡하기 때문입니다.
처음 봤을 때는 '왜 이런 코딩 패턴을 사용하는거지?'라는 생각이 들더라구요.

홍드로이드님 강좌를 따라하면 분명히 잘 작동하지만, 호기심 많은 크레이는 여기 저기 찾아다니며 왜 이렇게 사용해야 하는거지? 의문을 가지고 좀 더 내용을 알아보기로 하였지요. 그러다 한가지 알아낸 사실이 있습니다.

Recycle View(리싸이클 뷰)의 앞에 붙는 Recycle (재활용) 이라는 단어가 그냥 쓰인게 아니라는 걸요.
그러면서 리싸이클 뷰의 놀라운 성능에 '와우~'라는 감탄을 자아냈습니다.

리싸이클 뷰는 보여줄 목록이 1,000개이든 10,000개이든지 앱에 무리가 없습니다.
그 이유는 화면에 보여줄 뷰의 갯수 + 알파(몇개인지는 모릅니다) 만큼만 뷰를 사용하기 때문이라는 점인데요.
이 부분에 대해서는 아래 블로그에 그림으로 안내된 내용이 있기 때문에 참고하시면 도움이 되실 것 같습니다.

https://wooooooak.github.io/android/2019/03/28/recycler_view

크레이가 이해한 내용을 설명드리자면, 화면에 등장하는 한줄 한줄의 각 요소를 '뷰 홀더(view holder)'라 부르는데요.
일반적인 리스트 뷰의 경우 10,000개의 목록을 보여주려면 10,000개의 화면 요소들을 모두 생성하지만,
리싸이클 뷰는 그렇지 않습니다. 화면에 보여줄 분량만 만들어놓거든요.
그러다가 목록을 위로 스크롤할 때 사라지는 뷰 홀더들을 아래쪽에 갖다 붙입니다.
반대로 목록을 아래로 스크롤할 때 사라지는 뷰 홀더들을 다시 위쪽에 붙입니다.
이게 바로 재활용 뷰의 의미입니다. 뷰 홀더를 새로 만들지 않고 있는것만 가지고 재활용하는 것이지요.
사용방법이 이렇다 보니, 화면을 스크롤할 때 보여주는 화면 요소들은
실시간으로 내용을 바로 바로 갱신해 주어야 합니다.
그 속도는 별 영향을 주지 않습니다.
도리어 스마트폰의 메모리를 적게 써서 그런지 아주 빠르더라구요 :)
그 원리를 이해하고 나니 특색있는 코드패턴이 이해가 되었습니다.
요약하면 '위에서의 이런 기능들을 제공할테니, 사용법대로 최소한의 코드만 작성하시오!' 입니다.


안드로이드 돌핀 RecyclerView 기본 포함

이 점은 아주 편리했습니다.
크레이가 사용하는 버전은 안드로이드 돌핀 2021.3.1인데요.


이 버전에는 Gradle 에 이 기능이 포함되어 있습니다.
그래서 따로 RecycleView 버전을 Gradle 에 추가하고 Sync 하는 과정이 필요없더군요.


레이아웃 구성

홍드님의 레이아웃 XML을 직접 편집하는 방법도 연습해보았지만,
화면을 보면서 GUI 방식으로 UI를 수정하기도 해보았습니다.

양쪽 다 알아놓으면 편리하거든요.
특히 홍드로이드님이 영상 초반에 진행하는 리니어 레이아웃 변경은 GUI에서
아래 방법을 사용하면 됩니다.
Component Tree 창에서 1) ConstraintLayout 우클릭 - 2) Convert view - 3) LinearLayout - 4) Apply

그래서 아래와 같이 activity_main.xml 과,

뷰 홀더를 표시할 booklist.xml 레이아웃을 구성하였습니다.

각 컴포넌트 속성들은 Attribute 창에서 직접 입력했는데요.
GUI 프로그래밍을 하는 방식이라 좀 더 쉽게 할 수 있습니다.
대신, XML 편집 방식은 속성들을 어느정도 암기하고 있으면 속도가 훨씬 빠른 이점이 있지요.


각 xml 소스는 아래와 같습니다.

<activity_main.xml>


<?xml version="1.0" encoding="utf-8"?>
<LinearLayout
   
android:orientation="vertical"
   
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" >

    <
TextView
       
android:id="@+id/txtBook"
       
android:layout_width="match_parent"
       
android:layout_height="100sp"
       
android:text="책 정보"
       
android:textSize="20sp" />

    <
androidx.recyclerview.widget.RecyclerView
       
android:id="@+id/rvBookList"
       
android:layout_width="match_parent"
       
android:layout_height="wrap_content"
       
android:fadeScrollbars="true"
       
android:scrollbarFadeDuration="0"
       
android:scrollbars="vertical" />

</
LinearLayout>


<booklist.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="wrap_content"
   
android:layout_margin="5sp"
   
android:orientation="vertical">

    <
TextView
       
android:id="@+id/txtName"
       
android:layout_width="match_parent"
       
android:layout_height="wrap_content"
       
android:background="#CCFF90"
       
android:text="책 제목"
        
android:textSize="20sp" />

    <
LinearLayout
       
android:layout_width="match_parent"
       
android:layout_height="wrap_content"
       
android:orientation="horizontal">

        <
TextView
           
android:id="@+id/txtAuthor"
           
android:layout_width="wrap_content"
           
android:layout_height="wrap_content"
           
android:layout_weight="3"
           
android:background="#FFE57F"
           
android:text="저자"
           
android:textSize="20sp" />

        <
TextView
            
android:id="@+id/txtPage"
           
android:layout_width="wrap_content"
           
android:layout_height="wrap_content"
           
android:layout_weight="3"
           
android:background="#FF9E80"
           
android:text="페이지"
           
android:textSize="20sp" />

        <
TextView
           
android:id="@+id/txtScore"
           
android:layout_width="wrap_content"
           
android:layout_height="wrap_content"
           
android:layout_weight="3"
           
android:background="#EA80FC"
            
android:text="평점"
           
android:textSize="20sp" />

        <
Button
           
android:id="@+id/btnUp"
           
android:layout_width="50sp"
           
android:layout_height="wrap_content"
           
android:text="" />

        <
Button
           
android:id="@+id/btnDown"
           
android:layout_width="50sp"
           
android:layout_height="wrap_content"
           
android:layout_weight="1"
           
android:text="다운" />
    </
LinearLayout>

</
LinearLayout>


코딩 자동화 극 편리!

홍드로이드님의 영상을 보면 리사이클러 뷰 코드를 구성할떄 때 자동완성 기능들이 정말 대박인데요.
분명 소스량은 적지 않지만, 이 자동완성 기능이 코드의 70%는 작성해준것 같습니다.

자동 완성 노하우 역시 홍드로이드님 영상을 보면서 익히는게 좋습니다.
글로 설명드리는건 그리 효율적이지 않은 듯 하네요.

크레이가 개발한 코드는 아래와 같습니다.

< MainActivity.java  >


package com.cray.recylerpractice1;

import androidx.appcompat.app.AppCompatActivity;
import androidx.recyclerview.widget.LinearLayoutManager;
import androidx.recyclerview.widget.RecyclerView;

import android.os.Bundle;
import android.view.View;
import android.widget.Button;
import android.widget.LinearLayout;
import android.widget.TextView;

import org.w3c.dom.Text;

import java.util.ArrayList;
import java.util.Random;

public class MainActivity extends AppCompatActivity {
 
// 도서 목록
 
private ArrayList<BookItem> bookList;

 
// 도서정보를 보여줄 텍스트 뷰
 
TextView txtBookInfo;

 
// 리사이클러 뷰
 
private BookListAdapter bookListAdapter;
 
private RecyclerView bookListRecyclerView;
 
private LinearLayoutManager bookListLinearLayout;

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

 
// 1000 권의 책 정보 추가
 
Random random = new Random();
 
bookList = new ArrayList<>();

 
for (int i=1;i<=1000;++i) {
  
bookList.add(new BookItem(
   
"나래를 펼쳐라" + String.valueOf(i),
   
"크레이",
   
"크레이펍스",
   
random.nextInt(100) + 10,
    (
float) (Math.floor(random.nextDouble() * 50)/10)));
  }

 
// 리싸이클뷰 컨트롤 연결
 
bookListRecyclerView = (RecyclerView)findViewById(R.id.rvBookList);
 
bookListLinearLayout = new LinearLayoutManager(this);
 
bookListRecyclerView.setLayoutManager(bookListLinearLayout);
 
bookListAdapter =new BookListAdapter(bookList, MainActivity.this);
 
bookListRecyclerView.setAdapter(bookListAdapter);

 
// 책 정보 텍스트뷰 컨트롤 연결
 
txtBookInfo = findViewById(R.id.txtBook);
 }
}


< bookItem.java >


package com.cray.recylerpractice1;

// 1권 정보 클래스 정의
public class BookItem {

 
// 책 속성 정의
 
private String BookName// 책 제목
 
private String Author;    // 저자
 
private String Publisher; // 출판사
 
private int    Page;      // 페이지
 
private float  Score;     // 평점

 
// 생성자
 
// 마우스 우클릭 - Generate - Constructor 선택,
 //
변수 선택하면 코드 자동생성 ( 편하다 ! )
 
public BookItem(String bookName, String author, String publisher, int page, float score) {
 
BookName = bookName;
 
Author = author;
 
Publisher = publisher;
 
Page = page;
 
Score = score;
 }


 
// 변수값 얻기, 설정하기 함수 ( Getter & Setter )
 //
마우스 우클릭 - Generate - Getter and Setter 선택,
 //
변수 선택하면 변수값 얻기, 설정하기 코드 주루룩 자동 생성 ( 편하다 ! )

 
public String getBookName() {
 
return BookName;
 }

 
public void setBookName(String bookName) {
 
BookName = bookName;
 }

 
public String getAuthor() {
 
return Author;
 }

 
public void setAuthor(String author) {
 
Author = author;
 }

 
public String getPublisher() {
  
return Publisher;
 }

 
public void setPublisher(String publisher) {
 
Publisher = publisher;
 }

 
public int getPage() {
 
return Page;
 }

 
public void setPage(int page) {
 
Page = page;
 }

 
public float getScore() {
 
return Score;
 }

 
public void setScore(float score) {
 
Score = score;
 }
}


< bookListAdapter.java >


package com.cray.recylerpractice1;

import android.app.Activity;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import android.widget.Button;
import android.widget.TextView;
import android.widget.Toast;

import androidx.annotation.NonNull;
import androidx.recyclerview.widget.RecyclerView;

import com.google.android.material.internal.ViewUtils;

import java.util.ArrayList;

public class BookListAdapter extends RecyclerView.Adapter<BookListAdapter.BookViewHolder> {

 
private ArrayList<BookItem> bookList;

 
Toast viewToast;

 
MainActivity mActivity;

 
public BookListAdapter(ArrayList<BookItem> bookList, MainActivity activity) {
 
this.bookList = bookList;
 
mActivity = activity;
 }

 
private void ToastMsg(View view, String msg) {
 
if(viewToast!=null)viewToast.cancel();
 
viewToast = Toast.makeText(
   view.getContext(),
   msg,
  
Toast.LENGTH_SHORT);
 
viewToast.show();
 }

 
private void showBookInfo(BookItem book) {
 
mActivity.txtBookInfo.setText(
   book.getBookName()
    +
"\n저자 : "+ book.getAuthor()
    +
" / 페이지 : " + String.valueOf(book.getPage())
    +
" / 평점 : " + String.format("%.1f", book.getScore())
  );
 }

 
@NonNull
 @Override
 
public BookListAdapter.BookViewHolder onCreateViewHolder(@NonNull ViewGroup parent, int viewType) {
 
View view = LayoutInflater.from(parent.getContext())
   .inflate(
R.layout.booklist, parent, false);
 
BookViewHolder holder = new BookViewHolder(view);
 
return holder;
 }

 
@Override
 
public void onBindViewHolder(@NonNull BookListAdapter.BookViewHolder holder, int position) {
  holder.
txtName.setText(bookList.get(position).getBookName());
  holder.
txtAuthor.setText(bookList.get(position).getAuthor());
  holder.
txtPage.setText("" + String.valueOf(bookList.get(position).getPage()));
  holder.
txtScore.setText("💎" + String.format("%.1f", bookList.get(position).getScore()));

  holder.
itemView.setTag(position);

  holder.
itemView.setOnClickListener(new View.OnClickListener() {
  
@Override
  
public void onClick(View view) {
   
// BookItem thisBook = bookList.get((Integer) view.getTag());
   
BookItem thisBook = bookList.get(holder.getAdapterPosition());

    ToastMsg(view,
thisBook.getBookName());
    showBookInfo(
thisBook);
   }
  });

  holder.
btnUp.setOnClickListener(new View.OnClickListener() {
  
@Override
  
public void onClick(View view) {
   
BookItem thisBook = bookList.get(holder.getAdapterPosition());
   
thisBook.setScore(thisBook.getScore() + 0.1f);
    notifyDataSetChanged();
    showBookInfo(
thisBook);
   }
  });

  holder.
btnDown.setOnClickListener(new View.OnClickListener() {
  
@Override
  
public void onClick(View view) {
   
BookItem thisBook = bookList.get(holder.getAdapterPosition());
   
thisBook.setScore(thisBook.getScore() - 0.1f);
    notifyDataSetChanged();
    showBookInfo(
thisBook);
   }
  });
 }

 
@Override
 
public int getItemCount() {
 
return (bookList == null ? 0 : bookList.size());
 }

 
public class BookViewHolder extends RecyclerView.ViewHolder {
 
protected TextView txtName;
 
protected TextView txtAuthor;
 
protected TextView txtPage;
 
protected TextView txtScore;
 
protected Button btnUp;
 
protected Button btnDown;

 
public BookViewHolder(@NonNull View itemView) {
  
super(itemView);
  
this.txtName =(TextView)itemView.findViewById(R.id.txtName);
  
this.txtAuthor =(TextView)itemView.findViewById(R.id.txtAuthor);
  
this.txtPage =(TextView)itemView.findViewById(R.id.txtPage);
  
this.txtScore =(TextView)itemView.findViewById(R.id.txtScore);
  
this.btnUp = (Button)itemView.findViewById(R.id.btnUp);
  
this.btnDown = (Button)itemView.findViewById(R.id.btnDown);
  }
 }
}


코드에서는 성능을 시험해보기 위해 1,000개의 책 목록을 생성하였습니다.
스크롤을 빠르게 시도해보았는데 문제가 없더라구요
( 10,000개도 테스트해봤는데 문제는 없었습니다. )

책을 한권 선택하면 토스트메시지와 함께 책 정보를 메인 액티비티에 표시해주는데요.
접근성 문제로 메인 액티비티에 바로 접근할수가 없더군요.
여러 방법을 찾아보고 가장 쉬운 방법을 택했습니다.

어댑터를 생성할 때 메인액티비티의 this 속성을 넘겨주면,

bookListAdapter =new BookListAdapter(bookList, MainActivity.this);

어댑터쪽 소스에서 액티비티 속성을 정의하고

MainActivity mActivity;

어댑터 생성자에서 보관을 해 놓으면 됩니다.

public BookListAdapter(ArrayList<BookItem> bookList, MainActivity activity) {
 
this.bookList = bookList;
 
mActivity = activity;
}

그러면 이 변수를 통해 메인액티비티의 컨트롤들을 자유롭게 접근할 수 있더라구요.
아래는 어댑터쪽 클래스에 정의한 책정보를 보여주는 함수입니다.

private void showBookInfo(BookItem book) {
 
mActivity.txtBookInfo.setText(
  book.getBookName()
   +
"\n저자 : "+ book.getAuthor()
   +
" / 페이지 : " + String.valueOf(book.getPage())
   +
" / 평점 : " + String.format("%.1f", book.getScore())
 );
}

그 외에 추가로 구현한 것은 평점 조정 버튼인데요.
'업', '다운' 버튼을 클릭하면 평점이 바뀝니다.
onBindViewHolder 메소드에서 이를 구현했는데요.
이 함수가 바로 실시간으로 뷰홀더가 연결될 때마다 실행되는 함수입니다.

@Override
public void onBindViewHolder(@NonNull BookListAdapter.BookViewHolder holder, int position) {
 holder.
txtName.setText(bookList.get(position).getBookName());
 holder.
txtAuthor.setText(bookList.get(position).getAuthor());
 holder.
txtPage.setText("" + String.valueOf(bookList.get(position).getPage()));
 holder.
txtScore.setText("💎" + String.format("%.1f", bookList.get(position).getScore()));

 holder.
itemView.setTag(position);

 holder.
itemView.setOnClickListener(new View.OnClickListener() {
 
@Override
 
public void onClick(View view) {
  
// BookItem thisBook = bookList.get((Integer) view.getTag());
  
BookItem thisBook = bookList.get(holder.getAdapterPosition());

   ToastMsg(view,
thisBook.getBookName());
   showBookInfo(
thisBook);
  }
 });

 holder.
btnUp.setOnClickListener(new View.OnClickListener() {
 
@Override
 
public void onClick(View view) {
  
BookItem thisBook = bookList.get(holder.getAdapterPosition());
  
thisBook.setScore(thisBook.getScore() + 0.1f);
   notifyDataSetChanged();
   showBookInfo(
thisBook);
  }
 });

 holder.
btnDown.setOnClickListener(new View.OnClickListener() {
 
@Override
 
public void onClick(View view) {
  
BookItem thisBook = bookList.get(holder.getAdapterPosition());
  
thisBook.setScore(thisBook.getScore() - 0.1f);
   notifyDataSetChanged();
   showBookInfo(
thisBook
);
  }
 });
}


크레이도 안드로이드 앱 개발은 새로 배우는 입장이지만
이번 글은 초급 개발자분에게 어려운 내용일 수 있습니다.
하지만 반복해서 시도해보고 연습하다 익숙해지면 어느순간에는 분명 쉬워지실 겁니다 :)

아무쪼록 전체 소스 공개해드렸으니 필요하신 분에게 도움되시길 바랍니다.

그냥 예의상 글 읽어주신 방문자분께도 매우 감사드립니다 :)
복 받으세요~ 미리 크리스마스!