[개념] 리스트뷰와 리사이클러뷰에서 ViewHolder 패턴 활용

쓰리원·2022년 4월 5일
0
post-thumbnail

1. ListView 에서의 ViewHolder

이전 글에서 ListView를 직관적으로 분해하여 다뤄보았습니다. 이번에는 조금 코틀린스럽게 다뤄서 글을 작성해 보겠습니다.

https://velog.io/@errored_pasta/Android-ViewHolder

ListView의 내용은 위 글을 참고해서 적겠습니다.

1. ViewHolder 없이 구현

(위 경우는 이전 글을 참고하면 findViewById의 중복 호출이 발생하는 것을 알 수 있습니다.)

class ListViewAdapter(
    private val items: List<ListViewModel>
) : BaseAdapter() {
    override fun getCount(): Int = items.size
    override fun getItem(position: Int): Any = items[position]
    override fun getItemId(position: Int): Long = items[position].id

    override fun getView(position: Int, convertView: View?, parent: ViewGroup?): View {
        // No ViewHolder
        val view =
            convertView ?: LayoutInflater.from(parent?.context)
                .inflate(R.layout.list_view_item, parent, false)

        bind(view, position)

        return view
    }

    /**
     * View에 알맞은 data를 binding하는 함수
     * @param view data binding을 할 View
     * @param position ListView에서 표시될 View의 위치
     */
    private fun bind(view: View, position: Int) {
        view.findViewById<TextView>(R.id.listViewItemTextView).text = 
        	items[position].content
        
        // Glide를 이용하여 이미지 url에서 이미지를 불러온다.
        view.findViewById<ImageView>(R.id.listViewItemImageView)
        	.load(items[position].imageUrl)
    }
}

1. val view = convertView ?: -> convertView가 null이면 LayoutInflater.from(parent?.context)
.inflate(R.layout.list_view_item, parent, false) 를 하겠다 라는 뜻 입니다.

2. bind(view, position) -> view, position 을 파라미터로 받아서 findViewById를 따로 관리해주는 것 입니다.

2. ViewHolder를 만들어서 구현

class ListViewAdapter(
    private val items: List<ListViewModel>
) : BaseAdapter() {
    override fun getCount(): Int = items.size
    override fun getItem(position: Int): Any = items[position]
    override fun getItemId(position: Int): Long = items[position].id

	override fun getView(position: Int, convertView: View?, parent: ViewGroup?): View {
    // Using ViewHolder
    	val viewHolder = if (convertView == null) {
        val tempViewHolder = ListViewHolder(
            // 재사용할 View가 없으므로 inflate
            LayoutInflater.from(parent?.context)
                .inflate(R.layout.list_view_item, parent, false)
        )

        tempViewHolder.root.tag = tempViewHolder
        tempViewHolder
     
    	} else {
        // 이전에 저장한 ViewHolder를 convertView의 tag에서 가져온다.
        	convertView.tag
    	} as ListViewHolder

    	viewHolder.bind(items[position])

    	return viewHolder.root
	}
    class ListViewHolder(val root: View) {
    
    	val textView: TextView = root.findViewById(R.id.listViewItemTextView)
    	val imageView: ImageView = root.findViewById(R.id.listViewItemImageView)

    	/**
     	* ViewHolder의 View에 알맞은 data를 binding
     	* @param data binding할 data
     	*/
    	fun bind(data: ListViewModel) {
        	textView.text = data.content
        	imageView.load(data.imageUrl)
    	}
	}
}

ListView에서 ViewHolder는 직접 클래스를 만들어서 구현해야 됩니다.

class ListViewHolder(
    val root: View
) {
    val textView: TextView = root.findViewById(R.id.listViewItemTextView)
    val imageView: ImageView = root.findViewById(R.id.listViewItemImageView)

    /**
     * ViewHolder의 View에 알맞은 data를 binding
     * @param data binding할 data
     */
    fun bind(data: ListViewModel) {
        textView.text = data.content
        imageView.load(data.imageUrl)
    }
}

