불순분자 Kotlin [16] - RecyclerView

불순분자들·2022년 8월 11일
0

List App 만들기

목록 보기
16/18

RecyclerView

RecyclerView는 안드로이드에서 사용할 때, 개발자가 뷰의 모양을 정의하고 대량의 데이터 목록을 화면에 출력해 동적으로 표현한다.
RecyclerView는 ListView와는 다르게 재사용성이 높은 위젯이다.

RecyclerView를 구현하기 위해 사용한 클래스는 다음과 같다.

  • ViewHolder : 각 항목의 뷰를 재활용하기 위해 보관하는 클래스
  • LayoutManager : 목록을 어떻게 배치할지 결정
  • Adapter : 뷰를 뷰의 데이터에 대치

1. RecyclerView를 포함한 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:layout_width="match_parent"
    android:layout_height="match_parent"
    tools:context=".ListMainAcitvity">

    <TextView
        android:id="@+id/textView2"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:layout_marginStart="16dp"
        android:fontFamily="@font/kotra_hope"
        android:text="List - Life"
        android:textColor="#00ABF6"
        android:textSize="32sp"
        android:textStyle="bold"
        app:layout_constraintBottom_toBottomOf="@+id/imageView"
        app:layout_constraintStart_toEndOf="@+id/imageView"
        app:layout_constraintTop_toTopOf="@+id/imageView" />

    <ImageView
        android:id="@+id/imageView"
        android:layout_width="60dp"
        android:layout_height="60dp"
        android:layout_marginStart="32dp"
        android:layout_marginTop="24dp"
        app:layout_constraintStart_toStartOf="parent"
        app:layout_constraintTop_toTopOf="parent"
        app:srcCompat="@drawable/img_list" />

    <View
        android:id="@+id/view"
        android:layout_width="0dp"
        android:layout_height="1dp"
        android:layout_marginStart="32dp"
        android:layout_marginTop="16dp"
        android:layout_marginEnd="32dp"
        android:background="#403838"
        app:layout_constraintEnd_toEndOf="parent"
        app:layout_constraintStart_toStartOf="parent"
        app:layout_constraintTop_toBottomOf="@+id/imageView" />

    <androidx.recyclerview.widget.RecyclerView
        android:id="@+id/rv_list_life"
        android:layout_width="0dp"
        android:layout_height="0dp"
        android:layout_marginStart="16dp"
        android:layout_marginTop="16dp"
        android:layout_marginEnd="16dp"
        android:layout_marginBottom="16dp"
        app:layoutManager="androidx.recyclerview.widget.LinearLayoutManager"
        app:layout_constraintBottom_toBottomOf="parent"
        app:layout_constraintEnd_toEndOf="parent"
        app:layout_constraintStart_toStartOf="parent"
        app:layout_constraintTop_toBottomOf="@+id/view"
        tools:listitem="@layout/list_item" />
</androidx.constraintlayout.widget.ConstraintLayout>

메인 레이아웃은 ConstrainLayout으로 하고 내부에 RecyclerView를 넣어주었다.

app:layoutManager="androidx.recyclerview.widget.LinearLayoutManager"

이 코드는 리사이클러뷰의 위젯배치를 어떻게 할 것이냐를 나타낸다.
이게 없으면 실행 시 리스트가 표현되지 않는다. 적어도 난 그랬다.

tools:listitem="@layout/list_item"

이 코드는 각각의 listItem을 잘 표현했는지 가시적으로 보여주기 위해서 생긴 코드이다.

그리고 RecyclerView의 id는 반드시 부여하도록하자. 왜냐하면 어댑터를 등록할 때, id가 없으면 리사이클러뷰에 어댑터를 생성할 수 없다.

2. 각각의 리스트 아이템 xml 구현

<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_width="match_parent"
    android:layout_height="wrap_content"
    android:layout_margin="16dp"
    android:orientation="horizontal">

    <LinearLayout
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:layout_weight="1"
        android:orientation="vertical">

        <TextView
            android:id="@+id/tv_content"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:fontFamily="@font/kotra_hope"
            android:textSize="24sp"
            android:textColor="@color/black"
            android:text="내용란"/>

        <TextView
            android:id="@+id/tv_date"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:fontFamily="@font/kotra_hope"
            android:textSize="16sp"
            android:textColor="#4E4D4D"
            android:text="2022-08-10 03:03"/>


    </LinearLayout>

    <ImageView
        android:id="@+id/btn_remove"
        android:layout_width="30dp"
        android:layout_height="30dp"
        android:src="@drawable/img_remove"/>

