본문 바로가기

Android

단어장앱-1 (RecyclerView)

이번 포스팅에서는 단어장 앱 개발에 대해 설명드리고자 합니다.

2편으로 나눠서 설명할 것이며 이유는 담는 내용이 많기 때문에 나눠서 설명하겠습니다.

 

* RecyclerView 

1) xml에 RecyclerView를 선언한다.

2) Collection으로 만드는 model 하나를 구현한다. (data class)

3) item으로 쓸 xml을 만들고 내부 위젯들을 구성한다.

4) Adapter를 생성한다.  [ㄱ. onCreateViewHolder ㄴ. onBindViewHolder ] 필수!!

 

5) onCreateViewHolder에서 4의 item.xml을 inflate한다.

6) onBindViewHolder에서 list의 postion 값으로 하나의 객체를 뽑아온다.

7) holder를 통해 요소에 접근할 수 있고 ViewHolder 클래스에서 bind 함수를 구현하여 

요소에 접근할 수도 있습니다.

8) list의 itemClickListener를 구현하여 각 요소들 접근을 가능하도록 한다.

9) RecyclerView에 해당 adapter를 꼽고 LayoutManager를 통해 어떻게 구조를 짜서 보여줄 지 정리한다.

 

다음 내용을 그대로 실행하면 되지만 너무 많고 어려운 것 같습니다.   따라서 코드를 통해 하나씩 설명해보겠습니다.


1) xml에 RecyclerView를 선언한다.

    
    <View
        android:id="@+id/line"
        android:layout_width="match_parent"
        android:layout_height="1dp"
        android:layout_marginTop="24dp"
        android:background="#BCBBBB"
        app:layout_constraintTop_toBottomOf="@id/meanTextView" />
        
    <androidx.recyclerview.widget.RecyclerView
        android:id="@+id/wordRecyclerView"
        android:layout_width="match_parent"
        android:layout_height="0dp"
        app:layout_constraintBottom_toBottomOf="parent"
        app:layout_constraintTop_toBottomOf="@+id/line" />

    <com.google.android.material.floatingactionbutton.FloatingActionButton
        android:id="@+id/addButton"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:backgroundTint="#F8E540"
        android:src="@drawable/baseline_add_24"
        app:layout_constraintBottom_toBottomOf="parent"
        app:layout_constraintEnd_toEndOf="parent" />

 

다음처럼 activity_main.xml에서 RecyclerView를 선언합니다.  높이는 0dp로 두었지만 constraint 조건에 따라

line이라는 view 아래부터 밑바닥까지 쭉 정의가 됩니다.


2) Collection으로 만드는 model 하나를 구현한다. (data class)

package com.example.voca_book.models

// data class -> 상속 불가능
// toString, hashcode, equals,
data class Word(
    val text : String,
    val mean : String,
    val type : String,
)

 

data class로 만들어놓고 이 Word라는 클래스 즉, 하나의 객체에서 사용될 요소들(text, mean, type)을 설정합니다.


3) item으로 쓸 xml을 만들고 내부 위젯들을 구성한다.

 

[word_item.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="wrap_content"
    android:padding="24dp">

    <TextView
        android:id="@+id/textTextView"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:textSize="20sp"
        app:layout_constraintEnd_toEndOf="parent"
        app:layout_constraintTop_toTopOf="parent"
        tools:text="단어" />

    <TextView
        android:id="@+id/meanTextView"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:layout_marginTop="8dp"
        app:layout_constraintEnd_toEndOf="@id/textTextView"
        app:layout_constraintTop_toBottomOf="@id/textTextView"
        tools:text="뜻" />

    <com.google.android.material.chip.Chip
        android:id="@+id/typeChip"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        tools:text="품사"
        tools:ignore="MissingConstraints" />
</androidx.constraintlayout.widget.ConstraintLayout>

 

실제 리스트에 담길 요소들입니다.   UI 구성을 잘 짜두시기 바랍니다.


4) Adapter를 생성한다.  [ㄱ. onCreateViewHolder ㄴ. onBindViewHolder ] 필수!!