필요한 View들의 reference를 저장할 수 있도록 클래스를 구현하고 이를 adapter에서 사용하면 됩니다. ViewHolder를 사용하면 getView의 코드를 아래와 같이 작성할 수 있습니다.

override fun getView(position: Int, convertView: View?, parent: ViewGroup?): View {
    // Using ViewHolder
    val viewHolder = if (convertView == null) {
        val tempViewHolder = ListViewHolder(
            // 재사용할 View가 없으므로 inflate
            LayoutInflater.from(parent?.context)
                .inflate(R.layout.list_view_item, parent, false)
        )

        tempViewHolder.root.tag = tempViewHolder
        tempViewHolder
    } else {
        // 이전에 저장한 ViewHolder를 convertView의 tag에서 가져온다.
        convertView.tag
    } as ListViewHolder

    viewHolder.bind(items[position])

    return viewHolder.root
}

2. RecyclerView 에서의 ViewHolder

공식문서를 보지않고 남들이 짜놓은 코드만 보고 클론코딩을 한다면 기능은 구현 할 수 있지만 어떻게 리사이클러뷰가 구현되고 변형되어 있는지 파악 할 수가 없습니다.

그래서 공식문서의 리사이클러뷰부터 설명을 해보겠습니다.

1. 구글 공식문서 RecyclerAdapter 코드 설명


class CustomAdapter(private val dataSet: Array<String>) :
        RecyclerView.Adapter<CustomAdapter.ViewHolder>() {

    /**
     * Provide a reference to the type of views that you are using
     * (custom ViewHolder).
     */
    class ViewHolder(view: View) : RecyclerView.ViewHolder(view) {
        val textView: TextView

        init {
            // Define click listener for the ViewHolder's View.
            textView = view.findViewById(R.id.textView)
        }
    }

    // Create new views (invoked by the layout manager)
    override fun onCreateViewHolder(viewGroup: ViewGroup, viewType: Int): ViewHolder {
        // Create a new view, which defines the UI of the list item
        val view = LayoutInflater.from(viewGroup.context)
                .inflate(R.layout.text_row_item, viewGroup, false)

        return ViewHolder(view)
    }

    // Replace the contents of a view (invoked by the layout manager)
    override fun onBindViewHolder(viewHolder: ViewHolder, position: Int) {

        // Get element from your dataset at this position and replace the
        // contents of the view with that element
        viewHolder.textView.text = dataSet[position]
    }

    // Return the size of your dataset (invoked by the layout manager)
    override fun getItemCount() = dataSet.size

}

RecyclerView.Adapter를 상속 받을때 왜 Adpater를 위와 같이 만드는지 차근히 확인해 보겠습니다.

RecyclerView.Adapter 를 상속하여 어댑터를 만들 때, RecyclerAdapter에서 구현해야 할 함수들은 onCreateViewHolder, onBindViewHolder, getItemCount, ViewHolder 입니다.

class CustomAdapter(private val dataSet: Array<String>) :
        RecyclerView.Adapter<CustomAdapter.ViewHolder>() {
        ...
}

Adapter의 클래스는 RecyclerView.Adapter<CustomAdapter.ViewHolder>() 를 상속 받습니다. 그리고 제네릭 타입으로 어댑터의 뷰홀더를 넘기는 것을 볼 수 있습니다. 그 이유는 아래와 같습니다.

RecyclerView.java 파일을 열어보시면 위와 같은 Adapter 추상 클래스를 찾을 수 있습니다. 여기서 <> 안에 ViewHolder를 상속 받는 클래스 타입을 넣어야 한다고 되어 있습니다.

    class ViewHolder(view: View) : RecyclerView.ViewHolder(view) {
        val textView: TextView

        init {
            // Define click listener for the ViewHolder's View.
            textView = view.findViewById(R.id.textView)
        }
    }