</LinearLayout>

위와 같이 리스트 하나의 디자인을 구현해주어야한다. 그러면 그 하나하나의 디자인이 리사이클러뷰에 계속해서 추가되어지게 된다.

3. list에 들어갈 model 클래스 생성

class ListInfo {
    var listContent: String = "" // 메모 내용
    var listDate: String = "" // 메모 일자
}

위와 같이 리스트에 들어갈 정보를 넣어줄 클래스 하나를 생성해야한다.
물론, 위 뿐만아니라 listLike, listNumber 등 어떤 정보를 넣었는지에 따라 model클래스 구현은 달라질 것이다.

4. Adapter 생성하기

Adapter는 데이터 테이블을 목록 형태로 보여주기 위해 사용되는 것으로 데이터를 다양한 형식의 리스트를 보여주기 위해서 데이터와 리스트뷰 사이에 존재하는 것이다.
-> 데이터와 리스트 뷰 사이의 통신을 위한 다리 역할을 함.
-> 데이터의 원본을 받아 관리하고, 어댑터뷰가 출력할 수 있는 형태로 데이터를 제공하는 중간 객체 역할을 함.

import android.view.LayoutInflater
import android.view.ViewGroup
import androidx.recyclerview.widget.RecyclerView
import com.android.listapp.databinding.ListItemBinding
import com.android.listapp.model.ListInfo

// ViewHolder는 각 ItemList를 저장하는 객체이다.

class TodoAdapter : RecyclerView.Adapter<TodoAdapter.TodoViewHolder>() {
    private var lstTodo : ArrayList<ListInfo> = ArrayList()

    init {
        // init은 클래스가 생성될 때 가장 먼저 호출됨. 생성자 보다 먼저 호출.
        // 샘플 데이터
        val todoItem = ListInfo()
        todoItem.listContent = "점심 밥 먹기"
        todoItem.listDate = "2022-06-01 12:23"
        lstTodo.add(todoItem)

        val todoItem2 = ListInfo()
        todoItem2.listContent = "아침 밥 먹기"
        todoItem2.listDate = "2022-06-02 08:23"
        lstTodo.add(todoItem2)

        val todoItem3 = ListInfo()
        todoItem3.listContent = "저녁 밥 먹기"
        todoItem3.listDate = "2022-06-01 18:23"
        lstTodo.add(todoItem3)
    }


    class TodoViewHolder(private val binding: ListItemBinding) : RecyclerView.ViewHolder(binding.root) {
        fun bind(listItem : ListInfo) {
            // 리스트 뷰 데이터를 UI에 연동
            binding.tvContent.setText(listItem.listContent)
            binding.tvDate.setText(listItem.listDate)

            // 리스트 삭제 버튼 클릭 연동
            binding.btnRemove.setOnClickListener {
                // 쓰레기통 이미지 클릭 시 이벤트 처리
            }
        }

    }
    // ViewHoler가 만들어질 때. -> 뷰 홀더가 생성됨( 각 리스트 아이템 1개씩 구성될 때마다 이 오버라이드 메소드가 호출 됨 )
    override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): TodoAdapter.TodoViewHolder {
        val binding = ListItemBinding.inflate(LayoutInflater.from(parent.context), parent, false)
        return TodoViewHolder(binding)
    }

    // ViewHoler가 결합될 때. -> 뷰 홀더가 바인딩( 결합 )이 이루어질 때 해줘야할 처리들을 구현. position은 index 즉 배열과 비슷함.
    override fun onBindViewHolder(holder: TodoAdapter.TodoViewHolder, position: Int) {
        holder.bind(lstTodo[position])
    }

    // list의 item의 개수를 어댑터에 알려주어야함.
    override fun getItemCount(): Int {
        return lstTodo.size
    }
}

위 코드는 어댑터뷰를 내부클래스에 구현하는 방법이다.

하나하나 쪼개서 코드를 리뷰해보자.

