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 {
// Inflating
val view = LayoutInflater.from(parent?.context).inflate(R.layout.item,parent,false)
val getData = dataList[position]
// findViewById
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에 대한 내용과 일치하는지 확인하기
Adapter가 알고 있어야 하는 것
(1) Data가 무엇인지 알고 있어야 한다.
(2) 어떤 형태로 item을 구성할지 layout을 알고 있어야 한다.
(3) Data와 layout을 매핑할 수 있어야 한다.
(4) item도 화면에 표현하는 내용이기 때문에 Context를 알고 있어야 한다.
(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)
// Data
val noteBookList = listOf<NoteBook>(
NoteBook("MacBook Pro","Sliver","OS X"),
NoteBook("MacBook Pro","Gray","OS X"),
NoteBook("Galaxy Book","Sliver","Windows")
)
//CustomAdapter를 View에 추가
var arrayAdapter = CustomBaseAdapter(this,noteBookList)
listView.adapter =arrayAdapter
}
2) 문제점
getView메서드 내에
Log.d를 찍어보면 알겠지만
화면에 Item이 보여야 할 때마다
계속 반복적으로 호출된다.
앞에서 반복적으로 이야기했듯
문제는 여기서 생긴다.
getView 안을 살펴보면 아래와 같은 구문들이
아무 조건 없이 반복적으로 실행되기 때문이다.
...
override fun getView(position: Int, convertView: View?, parent: ViewGroup?): View {
/* 리소스 사용!! */
// Inflating
val view = LayoutInflater.from(parent?.context).inflate(R.layout.item,parent,false)
...
/* 리소스 사용!! */
// findViewById
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() {
// item을 구성하고 있는 View에 맞게 변수 생성
var textName:TextView? = null
var textOs:TextView? = null
var textColor:TextView? = null
// setter 생성
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 view: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를 통해 조작할 수 있어
더 편하게 구현이 가능하다.
'Mobile 개발 > 재미있는 Android' 카테고리의 다른 글
[Android] WebView 설정 모아보기 (1) | 2020.11.20 |
---|---|
[Android] Retrofit을 쓰자 - 기본적인 사용 방법 (0) | 2020.11.19 |
[Android] RecyclerView 이해하기 (1) - Adapter는 무엇인가 (0) | 2020.09.09 |
[Android] 이벤트 쉽게 풀어서 이해하기 (0) | 2020.02.13 |
[팁] FileUriExposedException : 안드로이드 카메라 인텐트 에러 (0) | 2017.11.22 |