// ItemClickListener 추가
class WordAdapter(
    private val list: MutableList<Word>,
    private val itemClickListener: ItemClickListener? = null
): RecyclerView.Adapter<WordAdapter.ViewHolder>() {

    // onBindViewHolder에 binding으로 접근하기 위해서 binding을 뷰홀더에 넣고 뷰홀더 부분에서는 item.xml 바인딩을 두어
    // holder에서도 바인딩을 접근할 수 있게 만듬
    class ViewHolder(private val binding: WordItemBinding) : RecyclerView.ViewHolder(binding.root){
        // onBindViewHolder의 역할은 데이터와 뷰의 바인딩 작업을 하는건데 이걸 ViewHolder 클래스에서 bind라는 함수를 만들어서
        // 구현할 수 있음
        fun bind(word: Word){
            binding.apply {
                textTextView.text = word.text
                meanTextView.text = word.mean
                typeChip.text = word.type
            }
            // itemView는 뷰홀더에서 들어온 아이템 뷰 들을 바로 접근할 수 있는 인자
        }
    }

    override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ViewHolder {
        // viewHolder만들기
        // item.xml 연결하기
        // ViewHolder를 생성하는데 필요한 것
        /*
        1) parent.context.getSystemService에서 Context.LAYOUT_INFLATER_SERVICE 로부터 LayoutInflater를 생성해온다.
        2) 바인딩 작업을 해야하는데, Item.xml을 바인딩으로 가져오되, inflate할 때에는 앞서 찾은 inflater 넣어주기, parent, false
        3) RecyclerView.ViewHolder를 상속받아 구현하는 부분에 대해서 itemView랑 현 item.xml의 binding.root를 매핑한다.
         */
        val inflater = parent.context.getSystemService(Context.LAYOUT_INFLATER_SERVICE) as LayoutInflater
        val binding = WordItemBinding.inflate(inflater,parent,false)
        return ViewHolder(binding)
    }

    override fun getItemCount(): Int {
        return list.size
    }

    /*
    BindViewHolder에서는 데이터랑 뷰홀더랑 바인딩하는 작업을 한다. (데이터 호출 정보/화면에 보여주기 위한 뷰정보 포함)
     */
    override fun onBindViewHolder(holder: ViewHolder, position: Int) {
        // list - Postion 이 원하는 데이터
        // item.xml에 있는 요소들과 list 포지션으로해서 각 요소들 바인딩 완료
        val word = list[position]
        holder.bind(word)
        // holder.itemView 접근 가능(각 요소 들)
        holder.itemView.setOnClickListener {
            itemClickListener?.onClick(word)
        }
        /*
        holder.binding.apply {
            textTextView.text = list[position].text
            meanTextView.text = list[position].mean
            typeChip.text = list[position].type
        }
        */
    }
}

interface ItemClickListener {
    abstract fun onClick(word: Word)

}

 

Adapter를 생성하는데 전체 코드부터 보여주고 하나씩 뜯어서 보겠습니다.

 

class WordAdapter(
    private val list: MutableList<Word>,
    private val itemClickListener: ItemClickListener? = null
): RecyclerView.Adapter<WordAdapter.ViewHolder>()

 

어댑터 클래스를 만드는데, 나중에 MainActivity.kt에서 WordAdapter를 객체로 생성할 때 여하튼 RecyclerView 안에 

어댑터를 꼽으면서 요소들(리스트)을 적용시켜야 하기 때문에 첫 번째 인자는 변형 가능한 MutableList이고

이 리스트를 선택했을때 리스너를 달기 위해서 ItemClickListener를 두 번째 인자로 셋팅합니다.

그리고 이 클래스는 RecyclerView.Adapter니까 상속으로 해두되, ViewHolder는 WordAdapter 클래스 안에서

