RecyclerView 에서 notifyItemChanged()의 payload 이해하기


notifyItemChanged()의 payload


RecyclerView를 사용하다 보면 특정 position 항목만 갱신해야 될 때가 있습니다.
예를들어 버튼을 클릭했을 때, RecyclerView의 마지막 항목만 애니메이션하거나 마지막 항목의 텍스트나 이미지를 업데이트할 수 있습니다.

안드로이드 RecyclerView는 Adapter의 onCreateViewHolder(), onBindViewHolder()를 override해서 구현합니다.

보통 특정 position 항목만 갱신할 때는 notifyItemChanged(position)을 사용하고 여러개가 변경된 경우 notifyItemRangeChanged(positionStart, itemCount)을 사용합니다.

하지만 onBindViewHolder()는 대부분 뷰를 초기화 작업들이 있기 때문에 모든 View를 업데이트하지 않고 특정 View만 Animation 한다던가 TextView의 text만 변경하는 작업들을 하고 싶을 때는 뭔가 조건문으로 처리해야 할 필요성이 생깁니다.

그래서 Recycerview에 payload라는 기능이 추가되었습니다.

payload란 무엇일까?

notifyItemChanged(position, payload)에서 payload는 아답터의 onBindViewHolder()가 호출될 때 넘겨받는 객체입니다.
특정 position의 holder를 업데이트할 때 payload 값으로 구분하여 애니메이션 하거나 뷰를 업데이트할 수도 있습니다.

사용하는 방법?

notifyItemChanged(int position, Object payload)
업데이트하고 싶은 position 항목에게 payload를 전달합니다.

notifyItemRangeChanged(int positionStart, int itemCount, Object payload)
업데이트하고 싶은 범위를 지정하여 모든 항목에 payload를 전달합니다.

onBindViewHolder(RecyclerView.ViewHolder holder, int position, List<Object> payloads)
아답터에서 업데이트되는 position 항목만 호출하여 payload를 받아 뷰를 업데이트 할 수 있습니다.

onBindViewHolder(RecyclerView.ViewHolder holder, int position)
payload 없이 notifyItemChanged(position)을 호출하거나 payload에 null을 전달한 경우 view를 초기화 해주는 로직들이 들어갑니다.

예를 들어 한가지 셈플을 작성해보겠습니다.
첫 번째 버튼을 클릭하면 Recyclerview의 마지막 아이템 항목의 ImageView의 scale이 커지면서 페이드인(fade in)하는 애니메이션을 실행합니다.
그리고 두번째 버튼을 클릭하면 Recyclerview의 모든 아이템 항목에 애니메이션을 실행합니다.

마지막 항목과 전체 항목을 paylod를 사용한 업데이트
마지막 항목과 전체 항목을 paylod를 사용한 업데이트

우선 com.android.support:recyclerview-v7:24.1.0 이상 버전을 build.gradle의 dependencies에 추가합니다.

다음은 payload를 전달하여 애니메이션 하는 코드입니다.



Recyclerview를 생성하고 LinearLayoutManager과 Adapter를 설정해줍니다.
데이터는 간단하게 ArrayList<String>을 전달해서 텍스트로 된 리스트를 보여줍니다.

last_item_favorite_button 버튼을 클릭하면 마지막 아이템만 "click"을 전달하여 아이콘이 커졌다가 작아지는 애니메이션을 실행시킵니다.
all_item_favorite_button 버튼을 클릭하면 전체 아이템에 "click"을 전달하여 애니메이션을 실행시킵니다.
  • adapter.notifyItemChanged(adapter.getItemCount() - 1, "click")
  • adapter.notifyItemRangeChanged(0, adapter.getItemCount(), "click")
notifyItemChanged(adapter.getItemCount() - 1, "click")으로 마지막 항목만을 업데이트할 수 있으며, notifyItemRangeChanged(0, adapter.getItemCount(), "click")으로 범위를 지정하여 업데이트할 수 있습니다.

다음은 Adapter에 onBindViewHolder(RecyclerView.ViewHolder holder, int position)를 구현하고 여기에서는 TextView에 텍스트를 설정하는 것 처럼 뷰를 초기화하는 로직을 구현합니다.

그리고 onBindViewHolder(... List<Object> payloads)에서는 payload를 전달받아 뷰를 업데이트하거나 애니메이션을 실행할 수 있습니다.


payload가 비어있지 않은 경우에는 뷰를 업데이트합니다.
payload는 List이기 때문에 for 루프를 사용하거나 contain()을 사용하여 payload에 "click"이 있는 경우에 TextView를 애니메이션합니다.

payload가 비어있을 때에는 아이템을 업데이트하지 않고 초기화해주도록 super를 호출하여 onBindViewHolder(holder, position)가 호출될 수 있도록 해줍니다.

payload는 Object 객체이기 때문에 기본 자료형(int, float, double, long)을 제외한 어떤 객체든 payload로 전달할 수 있습니다.

payload는 왜 List 인가?

payload가 List인 이유는 같은 holder에 notifyItemChanged()notifyItemRangeChanged()를 여러 번 호출한 payload들을 병합하기 때문입니다.
예를 들어 같은 position의 holder에 image1, image2, image3 이라는 뷰가 존재하고 각각 애니메이션을 하기 위해서 payload를 "click1", "click2", "click3"으로 notifyItemChanged()를 3번 호출해도,  onBindViewHolder()는 한번 불리고 payload의 size는 3개가 됩니다.
payload click1은 첫 번째 이미지를 애니메이션하고 click2는 두 번째, clcik3은 세 번째 이미지를 애니메이션 합니다.

같은 holder에 연달아 payload를 전달했을 때
같은 holder에 연달아 payload를 전달했을 때


다음은 코드는 notifyItemChanged(position, payload)와 notifyItemRangeChanged(positionStart, itemCount, payload) 연달아 3번 호출하고 있습니다. 다만, payload 값만 다르게 하여 호출할 것입니다.



그러면 3번을 호출했어도 onBindViewHolder(... List<Object> payloads)는 한 번만 호출됩니다. 그리고 List payloads의 사이즈는 3개이고 각각 "click1", "click2", "click3"이 배열에 들어있기 때문에 payload로 분기하여 각각의 ImageView를 애니메이션 할 수 있습니다.


그외...


notifyItemChanged(position, payload)에서 position이 리스트에서 보이지 않는 아이템을 호출한 경우 onBindViewHolder()가 호출되지 않습니다.

Recyclerview 아답터는 onBindViewHolder(... List<Object> payloads)  → onBindViewHolder(holder, position) 순으로 호출됩니다. payload의 size()를 체크하여 초기화할지 업데이트할지 결정해야 합니다.


수정해야 될 정보가 있다면 이메일 또는 댓글 부탁드립니다.




댓글

  1. 덕분에 해결했습니다. 감사합니다! ㅜㅜ

    답글삭제
  2. ... 뷰를 재활용하는 구간에서는 안먹히더라구요. spar....어쩌구가 있는데, 그걸로 해도
    재활용 구간에서는 바로 적용이 안되고, 스크롤해서 화면에 사라지게 한 후에, 다시 그 자리로
    스크롤해서 돌아와야, 적용되더라구요. 이건 어떻게 해결하셨죠?

    답글삭제

댓글 쓰기

이 블로그의 인기 게시물

코틀린 (Kotlin) filter, map, all, any, count, find, groupBy, flatMap 함수 정리

코틀린 (Kotlin) 인터페이스 정리