ListView

//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를 통해 뷰를 찾는 것은 너무 리소스 낭비임.

스크린샷 2023-01-12 오후 1.15.13.png

스크린샷 2023-01-12 오후 1.15.28.png

convertView를 통해 성능 개선 - 이미 inflate 된 뷰 재사용 하기

스크린샷 2023-01-12 오후 1.38.24.png

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 해주지 않아도 된다.

스크린샷 2023-01-12 오후 1.52.22.png

스크린샷 2023-01-12 오후 1.52.12.png

스크린샷 2023-01-12 오후 1.52.15.png

화면에 보여지는 리스트는 10개쯤이다. 그래서 처음에만 12개정도 뷰가 inflate되고 , 나머지는 이 12개의 convertView를 재활용한다.

viewHolder를 통해 성능 개선 - findViewById 사용하지 않기

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로 뷰들을 찾아주지 않고 이미 저장된 뷰를 가져와서 거기에 데이터만 할당해 줄 수 있습니다.

viewHolder 패턴을 강제화한 RecyclerView

listView에서 convertVIew와 viewHolder를 이용해 성능을 개선시킬 수 있지만, 필수 구현 사항이 아니기 때문에 viewHolder를 강제화하고 이미 inflate된 뷰를 재활용하는 recyclerView가 나왔습니다.