구현하겠다는 식으로 Adapter<WordAdapter.ViewHolder>()로 표현합니다.

 

 // onBindViewHolder에 binding으로 접근하기 위해서 binding을 뷰홀더에 넣고 뷰홀더 부분에서는 item.xml 바인딩을 두어
    // holder에서도 바인딩을 접근할 수 있게 만듬
    class ViewHolder(private val binding: WordItemBinding) : RecyclerView.ViewHolder(binding.root){
        // onBindViewHolder의 역할은 데이터와 뷰의 바인딩 작업을 하는건데 이걸 ViewHolder 클래스에서 bind라는 함수를 만들어서
        // 구현할 수 있음
        fun bind(word: Word){
            binding.apply {
                textTextView.text = word.text
                meanTextView.text = word.mean
                typeChip.text = word.type
            }
            // itemView는 뷰홀더에서 들어온 아이템 뷰 들을 바로 접근할 수 있는 인자
        }
    }

 

앞서 ViewHolder를 WordAdapter 클래스에서 구현하기로 했으니 RecyclerView.ViewHolder를 상속해 구현하는

ViewHolder 클래스를 만들어줍니다.

본래 여기서는 itemView를 접근하는 곳이지만 다음과 같이 word_item.xml을 binding 형태로 바로 접근하여 

우리가 그동안 해왔던 binding.위젯 형태로 word_item.xml, 즉 item 요소에 확장함수로 접근할 수 있습니다.

 


5) onCreateViewHolder에서 4의 item.xml을 inflate한다.

좀 더 자세히 설명하고자 onCreateViewHolder를 먼저 설명드리겠습니다.

override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ViewHolder {
   val inflater = parent.context.getSystemService(Context.LAYOUT_INFLATER_SERVICE) as LayoutInflater
   val binding = WordItemBinding.inflate(inflater, parent, false)
   return ViewHolder(binding)
}

 

onCreateViewHolder에서는 item으로 쓰이는 xml의 바인딩을 뽑아내고 inflater를 시스템 상에서 객체로 생성한 다음에

인플레이팅 합니다.   또한 여기 선언한 binding은 word_item.xml을 바인딩 형태로 가져와서는 실제 부모 컨텍스트에 

넣을 것으로 하여 inflate를 하되, 두 번째 인자에 parent를 넣어줍니다.

그리고 이걸 ViewHolder를 반환할 때 binding을 같이 넣어줍니다.   이렇게 하면 앞서 class ViewHolder에서 

부모 컨텍스트(MainActivity)에서 자식 (word_item.xml)을 담기 위해서 inflate하는거고 이 ViewHolder에서

각 요소들을 더 쉽게 접근하기 위해서 binding 객체를 보내는 것입니다.

단! ViewHolder에는 현재 word_item.xml 요소들이 들어가기 때문에 RecyclerView.ViewHolder(binding.root)로 

자식 뷰를 넣어주는 것입니다.

 

또한 여기서 bind 함수를 선언해서 binding 객체로 각 요소를 접근하는 이유는 bind 함수의 객체로 Word 모델(데이터 클래스)

을 받았는데, 이 바인딩 하는 작업이 onBindViewHolder에서 이뤄지기 때문에 여기 함수의 list[position]이 결국

Word 객체 하나이므로 바로 ViewHolder의 bind 함수 호출로 셋팅이 가능합니다.


6) onBindViewHolder에서 list의 postion 값으로 하나의 객체를 뽑아온다.

override fun onBindViewHolder(holder: ViewHolder, postion: Int) {
   val word = list[position]  // list는 어댑터의 인자로 이미 선언해뒀기에 가져올 수 있음
   holder.bind(word)
   holder.itemView.setOnClickListener {
      itemClickListener?.onClick(word)
   }
}


interface ItemClickListener {
   abstract fun onClick(word: Word)
}

 

onBindViewHolder에서 다음과 같이 ViewHolder의 bind 함수를 호출하되, list는 어댑터의 인자로 들어간 것이고

추후에 부모 컨텍스트에서 어댑터를 꼽을때 같이 선언할 list와 동일 객체가 될 것입니다.

이 list는 onBindViewHolder 함수에서 가져올 수 있고 postion으로 접근하여

