[Kotlin] 12 - Android RecyclerView

2024. 6. 27. 10:29· 공부/Kotlin
목차
  1. 사용 프로젝트
  2. Recycler View?
  3. Recycler View의 장점
  4. Recycler View의 구성
  5. Adapter
  6. 실습 목표
  7. Recycler View 작업
  8. 아이템용 레이아웃 작성
  9. Adapter 작성
  10. Swipe Helper - 드래그 & 드롭 스와이프하여 삭제
  11. Callback 작성
  12. RecyclerView에 적용하기
  13. 클릭 이벤트
  14. 새 Activity 추가
  15. ViewHolder에 클릭 이벤트 추가
  16. Listener 코드 작성
반응형

애플리케이션 : 4대 컴포넌트

사용 프로젝트

  • 새 프로젝트
  • 이름 : ListEx
  • 패키지 : com.example.listpex
  • 새 프로젝트 생성 후 View Binding을 적용한다.
  1. build.gradle.kts(:app)
android {
	...
	buildFeatures.viewBinding = true
  1. MainActivity
class MainActivity : AppCompatActivity() {
    private val binding by lazy { ActivityMainBinding.inflate(layoutInflater) }
    override fun onCreate(savedInstanceState: Bundle?) {
    ...
    setContentView(binding.root)
    ViewCompat.setOnApplyWindowInsetsListener(binding.main)
	}

Recycler View?

  • Android SDK에는 ListView가 있음. 그러나 findViewById를 많이 해야 하는 구조로 순정 코드로 작성할 경우 스크롤 성능이 좋지 않았음.
  • 개발자들이 순정 ListView에 ViewHolder Pattern을 적용하여 스크롤 성능을 높였음
    • 이미 findViewById를 해 놓은 View가 있다면 이를 재활용하여 성능을 높임
  • 구글에서 개발자들이 만들어 쓰던 ViewHolder Pattern을 적용한 라이브러리를 배포함
    • -> Recycler View

Recycler View의 장점

  • View Holder Pattern을 공식 지원하는 것 외에
    • Grid List, Staggered Grid List(가변 높이의 Grid) 등의 다양한 유형을 제공함.
    • 한 리스트 안에서 두 가지 이상의 아이템 디자인을 사용할 수 있음.

Recycler View의 구성

Adapter

  • RecyclerView의 내부를 채우는 각 항목(리스트 아이템들)을 제공하는 역할
  • 데이터와 디자인이 필요함

ViewHolder에 디자인을 씌워 xml 파일에서 inflate를 해주면 해당 데이터가 bind 된다.

  • Data의 수만큼 ViewHolder가 필요할까?
    • 즉 ViewHolder 수 != Data 수
  • Viewholder를 재활용 하려면 TextView의 글자를 변경하고 imageView의 그림을 변경하는 일이 필요하다. ⇒ Bind

Viewholder를 조금 만들고 화면 밖에 나간 Viewholder를 비우고 다시 사용한다(껍데기는 놔두고 내부 내용만 바꾼다).

  • Adapter의 필수 기능
    • onCreateViewHolder: Data보다는 적은수지만 RecyclerView를 가득 채우고 스크롤 할 수 있을 만큼의 ViewHolde는 만들어야 한다. 어떤 xml 파일을 inflate할지도 지정해야 한다
    • onBindViewHolder: ViewHolder에 Data 의 내용을 넣는 작업.
    • getItemCount: Data의 개수를 알려주는 함수. 실제 Data가 많아도 여기서 0을 반환 하면 Recycler view에는 아무 것도 그려지지 않는다.
  • ViewHolder
    • RecyclerView.ViewHolder를 상속 받는 클래스로 나만의 ViewHolder 정의가 필요하다.
    • ViewHolder에서 실제 데이터와 아이템의 요소에 데이터를 넣는 과정을 함

실습 목표

  • 데이터 : 채팅 앱의 채팅 목록 리스트를 구현
    • 가상의 데이터 응용 {id: Int, image:Int, name:String, time:String}
    • 단 기본 ImageView는 인터넷 상의 이미지를 출력하지 못한다.
  1. 데이터 클래스 만들기

ChatRoomInfo

package com.example.listex 
import android.graphics.Color 
data class ChatRoomInfo( 
 val id:Int, 
 val image:Int, 
 val tint:Int, 
 val name:String, 
 val time:String 
) 
  1. 데이터 생성기 만들기
  • 서버에서 데이터를 받아오는 것처럼 가짜 데이터 생성기

DataGenerator

package com.example.listpex

import android.graphics.Color
import kotlin.random.Random
class DataGenerator{
    companion object{
        @JvmField
        val images = arrayOf(
            R.drawable.baseline_3p_24, R.drawable.baseline_ac_unit_24, R.drawable.baseline_accessibility_24,
            R.drawable.baseline_accessible_24, R.drawable.baseline_adb_24, R.drawable.baseline_account_circle_24
        )
        @JvmField
        val charPool: List<Char> = ('a'..'z') + ('A'..'Z') + ('0'..'9')
        @JvmStatic
        fun getRandomData():MutableList<ChatRoomInfo> {
            val count = Random.nextInt(1, 101)
            val list = MutableList(count){
                val imageIndex = Random.nextInt(images.size)
                val nameLength = Random.nextInt(3, 20)
                val randomString = (1..nameLength)
                    .map { charPool[Random.nextInt(0, charPool.size)] }
                    .joinToString("")
                val randomDay = Random.nextInt(1, 32)
                val color = Random.nextInt()
                ChatRoomInfo(it, images[imageIndex], color, randomString, "3월 ${randomDay}일")
            }
            return list
        }
    }
}


Recycler View 작업

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:id="@+id/main"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    tools:context=".MainActivity">

    <Button
        android:id="@+id/buttonRandom"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:text="랜덤 데이터 생성"
        app:layout_constraintEnd_toEndOf="parent"
        app:layout_constraintTop_toTopOf="parent" />
        
        <!-- RecyclerView 생성 -->
    <androidx.recyclerview.widget.RecyclerView
        android:id="@+id/recyclerView"
        android:layout_width="0dp"
        android:layout_height="0dp"
        app:layout_constraintBottom_toBottomOf="parent"
        app:layout_constraintEnd_toEndOf="parent"
        app:layout_constraintStart_toStartOf="parent"
        app:layout_constraintTop_toBottomOf="@+id/buttonRandom" />
</androidx.constraintlayout.widget.ConstraintLayout>
  • RecylerView 사용 시 주의점

Design에서 RecylerView를 드래그 앤 드롭 하여 생성하면 layout_width, layout_height가 픽셀폰의 너비로 하드코딩이 된다. 그러면 다른 휴대폰에서는 해당 요소들이 꽉 차지 않을 수가 있다. 0dp 로 설정하여 최대 너비와 높이로 적용될 수 있게 한다.

 

아이템용 레이아웃 작성

  • RecyclerView에 들어갈 하나의 아이템 레이아웃 → Adapter를 거쳐 해당 레이아웃의 아이템들이 여러개 생성됨
  • res/layout에서 마우스 우클릭 > New > Layout Resource File
  • 파일 이름 : item_chat_room.xml
  • Root element : androidx.constraintlayout.widget.ConstraintLayout

<?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"
    android:layout_width="match_parent"
    android:layout_height="wrap_content"
    android:padding="8dp"
    android:layout_margin="4dp">
    <ImageView
        android:id="@+id/imageView"
        android:layout_width="40dp"
        android:layout_height="40dp"
        android:contentDescription="User Icon"
        app:layout_constraintBottom_toBottomOf="parent"
        app:layout_constraintStart_toStartOf="parent"
        app:layout_constraintTop_toTopOf="parent"
        app:srcCompat="@drawable/baseline_3p_24" />
    <TextView
        android:id="@+id/textViewName"
        android:layout_width="0dp"
        android:layout_height="wrap_content"
        android:layout_marginStart="16dp"
        android:ellipsize="end"
        android:maxLines="1"
        android:text="이름 들어갈 자리"
        android:textColor="@color/black"
        android:textSize="18sp"
        app:layout_constraintBottom_toTopOf="@+id/textViewTime"
        app:layout_constraintEnd_toEndOf="parent"
        app:layout_constraintStart_toEndOf="@+id/imageView"
        app:layout_constraintTop_toTopOf="parent" />
    <TextView
        android:id="@+id/textViewTime"
        android:layout_width="0dp"
        android:layout_height="wrap_content"
        android:layout_marginTop="4dp"
        android:text="시간 들어갈 자리"
        app:layout_constraintBottom_toBottomOf="parent"
        app:layout_constraintEnd_toEndOf="parent"
        app:layout_constraintStart_toStartOf="@+id/textViewName"
        app:layout_constraintTop_toBottomOf="@+id/textViewName" />
</androidx.constraintlayout.widget.ConstraintLayout>

메인 레이아웃의 constraint 세로 리스트를 위해 높이를 주고 wrap_content로 주고 padding, margin을 좀 준다.

android:ellipsize=”end” , android:maxLines=”1”: 너무 길어 지면 줄바꿈 없이 … 처리

Adapter 작성

  • com.example.listex에 new > kotlin class
  • 이름은 ChatRoomAdapter
  • 다음 순서로 작업한다.
    • 1) ViewHolder 클래스 작성
    • 2) ChatRoomAdapter가 RecyclerView의 Adapter를 상속받도록 구현
  1. ViewHolder 클래스 작성