그렇기 때문에 ViewHolder는 RecyclerView.ViewHolder(view) 를 상속 받아야 합니다.

위 사진과 같은 ViewHolder 클래스를 상속을 받고 생성자에의 파라미터로 itemView를 받아옵니다. 그리고 그 itemView는 ListView의 convertView 역할을 하게 됩니다. convertView의 역할을 하는 이유는 아래의 코드에서 확인할 수 있습니다.

    // Create new views (invoked by the layout manager)
    override fun onCreateViewHolder(viewGroup: ViewGroup, viewType: Int): ViewHolder {
        // Create a new view, which defines the UI of the list item
        val view = LayoutInflater.from(viewGroup.context)
                .inflate(R.layout.text_row_item, viewGroup, false)

        return ViewHolder(view)
    }

위 코드에서 우리는 ViewHolder의 생성을 강제하고 있습니다. 그리고 그 ViewHolder에는 inflate되는 view를 ViewHoler에 생성자에 넘겨주는 것을 확인할 수 있습니다.

    class ViewHolder(view: View) : RecyclerView.ViewHolder(view) {
        val textView: TextView

        init {
            // Define click listener for the ViewHolder's View.
            textView = view.findViewById(R.id.textView)
        }
    }

ListView의 ViewHolder의 역할과 똑같게 findViewById(R.id.textView)해서 얻어온 View의 id를 ViewHolder에 저장해 줍니다. 이로인해 우리는 고비용이 될 수 있는 findViewById()를 한번만 호출하면 이 후에는 저장된 값을 재사용하기 때문에 호출을 안해도 됩니다.

    override fun onBindViewHolder(viewHolder: ViewHolder, position: Int) {

        // Get element from your dataset at this position and replace the
        // contents of the view with that element
        viewHolder.textView.text = dataSet[position]
    }

ViewHolder에는 findViewById()가 호출되어서 View의 id가 이미 저장되어 있는 상황입니다. 이제는 ViewHoler를 통해서 재사용을 해주고, dataSet[position]으로 data값을 리스트의 순서에 맞게 넣어주면 됩니다.

2. 간단 정리

getItemCount: 보여줄 리스트 몇 개인지 알려줍니다.

ViewHolder: 각 요소를 findViewById 를 통해 저장합니다. 그리고, 다음부터는 ViewHolder로 불러와서 재사용하게 됩니다. 이렇게하면 xml 리소스에 findeViewById로 직접 접근하지 않아도 됩니다.

onCreateViewHolder : ViewHolder를 새로 만들 때 호출되는 메서드로, 이를 통해 각 아이템을 위한 XML 레이아웃을 활용한 뷰 객체를 생성하고 이를 뷰 홀더 객체에 담아 리턴 합니다. ViewHolder 가 아직 어떠한 데이터에 바인딩된 상태가 아니기 때문에 각 뷰의 내용 (TextView 의 Text 등)은 없습니다.

onBindViewHolder: 생성된 View에 보여줄 데이터를 설정 때 호출되는 메서드입니다. position 이라는 파라미터를 활용하여 데이터의 위치(index값)에 맞게 바인딩 해줄수 있습니다.

3. Outro

이로써 리스트뷰와 리사이클러뷰의 ViewHolder의 쓰임새에 대해서 자세히 알아봤습니다.

다음에는 리사이클러뷰에서 ViewHolder를 어떻게 커스텀해서 쓰는지 직접 리사이클러뷰를 구현해보겠습니다. 그리고 여러가지 방법으로 코드를 작성하는 방법에 대해서 알아보겠습니다.

리사이클러뷰로 만드는 예제 입니다.

https://github.com/ilil1/RecyclerviewExample.git

4. reference

https://developer.android.com/guide/topics/ui/layout/recyclerview
https://developer.android.com/guide/topics/ui/layout/recyclerview-custom

profile
가장 아름다운 정답은 서로의 협업안에 있다.

0개의 댓글