Post

RecyclerView의 Adapter와 ViewHolder

MVVM에서는, View를 가지고 있는 ViewHolder나, ViewHolder에 데이터를 넣어주는 Adapter나 둘 다 (V)로 분류한다.

ViewHolder

데이터가 한 1만건 있다고 치면, 뷰를 1만개나 만드는건 화면에 다 보이지도 않고 비효율적이니까 View는 화면에 보일 정도로 조금만 만든 다음에 화면이 이동하면 View에 들어가는 Data만 바꿔주는 식으로 View를 재활용하자는 컨셉. 즉, ViewHolder와 그가 갖고 있는 View 멤버변수는 만들었다 지웠다 하는게 아니라, 그 안에 들어가는 데이터만 바꿔주는 것임.(val) onBindViewHolder()에서 ViewHolder를 통해서 View에 해당 position의 data를 집어 넣어 주는 식으로 동작한다.

Adapter

Adapter의 역할은

  1. 실제 Data를 들고 있으면서,
  2. 이 Data를 ViewHolder안에 있는 View에 할당(bind)해 준다.
  3. List에 표시될 Item이 어떤 View를 사용할건지도 여기서 결정한다. (예를 들어 only_image_item.xml을 사용할건지, image_with_text_item.xml을 사용할건지 같은 것.)

Adapter, 누구냐 넌? — Data? View?

Adapter는 Data를 들고 있어야 하니 Data의 성격도 가지고 있으면서, 동시에 이 데이터를 View로 연결해줘야 하니 View의 성격도 가지고 있다고 볼 수 있다. ** 인터페이스를 이용해서 AdapterDataModel과 AdapterDataView로 분리하기도 하는데 이렇게까지는 잘 안하고, 그냥 저렇다고 알고만 있으면 될 듯.

1
2
3
4
5
// 뷰홀더는 뷰를 들고 있다.
class RepoViewHolder(view: View): RecyclerView.ViewHolder(view) {
**val** name = view.findViewById<TextView>(R.id.repo\_full\_name)
}

1
2
3
4
5
6
7
8
class RepoSelectAdapter : RecyclerView.Adapter<RepoViewHolder>() {
var items = mutableListOf<RepoItem>()
...
override fun onBindViewHolder(viewHolder: RepoViewHolder, idx: Int) {
viewHolder.name.text = items[idx].full\_name
}
...

data binding을 쓰면 이렇게 된다. (MVVM아님.)

1
2
3
4
5
6
7
8
9
class RepoSelectAdapter : RecyclerView.Adapter<RepoViewHolder>() {
...
override fun onBindViewHolder(repoViewHolder: RepoViewHolder, position: Int) {
repoViewHolder.binding.repo = items.get(position)
}
inner class RepoViewHolder(view: View): RecyclerView.ViewHolder(view) {
val binding: RepoItemBinding = DataBindingUtil.bind(view)!!
}

RecyclerView의 MVVM

두 개를 고려해야 한다.

  1. RecyclerView가 가지고 있는 ItemList에 뭔가가 추가되거나 삭제되어 리스트 자체에 변화가 생긴 경우.
  2. 리스트의 원소인 한 Item 내에서 텍스트가 변경된다던가 하는, Item 내에서 변화가 생긴 경우.

그래서 결국 ViewModel은 2개 필요함.

RepoSelectActivity.kt

RepoSelectViewModel.kt

RepoSelectAdapter.kt

RepoItemViewModel.kt

activity_repo_select.xml

repo_item.xml

onClick이 좀 까다로운데, repo_item.xml에서 () -> vm.onClick() 으로 지정할 수 있으려면 RepoItemViewModel에 onClick()함수가 있어야 한다. 그런데 onClick의 동작이 이런 저런 context를 다뤄야 하는 등 뷰와 관련된 코드다. 그래서 코드 본체는 RepoViewHolder.onItemClick()에 두고, 이 함수를 주고 받을 수 있는 Interface를 하나 정의한 다음 ViewModel을 선언하면서 이 함수를 넘기고 ViewModel의 onClick() 함수 내에서 listener.onItemClick()을 불러주도록 구성했다.

RecyclerView의 무한 스크롤 (load more) 최하단 감지하기

https://medium.com/@ydh0256/android-recyclerview-최상단과 최하단 이벤트 감지하기

RecyclerView.OnScrollListener()를 override해서 지정한다. 참고 ) 이 클래스는 메서드가 2개이므로 data binding으로(android:onScroll) 지정하는 것이 불가능하다. 직접 코드에서 붙여주어야 한다.

근데 안드로이드 jet pack에 포함된 paging library를 쓰면 무한 스크롤을 자동으로 처리해준다…

https://medium.com/@jungil.han/paging-library-%EA%B7%B8%EA%B2%83%EC%9D%B4-%EC%93%B0%EA%B3%A0%EC%8B%B6%EB%8B%A4-bc2ab4d27b87

여러 모로 장점이 많으니 고려해볼 것.

ViewModel의 Observable 변수에 대한 참조를 Adapter를 만들면서 전달해도 괜찮은가?

방법은 총 3가지 정도 되는데…

  1. MainActivity에서 Adapter를 생성하면서 생성자로 ViewModel.items을 넘겨 레퍼런스를 갖게 하는 방법.
  2. @BindingAdapter를 써서 인자로 넘어온 item을, adpater.items = items으로 할당.
  3. @BindingAdapter를 써서 인자로 넘어온 item을, adpater.updateItems(items)으로 addAll. 이 정도는 그냥 선택인 것 같은데, 아무래도 RecyclerAdapter를 만들면서 ViewModel.items를 넘기는 것 보다는 2번 방법이 ViewModel이 변경되어도 좀 더 유연하게 대처 가능할 것 같다.

notifyDataSetChanged() 불러주면 바로 뷰가 변경되는데 뭔가 뷰가 업데이트 되면서 전체적으로 흔들, 하는 느낌으로 뷰가 잠시동안 변경되었다가 원래대로 돌아온다. notifyDataSetChanged()를 호출하지 않아도 데이터가 변경되면 뷰가 갱신은 되는데… 아무래도 데이터가 변경되고 나서 뷰가 바로 변경되지는 않는 느낌이다. 이 외에도 뷰가 갱신되는 시점이 좀 지멋대로라 꼭 불러줘야할듯.

This post is licensed under CC BY 4.0 by the author.