1. ViewHolder의 필요성
앞선 글에서 설명했듯 많은 데이터를
항목화하여 표현하는 AdapterView 에서는
동작할 때마다 각 항목
(xml형식의 레이아웃)을
View객체로 가져와야 한다
그리고 이렇게 생성한 View에
포함된 뷰를 찾아
( Java 에서는 findViewById)
접근하는 일도 아주 빈번하게 일어난다.
문제는 이 작업은
많은 리소스를 필요로 한다는 점이다.
*참고
이를 해결하기 위해 사용되는 것이 ViewHolder이다.
한번 inflate 된 View를 객체에 담아 두어
재사용하는 방식이다.
즉, View를 생성하고 활용하는 과정에서
리소스를 효율적으로 사용하기 위한
패턴이라고 볼 수 있다.
2. ListView에 ViewHolder 적용하기
이러한 ViewHolder 패턴은
꼭 RecyclerView에서만 사용되는 건 아니다. 오히려 ListView에
ViewHolder를 적용해보면 어떤 이점이 있는지 이해하기 편할 것 같다.
1) 기본 구성 (ViewHolder 없이 구현)
(1) activity_main.xml
먼저 기본적인 Main Activity의 화면 구성은 다음과 같이 단순하게 해 놓았다
<?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 =".MainActivity" >
<ListView
android:id ="@+id/listView"
android:layout_width ="0dp"
android:layout_height ="0dp"
android:layout_marginStart ="1dp"
android:layout_marginLeft ="1dp"
android:layout_marginTop ="1dp"
android:layout_marginEnd ="1dp"
android:layout_marginRight ="1dp"
android:layout_marginBottom ="1dp"
app:layout_constraintBottom_toBottomOf ="parent"
app:layout_constraintEnd_toEndOf ="parent"
app:layout_constraintStart_toStartOf ="parent"
app:layout_constraintTop_toTopOf ="parent" />
</androidx.constraintlayout.widget.ConstraintLayout >
(2) item .xml
ListView에 요소로 들어갈
item 레이아웃을 생성한다.
<?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 ="50dp"
android:gravity ="center_vertical"
android:orientation ="horizontal" >
<TextView
android:id ="@+id/textName"
android:layout_width ="0dp"
android:layout_height ="wrap_content"
android:layout_weight ="1"
android:text ="MacBook Pro" />
<TextView
android:id ="@+id/textColor"
android:layout_width ="0dp"
android:layout_height ="wrap_content"
android:layout_weight ="1"
android:text ="sliver" />
<TextView
android:id ="@+id/textOs"
android:layout_width ="0dp"
android:layout_height ="wrap_content"
android:layout_weight ="1"
android:text ="OS X" />
</LinearLayout >
(3) NoteBook.kt
Data class를 선언한다.
하나의 항목에 들어갈 데이터를
저장하는 객체라고 보면 된다.
data class NoteBook (val name :String , val color :String , val os :String)
(4) CustomBaseAdapter.kt
일반적으로 구현하는 어댑터이다.
getView를 자세히 보면
Item.xml을 inflate 하고 inflate 한 View에서
textView를 찾아서 데이터를 바인딩해준다.
그리고 마지막으로 inflate한 View전체에
click이벤트를 지정한다.
class CustomBaseAdapter (private val context :Context , private val dataList : List <NoteBook >) : BaseAdapter () {
override fun getView(position: Int , convertView: View? , parent: ViewGroup? ): View {
val view = LayoutInflater .from(parent?.context).inflate(R .layout.item,parent,false )
val getData = dataList[position]
view.textName.text = getData.name
view.textOs.text = getData.os
view.textColor.text = getData.color
view.setOnClickListener(View .OnClickListener {
Toast .makeText(context,getData.toString (),Toast .LENGTH_SHORT ).show()
})
return view
}
override fun getItem(position: Int ): Any {
return dataList[position]
}
override fun getItemId(position: Int ): Long {
return 0
}
override fun getCount(): Int {
return dataList.size
}
}
** 이전 글에서 Adapter에 대한 내용과 일치하는지 확인하기
(5) MainActivity.kt
Main Activity에서는 지금까지 작성한 내용을 조합한다고 보면 된다.
Data를 list로 선언해서 Adapter에 초기화 값으로 선언한 후
Adapter 를 ListView에 연결하면 정상적으로 동작한다.
class MainActivity : AppCompatActivity () {
override fun onCreate(savedInstanceState: Bundle? ) {
super .onCreate(savedInstanceState)
setContentView(R .layout.activity_main)
val noteBookList = listOf<NoteBook >(
NoteBook ("MacBook Pro" ,"Sliver" ,"OS X" ),
NoteBook ("MacBook Pro" ,"Gray" ,"OS X" ),
NoteBook ("Galaxy Book" ,"Sliver" ,"Windows" )
)
var arrayAdapter = CustomBaseAdapter (this,noteBookList)
listView.adapter =arrayAdapter
}
2) 문제점
getView메서드 내에
Log.d를 찍어보면 알겠지만
화면에 Item이 보여야 할 때마다
계속 반복적으로 호출된다.
앞에서 반복적으로 이야기했듯
문제는 여기서 생긴다.
getView 안을 살펴보면 아래와 같은 구문들이
아무 조건 없이 반복적으로 실행되기 때문이다.
...
override fun getView(position: Int, convertView: View?, parent : ViewGroup?): View {
val view = LayoutInflater.from(parent ?.context).inflate(R.layout.item,parent ,false )
...
view.textName.text = getData.name
view.textOs.text = getData.os
view.textColor.text = getData.color
...
return view
}
3) 해결방법 : ViewHolder
해결방법은 간단하다.
View를 한번 만들었으면
어딘가에 담아 두었다가
필요할 때 재사용하는 것이다.
(1) VIewHodler class 생성
그 '어딘가'가 ViewHolder 클래스이다.
Item.xml에서 사용한 구성요소에 맞추어
변수를 선언하고 setter를 생성해두면 된다.
class ViewHolder () {
var textName:TextView? = null
var textOs:TextView? = null
var textColor:TextView? = null
fun setNoteBook (noteBook: NoteBook) {
textName?.text = noteBook.name
textOs?.text = noteBook.os
textColor?.text = noteBook.color
}
}
(2) ViewHolder 사용하기
이렇게 생성된 ViewHolder는
getView 메서드 내에서 사용된다.
이전에 View로 inflate 한 xml을
다시 inflate 하지 않기 위해 getView의 두 번째 매개변수를 활용한다.
두 번째 매개변수 convertView는
이전에 inflate 한 View를 그대로 전달한다.
즉, convertView가 null이라면 최초 생성이라는
말이기 때문에 Inflate를 수행한다.
위에서 만들어 놓은 ViewHolder class를
객체로 생성하고
이 객체에 Inflate 된 View를 매핑한다.
ViewHolder객체를 잘 생성했다면
Inflate 된 View에 tag의 값으로
저장하고 반환한다.
override fun getView(position: Int, convertView: View?, parent: ViewGroup?): View {
// return
val vie w:View
val getData = dataList[position]
val vh:ViewHolder
if (convertView == null){
// Inflating
view = LayoutInflater.from(parent?.context).inflate(R.layout.item,parent,false)
// 매핑해놓음
vh = ViewHolder()
vh.textName = view .textName
vh.textOs = view .textOs
vh.textColor =view .textColor
// view 에 viewholder 저장
view .tag = vh
// 이벤트 생성
view ?.setOnClickListener{
Toast.makeText(context,getData.toString(),Toast.LENGTH_SHORT).show()
}
}else {
view = convertView
vh = convertView.tag as ViewHolder
}
//값 매칭
vh.setNoteBook(getData)
return view
}
이렇게 반환된 View는 화면에서 보이고
이후에 다시 getView가 실행될 때 convertView로 재사용된다.
convertView로 재사용되었다면
convertView 안에 tag를 꺼내어
ViewHolder로 활용한다.
ViewHolder에 Inflate 된 View가 다 매핑이 되었다면
ViewHolder의 setter를 통해
데이터를 세팅해주면 손쉽게
값이 바인딩된다.
4. RecyclerView와 다른 점
RecyclerView는 ListView와 달리
앞에 설명한 내용을
반드시 구현하도록 되어있다.
덕분에 자연스럽게 각 요소가 재활용될 수 있으며
추가적으로 배치 방법과 방향등을
LayoutManager를 통해 조작할 수 있어
더 편하게 구현이 가능하다.