package com.example.listpex

import android.content.res.ColorStateList
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import android.widget.ImageView
import android.widget.TextView
import androidx.recyclerview.widget.RecyclerView
import java.util.Collections

// ChatAdapter가 RecyclerView의 Adapter를 상속받도록 구현
class ChatRoomAdapter: RecyclerView.Adapter<ChatRoomAdapter.ChatRoomViewHolder>(){ // RecyclerView.Adapter를 상속받고 우리가 만든 Custom ViewHolder를 제네릭으로 넣어줌
    inner class ChatRoomViewHolder(view: View): RecyclerView.ViewHolder(view){ // View Holder를 상속받음
        val imageView: ImageView = view.findViewById(R.id.imageView) // 레이아웃의 요소를 참조
        val textViewName: TextView = view.findViewById(R.id.textViewName)
        val textViewTime: TextView = view.findViewById(R.id.textViewTime)
        private lateinit var item:ChatRoomInfo // 데이터 클래스를 참조
        fun bind(item:ChatRoomInfo){ // 데이터를 View에 바인딩
            this.item = item
            imageView.setImageResource(item.image)
            imageView.imageTintList = ColorStateList.valueOf(item.tint) // 생략해도 된다.
            textViewName.text = item.name
            textViewTime.text = item.time
        }

    }

