※ 이 게시글은 크레이의 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);
}
});
}
크레이도 안드로이드 앱 개발은 새로 배우는 입장이지만
이번 글은 초급 개발자분에게 어려운 내용일 수 있습니다.
하지만 반복해서 시도해보고 연습하다 익숙해지면 어느순간에는 분명 쉬워지실 겁니다 :)
아무쪼록 전체 소스 공개해드렸으니 필요하신 분에게 도움되시길 바랍니다.
그냥 예의상 글 읽어주신 방문자분께도 매우 감사드립니다 :)
복 받으세요~ 미리 크리스마스!
'코딩과 알고리즘' 카테고리의 다른 글
크레이의 앱개발 도전기 #7. 쓰레드편 (2) | 2022.12.08 |
---|---|
크레이의 앱개발 도전 #6. 프래그먼트?(Fragment) 와우~ (0) | 2022.12.06 |
크레이의 앱개발 도전기 #4. 갤러리에 사진 저장 (2) | 2022.11.27 |
크레이의 앱개발 도전기 #3. startActivityForResult 가 없어졌다구?! (2) | 2022.11.26 |
크레이의 앱개발 도전기 #2. 네비게이션 메뉴 (2) | 2022.11.21 |