이번 포스팅에서는 단어장 앱 개발에 대해 설명드리고자 합니다.
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
ChipGroup & Chip
https://developer.android.com/reference/com/google/android/material/chip/ChipGroup
전체적인 코드에 대해서는 아래 github 링크를 참고하면 좋을 것 같고 다음 단어장앱-2에서는 Room 데이터베이스를 이용해서
정보 관리를 하고 또 심화 부분, UI 구성 등 자세히 확인해보도록 하겠습니다. 두서 없이 적긴 했지만 포스팅하면서
개념 정리도 더 하려고 노력했고 코드도 다시 한 번 혼자 정리해보는 시간도 가졌습니다.
꾸준히 노력해서 체화하는 시간이 필요할 것 같습니다. 긴 글 읽어주셔서 감사합니다.
https://github.com/Haamseongho/Kotlin_Jetpack/tree/dev_haams_v2/chapter7
'Android' 카테고리의 다른 글
Permission 처리와 ListAdapter 활용법 (0) | 2024.08.01 |
---|---|
RoomDB 활용 방법과 Coroutine 사용 (0) | 2024.07.29 |
타이머(AlertDialog/CustomView/Thread/Progress) 앱 (7) | 2024.07.24 |
+/- 만 구현된 간단한 계산기앱(Decimal Format, Flow, StringBuilder) (0) | 2024.07.23 |
Layer / DatePickerDialog / Spinner / SharedPreference (6) | 2024.07.22 |