    private var data = mutableListOf<ChatRoomInfo>() // 어댑터의 데이터 목록

    fun updateData(data:MutableList<ChatRoomInfo>){ // 데이터를 업데이트
        this.data = data
        notifyItemRangeChanged(0, data.size, this.data) // 어댑터에 데이터가 변경되었음을 알림
    } // notify의 함수 대부분이 화면을 새로 그려라 라는 의미

    fun swapItem(from:Int, to:Int){
        Collections.swap(data, from, to)
        notifyItemMoved(from, to) // 아이템이 이동되었음을 알림
    }

    fun removeItem(index:Int){
        data.removeAt(index)
        notifyItemRemoved(index) // 아이템이 제거되었음을 알림
    }

    override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ChatRoomViewHolder { // 충분한 개수의 ViewHolder들을 생성하고 그 ViewHolder를 반환
        val view = LayoutInflater
            .from(parent.context)
            .inflate(R.layout.item_chat_room, parent, false) // item layout을 inflate
        return ChatRoomViewHolder(view) // ViewHolder 클래스 이름
    } // ViewHolder를 생성하고 그 ViewHolder를 반환 , 해당 코드는 거의 고정임

    override fun getItemCount(): Int { // RecyclerView가 표시할 아이템의 개수를 반환
        return data.size
    }  // data.size에 따라 화면 상에 최대로 표시할 아이템의 개수를 결정됨

    override fun onBindViewHolder(holder: ChatRoomViewHolder, position: Int) {
        // position 매개변수를 사용하여 데이터 세트에서 데이터를 찾고, 그 데이터를 ViewHolder의 View에 바인딩
        holder.bind(data[position])
    } // RecylerView가 아이템의 데이터를 표시해야 할 때 호출
}

Adapter 필수 함수 : onCreateViewHolder, getItemCount, onBindViewHolder

별도의 공간에 ViewHolder를 미리 만들어지고 화면을 보여줄 때 화면 상에 공간에 넣어준다.

스크롤 시 나머지 대기 중인 ViewHolder로 변경된다.

  1. MainActivity
class MainActivity : AppCompatActivity() {
    private val adapter = ChatRoomAdapter() // Adapter 생성

    override fun onCreate(savedInstanceState: Bundle?) {
				...
				
        binding.recyclerView.layoutManager = LinearLayoutManager(this) // RecyclerView에 LinearLayoutManager 설정
        binding.recyclerView.adapter = adapter // RecyclerView 초기화

        binding.buttonRandom.setOnClickListener {
            adapter.updateData(DataGenerator.getRandomData())
        } // RecyclerView 에 데이터 전달하기
    }
}

Swipe Helper - 드래그 & 드롭 스와이프하여 삭제

Callback 작성