리스트 안의 하나의 Word 객체로 구현 받을 수 있습니다.

 

이걸 bind함수에 넣는다면 ViewHolder 클래스에서 결국 각 리스트의 position 만큼 Word 객체 내 

아이템 요소들을 변경 할 수 있는 조건이 됩니다.

또한 onBindViewHolder에서는 컬랙션(데이터 클래스)와 itemView사이의 바인딩 역할을 하기 때문에

각 리스트의 itemView 클릭 리스너를 달 수 있습니다.

이 리스너에서 바로 word를 가지고 구현할 수 있으나, 인터페이스를 이용해서 ItemClickListener를 

메인 액티비티에서 구현하겠습니다. (실제 adapter 꼽는 구현부 액티비티에서 진행하는 것이 확인차 좋기 때문)


7) holder를 통해 요소에 접근할 수 있고 ViewHolder 클래스에서 bind 함수를 구현하여 요소에 접근할 수도 있습니다.

 class ViewHolder(private val binding: WordItemBinding) : RecyclerView.ViewHolder(binding.root){
        // onBindViewHolder의 역할은 데이터와 뷰의 바인딩 작업을 하는건데 이걸 ViewHolder 클래스에서 bind라는 함수를 만들어서
        // 구현할 수 있음
     fun bind(word: Word){
         binding.apply {
         textTextView.text = word.text
         meanTextView.text = word.mean
         typeChip.text = word.type
       }
          // itemView는 뷰홀더에서 들어온 아이템 뷰 들을 바로 접근할 수 있는 인자
     }
 }

 

이것이 가능한 이유는 onBindViewHolder에서 ViewHolder를 통해 item과 데이터클래스(word)를 바인딩하도록

했기 때문이고, 이는 ViewHolder에서 이미 bind 함수를 구현해뒀기 때문에 가능한 것입니다. 


8) list의 itemClickListener를 구현하여 각 요소들 접근을 가능하도록 한다.

이는 onBindViewHolder에서 itemView에 달 수 있는데, 인터페이스로 셋팅해뒀기 때문에 부모 액티비티에서 직접 구현해주면 됩니다.

    class MainActivity : AppCompatActivity(), ItemClickListener {
    
       wordAdapter = WordAdapter(dummyList, this) // clickListener를 여기서 구현함 (interface)
       override fun onClick(word: Word) {
           Toast.makeText(this, "${word.text} ${word.mean} ${word.type} 클릭완료", Toast.LENGTH_SHORT).show()
       }
    }

 

이런 식으로 어댑터를 생성할 때 첫 번째 인자가 리스트였고 두 번째 인자는 itemClickListener였기에 

이 부분이 인터페이스로 선언되어 있으므로 구현부가 구현될 현 액티비티, 즉 컨텍스트를 넣어주면 됩니다.

그렇게 한다면 구현부 컨텍스트에서는 상속 후 implementation을 진행하면 됩니다.

 


9) RecyclerView에 해당 adapter를 꼽고 LayoutManager를 통해 어떻게 구조를 짜서 보여줄 지 정리한다.

 

마지막으로 구현부 컨텍스트(액티비티)에서 어댑터를 생성할 때 리스트를 구현하는것, 그리고 RecyclerView에

apply 확장함수로 어댑터를 꼽고 LayoutManager를 선언해두는것, divider도 DividerItemDecoration으로

셋팅하는 것  이 모든 부분을 보여드리겠습니다.

 

 

 

[MainActivity.kt]

package com.example.voca_book

import android.content.Intent
import android.os.Bundle
import android.widget.Toast
import androidx.activity.enableEdgeToEdge
import androidx.appcompat.app.AppCompatActivity
import androidx.core.view.ViewCompat
import androidx.core.view.WindowInsetsCompat
import androidx.recyclerview.widget.DividerItemDecoration
import androidx.recyclerview.widget.LinearLayoutManager
import com.example.voca_book.adapters.ItemClickListener
import com.example.voca_book.adapters.WordAdapter
import com.example.voca_book.databinding.ActivityMainBinding
import com.example.voca_book.models.Word

