//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:layout_width="match_parent"
android:layout_height="match_parent"
android:gravity="center"
android:orientation="vertical"
tools:context=".recyclerview.MainActivity">
<ListView
android:id="@+id/list_view_main"
android:layout_width="0dp"
android:layout_height="0dp"
android:padding="10dp"
app:layout_constraintBottom_toTopOf="@id/button_main"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent"
tools:listitem="@layout/item_book" />
</androidx.constraintlayout.widget.ConstraintLayout>
//item_book.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">
<androidx.appcompat.widget.AppCompatImageView
android:id="@+id/image_view_item"
android:layout_width="50dp"
android:layout_height="50dp"
android:background="@color/teal_200"
app:layout_constraintEnd_toStartOf="@id/image_view_item"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent"
tools:src="@drawable/ic_launcher_foreground" />
<androidx.appcompat.widget.AppCompatTextView
android:id="@+id/text_view_item"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_marginStart="10dp"
app:layout_constraintBottom_toBottomOf="@id/image_view_item"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toEndOf="@id/image_view_item"
app:layout_constraintTop_toTopOf="@id/image_view_item"
tools:text="Test" />
</androidx.constraintlayout.widget.ConstraintLayout>
data class Book(
val name: String,
val image: Drawable,
)
class MainActivity : AppCompatActivity() {
private lateinit var binding: ActivityMainBinding
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
binding = ActivityMainBinding.inflate(layoutInflater)
setContentView(binding.root)
val adapter = ListView(getDummyBookList(), this)
binding.listViewMain.adapter = adapter
}
private fun getDummyBookList(): List<Book> {
val list = mutableListOf<Book>()
for (i in 0..100) {
val book = Book(
"android${i + 1}",
image = ContextCompat.getDrawable(this, R.drawable.ic_launcher_foreground)!!
)
list.add(book)
}
return list
}
}
class ListView(
private val bookList: List<Book>,
private val context: Context
) : BaseAdapter() {
override fun getCount(): Int = bookList.size
override fun getItem(position: Int): Any = bookList[position]
override fun getItemId(position: Int): Long = position.toLong()
override fun getView(position: Int, view: View?, parent: ViewGroup?): View {
println("getView ${position + 1} 번 째 중")
***val inflater = LayoutInflater.from(context)
val view = inflater.inflate(R.layout.item_book, parent, false)
val image = view.findViewById<ImageView>(R.id.image_view_item)
val name = view.findViewById<TextView>(R.id.text_view_item)
image.setImageDrawable(bookList[position].image)
name.text = bookList[position].name***
return view
}
}
getView에서 리스트 사이즈만큼 뷰들을 일일이 다 inflate 하고 findViewId를 통해 뷰를 찾는다.
모든 리스트 요소에 대해 레이아웃을 inflate 하고 findViewBy를 통해 뷰를 찾는 것은 너무 리소스 낭비임.
class ListView(
private val bookList: List<Book>,
private val context: Context
) : BaseAdapter() {
override fun getCount(): Int = bookList.size
override fun getItem(position: Int): Any = bookList[position]
override fun getItemId(position: Int): Long = position.toLong()
override fun getView(position: Int, **view: View?**, parent: ViewGroup?): View {
***var convertView : View? =view
if(convertView==null) {
println("getView ${position +1} 번 째 중")
val inflater = LayoutInflater.from(context)
convertView = inflater.inflate(R.layout.item_book, parent, false)
}
val image = convertView!!.findViewById<ImageView>(R.id.image_view_item)
val name = convertView!!.findViewById<TextView>(R.id.text_view_item)
image.setImageDrawable(bookList[position].image)
name.text = bookList[position].name***
***return convertView***
}
}
getView의 매개변수 view로 이미 inflate된 레이아웃 뷰가 들어온다. View: The old view to reuse, if possible. Note: You should check that this view is non-null and of an appropriate type before using. If it is not possible to convert this view to display the correct data, this method can create a new view. Heterogeneous lists can specify their number of view types, so that this View is always of the right type (see getViewTypeCount() and getItemViewType(int)). convertView==null 일때 즉, 처음에만 화면에 보여줄 뷰 (+ extra 뷰)를 inflate 하고 나머지 즉, conviertView≠null일때는 이미 inflate된 뷰를 재활용 하면 , 리스트 갯수만큼 모든 뷰를 inflate 해주지 않아도 된다.
화면에 보여지는 리스트는 10개쯤이다. 그래서 처음에만 12개정도 뷰가 inflate되고 , 나머지는 이 12개의 convertView를 재활용한다.
getView의 파라미터인 convertView를 통해 모든 리스트 요소에 대하여 레이아웃을 inflate 해주는 현상은 제거할 수 있었습니다. 하지만 여전히 모든 리스트 요소에 대하여 findViewById로 뷰를 찾고 있습니다. findViewById는 자신부터 자식까지 모든 뷰 계층을 순회하면서 뷰를 찾는데, 이것이 메인쓰레드에서 이루어져서 사용자 경험을 저해할 수 있습니다. 따라서 viewHolder를 사용하여 매번 findViewById를 호출하는 것을 방지해 보겠습니다.
viewHolder : 뷰 객체를 보관해두는 holder 객체
class ListView(
private val bookList: List<Book>,
private val context: Context
) : BaseAdapter() {
override fun getCount(): Int = bookList.size
override fun getItem(position: Int): Any = bookList[position]
override fun getItemId(position: Int): Long = position.toLong()
override fun getView(position: Int, view: View?, parent: ViewGroup?): View {
***var convertView: View? = view
val holder: BookViewHolder
if (convertView == null) {
println("getView ${position + 1} 번 째 중")
val inflater = LayoutInflater.from(context)
convertView = inflater.inflate(R.layout.item_book, parent, false)
holder = BookViewHolder()
holder.imageView = convertView!!.findViewById<ImageView>(R.id.image_view_item)
holder.textView = convertView!!.findViewById<TextView>(R.id.text_view_item)
convertView.tag = holder
} else {
holder = convertView.tag as BookViewHolder
}
holder.imageView?.setImageDrawable(bookList[position].image)
holder.textView?.text = bookList[position].name
return convertView***
}
}
class BookViewHolder {
var imageView: ImageView? = null
var textView: TextView? = null
}
뷰(imageView, textView) 객체를 보관해둘 BookViewHolder 클래스를 만듭니다. convertView == null 일때, 즉 리스트뷰가 화면에 처음 그려질때만 BookViewHolder객체를 만들고 여기에 imageVIew와 textView를 findViewbyId로 찾아서 저장해둡니다. convertView에 holder 태그를 달아줘서, 이미 converView가 있을 때는 이 태그를 BookViewHolder로 변환한 후 holder 변수에 할당해줍니다.
이렇게 뷰홀더를 사용하면, 매번 findViewId로 뷰들을 찾아주지 않고 이미 저장된 뷰를 가져와서 거기에 데이터만 할당해 줄 수 있습니다.
listView에서 convertVIew와 viewHolder를 이용해 성능을 개선시킬 수 있지만, 필수 구현 사항이 아니기 때문에 viewHolder를 강제화하고 이미 inflate된 뷰를 재활용하는 recyclerView가 나왔습니다.