class TodoAdapter : RecyclerView.Adapter<TodoAdapter.TodoViewHolder>() {
    private var lstTodo : ArrayList<ListInfo> = ArrayList()

TodoAdapter는 RecyclerView의 어댑터 클래스를 상속받는다.
<>는 제너릭 타입으로 다양한 타입의 객체에 대하여 재사용하는 프로그래밍 기법으로 java 1.5부터 추가되었다. <>안에는 타입이 들어간다.

lstTodo : ArrayList는 결과적으로 리스트에 담길 정보를 배열로 저장한다.

init {
        // init은 클래스가 생성될 때 가장 먼저 호출됨. 생성자 보다 먼저 호출.
        // 샘플 데이터
        val todoItem = ListInfo()
        todoItem.listContent = "점심 밥 먹기"
        todoItem.listDate = "2022-06-01 12:23"
        lstTodo.add(todoItem)

        val todoItem2 = ListInfo()
        todoItem2.listContent = "아침 밥 먹기"
        todoItem2.listDate = "2022-06-02 08:23"
        lstTodo.add(todoItem2)

        val todoItem3 = ListInfo()
        todoItem3.listContent = "저녁 밥 먹기"
        todoItem3.listDate = "2022-06-01 18:23"
        lstTodo.add(todoItem3)
    }

init은 클래스가 생성될 때 가장 먼저 호출되기 때문에 더미 데이터를 넣을 때 사용하였다.

class TodoViewHolder(private val binding: ListItemBinding) : RecyclerView.ViewHolder(binding.root) {
        fun bind(listItem : ListInfo) {
            // 리스트 뷰 데이터를 UI에 연동
            binding.tvContent.setText(listItem.listContent)
            binding.tvDate.setText(listItem.listDate)

            // 리스트 삭제 버튼 클릭 연동
            binding.btnRemove.setOnClickListener {
                // 쓰레기통 이미지 클릭 시 이벤트 처리
            }
        }

    }

위 코드는 내부클래스에 ViewHolder를 구현한 것이다.
이번 예제를 진행하면서 ViewBinding을 사용했기 때문에 findViewById는 필요 없다.

binding 변수 안에는 ListItemBinding이 들어가 있는데 내가 생성했던 xml 이름이 list_item이어서 낙타체로 ListItem으로 변환해 ViewBinding을 사용할 수 있다.

그 안에 함수 bind라는 것을 만들어 listItem 변수 안에 ListInfo들을 담아주었다.

// ViewHoler가 만들어질 때. -> 뷰 홀더가 생성됨( 각 리스트 아이템 1개씩 구성될 때마다 이 오버라이드 메소드가 호출 됨 )
    override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): TodoAdapter.TodoViewHolder {
        val binding = ListItemBinding.inflate(LayoutInflater.from(parent.context), parent, false)
        return TodoViewHolder(binding)
    }

뷰 홀더가 생성될때마다 이 메소드가 호출되고, TodoViewHolder에 리턴시켜 각 정보들을 setText할 수 있다.

// ViewHoler가 결합될 때. -> 뷰 홀더가 바인딩( 결합 )이 이루어질 때 해줘야할 처리들을 구현. position은 index 즉 배열과 비슷함.
    override fun onBindViewHolder(holder: TodoAdapter.TodoViewHolder, position: Int) {
        holder.bind(lstTodo[position])
    }

onBindViewHolder는 스크롤을 하면서 맨 위에있던 뷰 홀더( 레이아웃 )이 맨 아래로 이동해 재사용하는 것이다. 하지만 그 데이터는 새롭게 바뀌게 된다.
새롭게 보여질 데이터의 인덱스 값은 position의 이름으로 사용 가능하다.

// list의 item의 개수를 어댑터에 알려주어야함.
    override fun getItemCount(): Int {
        return lstTodo.size
    }

마지막은 list의 item의 개수를 어댑터에게 알려주어야 한다.
그래서 결과적으로 데이터가 담겨있는 lstTodo의 .size를 사용하여 크기를 전달해준다.

5. Activity에서 사용하기

import androidx.appcompat.app.AppCompatActivity
import android.os.Bundle
import com.android.listapp.adapter.TodoAdapter
import com.android.listapp.databinding.ActivityListMainBinding

class ListMainAcitvity : AppCompatActivity() {
    private lateinit var binding: ActivityListMainBinding
    private lateinit var listAdapter: TodoAdapter

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        binding = ActivityListMainBinding.inflate(layoutInflater)
        setContentView(binding.root)

        // 어댑터 인스턴스 생성
        listAdapter = TodoAdapter()

        // 리사이클러뷰에 어댑터 생성
        binding.rvListLife.adapter = listAdapter
    }
}

lateinit은 초기값을 설정하지 않고, 초기화를 미루는 방법으로 선언과 동시에 초기화하지 않아도 된다.

위 코드역시 ViewBindingd을 사용했다. 결과적으로 말하면

private lateinit var binding: ActivityListMainBinding

binding = ActivityListMainBinding.inflate(layoutInflater)
        setContentView(binding.root)

이 3줄의 코드가 레이아웃과 연결한 것이다.

profile
장래희망 : 침대 위 녹아든 치즈

0개의 댓글