class MainActivity : AppCompatActivity(), ItemClickListener {
    private lateinit var wordAdapter: WordAdapter
    private lateinit var binding: ActivityMainBinding
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        enableEdgeToEdge()
        binding = ActivityMainBinding.inflate(layoutInflater)
        setContentView(binding.root)
        ViewCompat.setOnApplyWindowInsetsListener(findViewById(R.id.main)) { v, insets ->
            val systemBars = insets.getInsets(WindowInsetsCompat.Type.systemBars())
            v.setPadding(systemBars.left, systemBars.top, systemBars.right, systemBars.bottom)
            insets
        }
        initRecyclerView()

        binding.addButton.setOnClickListener {
            startActivity(Intent(this, AddActivity::class.java))
        }
    }



    private fun initRecyclerView(){
        val dummyList = mutableListOf<Word>(
            Word(
                "weather",
                "날씨",
                "명사"
            ),
            Word(
                "honey",
                "꿀",
                "명사"
            ),
            Word(
                "run",
                "실행하다",
                "동사"
            ),
            Word(
                "아름다운",
                "beautiful",
                "형용사"
            )
        )
        wordAdapter = WordAdapter(dummyList, this) // clickListener를 여기서 구현함 (interface)
        // binding의 apply 속성 구성이기에 applicationContext로
        binding.wordRecyclerView.apply {
            adapter = wordAdapter
            layoutManager = LinearLayoutManager(applicationContext,LinearLayoutManager.VERTICAL, false) // reverseLayout = X 화면 변경 X
            val dividerItemDecoration = DividerItemDecoration(applicationContext, LinearLayoutManager.VERTICAL)
            addItemDecoration(dividerItemDecoration)
        } // adapter
    }
    override fun onClick(word: Word) {
        Toast.makeText(this, "${word.text} ${word.mean} ${word.type} 클릭완료", Toast.LENGTH_SHORT).show()
    }

}

 

다음과 같이 mutableListOf로 Word 객체를 넣어서 어댑터의 list로 넣어주고, 인터페이스 구현부 셋팅과

RecyclerView의 속성 정리[ 1) adapter 꼽기, 2) LayoutManager 셋팅 3) DividerItemDecoration 셋팅 ]로 마무리 했습니다.

 

참고사항으로 TextInputLayout 내 TextInputEditText가 들어가야하는데, 이 기능은 글자수 체크가 되는 EditText이고 

min/max 셋팅도 가능하다는 점이 장점입니다.   에러 설정도 가능하고 꽤나 유용한 위젯인 것 같습니다.

 

TextInputLayout & TextInputEditText

https://developer.android.com/reference/com/google/android/material/textfield/TextInputLayout

 

TextInputLayout  |  Android Developers

 

developer.android.com

 

ChipGroup & Chip

https://developer.android.com/reference/com/google/android/material/chip/ChipGroup

 

ChipGroup  |  Android Developers

 

developer.android.com

 

 

 

전체적인 코드에 대해서는 아래 github 링크를 참고하면 좋을 것 같고 다음 단어장앱-2에서는 Room 데이터베이스를 이용해서

정보 관리를 하고 또 심화 부분, UI 구성 등 자세히 확인해보도록 하겠습니다.       두서 없이 적긴 했지만 포스팅하면서 

개념 정리도 더 하려고 노력했고 코드도 다시 한 번 혼자 정리해보는 시간도 가졌습니다.

꾸준히 노력해서 체화하는 시간이 필요할 것 같습니다.   긴 글 읽어주셔서 감사합니다. 

 

https://github.com/Haamseongho/Kotlin_Jetpack/tree/dev_haams_v2/chapter7

 

Kotlin_Jetpack/chapter7 at dev_haams_v2 · Haamseongho/Kotlin_Jetpack

Kotlin 기초부터 Jetpack Compose까지. Contribute to Haamseongho/Kotlin_Jetpack development by creating an account on GitHub.

github.com

 

반응형