class MainActivity : AppCompatActivity() {  
    private val adapter = ChatRoomAdapter() // Adapter 생성
    private val itemTouchCallback = object:ItemTouchHelper.SimpleCallback(
        ItemTouchHelper.UP or ItemTouchHelper.DOWN, // 드래그 방향
        ItemTouchHelper.LEFT or ItemTouchHelper.RIGHT // 스와이프 방향
    ){
        override fun onMove( // 아이템 이동 시 호출
            recyclerView: RecyclerView,
            viewHolder: RecyclerView.ViewHolder,
            target: RecyclerView.ViewHolder
        ): Boolean {
            adapter.swapItem(viewHolder.layoutPosition, target.layoutPosition)
            return true
        }
        override fun onSwiped(viewHolder: RecyclerView.ViewHolder, direction: Int) { // 스와이프 이벤트 발생 시 호출
            adapter.removeItem(viewHolder.layoutPosition)
        }
    }

RecyclerView에 적용하기

override fun onCreate(savedInstanceState: Bundle?) {
	...
	binding.recyclerView.layoutManager = LinearLayoutManager(this) // RecyclerView에 LinearLayoutManager 설정
  binding.recyclerView.adapter = adapter // RecyclerView 초기화
  ItemTouchHelper(itemTouchCallback).attachToRecyclerView(binding.recyclerView) // ItemTouchHelper를 RecyclerView에 연결

클릭 이벤트

새 Activity 추가

  • 리스트 아이템을 클릭하면 ChatRoomActivity로 이동하도록 구현.
  • com.example.listpex 에서 우클릭 > New > Activity > EmptyActivity
  • 이름 ChatRoomActivity
  • 새로 생성된 Activity에도 View binding을 적용한다.
class ChatRoomActivity : AppCompatActivity() {
    private val binding by lazy { ActivityChatRoomBinding.inflate(layoutInflater) }

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        enableEdgeToEdge()
        setContentView(binding.root)
        ..
    }

ViewHolder에 클릭 이벤트 추가

class ChatRoomAdapter: RecyclerView.Adapter<ChatRoomAdapter.ChatRoomViewHolder>(){ // RecyclerView.Adapter를 상속받고 우리가 만든 Custom ViewHolder를 제네릭으로 넣어줌
	inner class ChatRoomViewHolder(view: View): RecyclerView.ViewHolder(view){ // View Holder를 상속받음
		...
		private lateinit var item:ChatRoomInfo // 데이터 클래스를 참조
		init {
            view.setOnClickListener {
                Log.i("ViewHolder", "${this.layoutPosition}th item")
                listener?.onItemClick(item)
            }
        }
		

Event를 누가 처리 할 것인가?

  • 클릭 이벤트는 ViewHolder에서 발생함.
  • 클릭이 발생하면 startActivity를 해야 함. -> 이는 MainActivity에서 할 수 있음.
  • ViewHolder의 이벤트를 Activity로 전달해야 하는 상황.
  1. Adapter에 listener interface를 추가한다.
  2. Adapter가 listener를 받아 저장해 둔다.
  3. ViewHolder에서 listener에게 알린다.
  4. Mainactivity가 interface를 Implement 한 뒤 자신을 listener로 등록한다.

클릭 이벤트는 ChatRoomViewHolder에서 Listener를 하는데 처리는 MainActivity에 전달을 해주어야 한다.

ChatRoomViewHolder → MainActivity 데이터 전달 : 역방향 데이터 전달

Listener 코드 작성

  • interface 를 추가하고
  • 해당 타입의 Property 및 설정 함수를 Adapter에 추가(ChatRoomAdapter)
class ChatRoomAdapter: RecyclerView.Adapter<ChatRoomAdapter.ChatRoomViewHolder>(){ // RecyclerView.Adapter를 상속받고 우리가 만든 Custom ViewHolder를 제네릭으로 넣어줌
    inner class ChatRoomViewHolder(view: View): RecyclerView.ViewHolder(view){ // View Holder를 상속받음
	    ...
    }
    
    ....
    
    fun interface OnItemClickListener { // 클릭 이벤트를 처리하기 위한 인터페이스
        fun onItemClick(item:ChatRoomInfo?)
    }

    private var listener:OnItemClickListener? = null
    fun setItemClickListener(listener:OnItemClickListener?){ // 클릭 이벤트를 처리하는 리스너를 설정
        this.listener = listener
    }
  • ChatAdapter에 listener 전달 (MainActivity)
class MainActivity : AppCompatActivity() {
override fun onCreate(savedInstanceState: Bundle?) {

	....
	adapter.setItemClickListener {
	            if(it != null){
	                val intent = Intent(this, ChatRoomActivity::class.java)
	                intent.putExtra("id", it.id)
	                intent.putExtra("name", it.name)
	                startActivity(intent)
	            }
	        }
	   }
}
  • ChatRoomActivity - activity_chat_room.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:id="@+id/main"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    tools:context=".ChatRoomActivity">
    <TextView
        android:id="@+id/textViewChatUser"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:layout_marginTop="24dp"
        android:text="TextView"
        android:textAppearance="@style/TextAppearance.AppCompat.Display1"
        app:layout_constraintEnd_toEndOf="parent"
        app:layout_constraintStart_toStartOf="parent"
        app:layout_constraintTop_toTopOf="parent" />
</androidx.constraintlayout.widget.ConstraintLayout>
  • ChatRoomActivity.kt
class ChatRoomActivity : AppCompatActivity() {
    private val binding by lazy { ActivityChatRoomBinding.inflate(layoutInflater) }

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        enableEdgeToEdge()
        setContentView(binding.root)
        ViewCompat.setOnApplyWindowInsetsListener(binding.main) { v, insets ->
            val systemBars = insets.getInsets(WindowInsetsCompat.Type.systemBars())
            v.setPadding(systemBars.left, systemBars.top, systemBars.right, systemBars.bottom)
            insets
        }

        binding.textViewChatUser.text = intent.getStringExtra("name")
    }
}



안드로이드에서 View Model이 중요한 이유

Thread 생성 가능

  • 문제점 : UI 업데이트는 Main Thread에서만 가능하다. HTTP 통신과, 데이터베이스 쿼리, 카메라 영상 처리 → 별도의 Thread에서 수행
  • 별도의 Thread 사용하지 않고 Main Thread에서 동기식으로 처리 시 클릭을 해도 응답이 올 때 까지 작동을 하지 않음 → 사용자는 앱이 뻗을 줄 알아버린다.

→ ViewModel 사용 시 딜레마 해결 가능

 

반응형
저작자표시 비영리 (새창열림)
  1. 사용 프로젝트
  2. Recycler View?
  3. Recycler View의 장점
  4. Recycler View의 구성
  5. Adapter
  6. 실습 목표
  7. Recycler View 작업
  8. 아이템용 레이아웃 작성
  9. Adapter 작성
  10. Swipe Helper - 드래그 & 드롭 스와이프하여 삭제
  11. Callback 작성
  12. RecyclerView에 적용하기
  13. 클릭 이벤트
  14. 새 Activity 추가
  15. ViewHolder에 클릭 이벤트 추가
  16. Listener 코드 작성
'공부/Kotlin' 카테고리의 다른 글
  • [Kotlin] 14 - Permissions
  • [Kotlin] 13 - Shared preference & ViewModel
  • [Kotlin] 9 - Widget And Listener
  • [Kotlin] 11 - Activity And Intent
Future0_
Future0_
rm -rf /
Future0_
Luna Developer Blog
Future0_
전체
오늘
어제
  • 분류 전체보기 (112)
    • 프로그래밍 (4)
      • 알고리즘 (4)
    • 보안 (14)
      • Dreamhack (4)
      • Hackthebox (1)
      • Webhacking (9)
    • 프로젝트 (4)
    • 공부 (80)
      • Database (2)
      • Python (11)
      • System (4)
      • Java (13)
      • JSP (13)
      • Spring (11)
      • Kotlin (16)
      • 자료구조 (10)
      • 기계학습 (0)
    • Docker (4)
    • Github (2)
    • Tip (1)
    • 잡담 (2)

블로그 메뉴

  • 홈
  • 태그

공지사항

인기 글

태그

  • Computer science
  • React
  • 디버깅키해시
  • jsp
  • cs
  • Java
  • 컴퓨터
  • dreamhack
  • native app
  • 코틀린기본문법
  • docker
  • shared preference
  • 알고리즘
  • ViewModel
  • Android Studio
  • 자료구조
  • webhacking
  • 자바빈즈
  • android studio 삭제
  • Database
  • 1.9.22
  • 키 해시
  • 상속
  • Kotlin
  • SpringBoot
  • 프로그래밍
  • Python
  • 보안
  • spring
  • api 통신

최근 댓글

최근 글

hELLO · Designed By 정상우.v4.2.0
Future0_
[Kotlin] 12 - Android RecyclerView
상단으로

티스토리툴바

단축키

내 블로그

내 블로그 - 관리자 홈 전환
Q
Q
새 글 쓰기
W
W

블로그 게시글

글 수정 (권한 있는 경우)
E
E
댓글 영역으로 이동
C
C

모든 영역

이 페이지의 URL 복사
S
S
맨 위로 이동
T
T
티스토리 홈 이동
H
H
단축키 안내
Shift + /
⇧ + /

* 단축키는 한글/영문 대소문자로 이용 가능하며, 티스토리 기본 도메인에서만 동작합니다.