애플리케이션 : 4대 컴포넌트
사용 프로젝트
- 새 프로젝트
- 이름 : ListEx
- 패키지 : com.example.listpex
- 새 프로젝트 생성 후 View Binding을 적용한다.
- build.gradle.kts(:app)
android {
...
buildFeatures.viewBinding = true
- 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는 인터넷 상의 이미지를 출력하지 못한다.
- 데이터 클래스 만들기
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
)
- 데이터 생성기 만들기
- 서버에서 데이터를 받아오는 것처럼 가짜 데이터 생성기
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 사용 시 주의점

아이템용 레이아웃 작성
- 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를 상속받도록 구현
- 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로 변경된다.
- 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로 전달해야 하는 상황.
- Adapter에 listener interface를 추가한다.
- Adapter가 listener를 받아 저장해 둔다.
- ViewHolder에서 listener에게 알린다.
- 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 사용 시 딜레마 해결 가능
애플리케이션 : 4대 컴포넌트
사용 프로젝트
- 새 프로젝트
- 이름 : ListEx
- 패키지 : com.example.listpex
- 새 프로젝트 생성 후 View Binding을 적용한다.
- build.gradle.kts(:app)
android {
...
buildFeatures.viewBinding = true
- 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는 인터넷 상의 이미지를 출력하지 못한다.
- 데이터 클래스 만들기
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
)
- 데이터 생성기 만들기
- 서버에서 데이터를 받아오는 것처럼 가짜 데이터 생성기
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 사용 시 주의점

아이템용 레이아웃 작성
- 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를 상속받도록 구현
- 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로 변경된다.
- 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로 전달해야 하는 상황.
- Adapter에 listener interface를 추가한다.
- Adapter가 listener를 받아 저장해 둔다.
- ViewHolder에서 listener에게 알린다.
- 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 사용 시 딜레마 해결 가능