IT_Programming/Android_Java

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

JJun ™ 2018. 8. 1. 08:28



 * 출처

 : https://zerogdev.blogspot.com/2018/07/recyclerview-notifyitemchanged-payload.html

 : https://alpoxdev.github.io/2018/07/31/안드로이드2/




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를 사용한 업데이트


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

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

RecyclerView recyclerView = findViewById(R.id.recyclerview);
LinearLayoutManager linearLayoutManager = new LinearLayoutManager(this, LinearLayoutManager.VERTICAL, false);
recyclerView.setLayoutManager(linearLayoutManager);
recyclerView.setItemAnimator(null);
final ArrayList<String> items = new ArrayList<>();
items.add("hello~ item1");
items.add("hello~ item2");
items.add("hello~ item3");
final RecyclerAdapter adapter = new RecyclerAdapter(items);
recyclerView.setAdapter(adapter);
findViewById(R.id.last_item_favorite_button).setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
//refresh last item
adapter.notifyItemChanged(adapter.getItemCount() - 1, "click");
}
});
findViewById(R.id.all_item_favorite_button).setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
//refresh all item
adapter.notifyItemRangeChanged(0,adapter.getItemCount(), "click");
}
});



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를 전달받아 뷰를 업데이트하거나 애니메이션을 실행할 수 있습니다.

@Override
public void onBindViewHolder(@NonNull RecyclerView.ViewHolder holder, int position) {
TextHolder textHolder = (TextHolder) holder;
textHolder.mTextView.setText(items.get(position));
}
@Override
public void onBindViewHolder(@NonNull RecyclerView.ViewHolder holder, int position, @NonNull List<Object> payloads) {
if (payloads.isEmpty()) {
super.onBindViewHolder(holder, position, payloads);
}else {
for (Object payload : payloads) {
if (payload instanceof String) {
String type = (String) payload;
if (TextUtils.equals(type, "click") && holder instanceof TextHolder) {
TextHolder textHolder = (TextHolder) holder;
textHolder.mFavorite.setVisibility(View.VISIBLE);
textHolder.mFavorite.setAlpha(0f);
textHolder.mFavorite.setScaleX(0f);
textHolder.mFavorite.setScaleY(0f);
//animation
textHolder.mFavorite.animate()
.scaleX(1f)
.scaleY(1f)
.alpha(1f)
.setInterpolator(new OvershootInterpolator())
.setDuration(300);
}
}
}
}
}


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를 전달했을 때



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

findViewById(R.id.last_item_favorite_button).setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
//refresh last item
adapter.notifyItemChanged(adapter.getItemCount() - 1, "click1");
adapter.notifyItemChanged(adapter.getItemCount() - 1, "click2");
adapter.notifyItemChanged(adapter.getItemCount() - 1, "click3");
}
});
findViewById(R.id.all_item_favorite_button).setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
//refresh all item
adapter.notifyItemRangeChanged(0, adapter.getItemCount(), "click1");
adapter.notifyItemRangeChanged(0, adapter.getItemCount(), "click2");
adapter.notifyItemRangeChanged(0, adapter.getItemCount(), "click3");
}
});



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

@Override
public void onBindViewHolder(@NonNull RecyclerView.ViewHolder holder, int position, @NonNull List<Object> payloads) {
if (payloads.isEmpty()) {
super.onBindViewHolder(holder, position, payloads);
} else {
for (Object payload : payloads) {
if (payload instanceof String) {
String type = (String) payload;
if (holder instanceof TextHolder) {
TextHolder textHolder = (TextHolder) holder;
if (TextUtils.equals(type, "click1")) {
startAnim(textHolder.mFavorite1);
} else if (TextUtils.equals(type, "click2")) {
startAnim(textHolder.mFavorite2);
} else if (TextUtils.equals(type, "click3")) {
startAnim(textHolder.mFavorite3);
}
}
}
}
}
}
private void startAnim(ImageView imageView) {
imageView.setVisibility(View.VISIBLE);
imageView.setAlpha(0f);
imageView.setScaleX(0f);
imageView.setScaleY(0f);
imageView.animate()
.scaleX(1f)
.scaleY(1f)
.alpha(1f)
.setInterpolator(new OvershootInterpolator())
.setDuration(300);
}


그외...


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


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






< Android > 안드로이드 리사이클러뷰 - Adapter 데이터 변경시


RecyclerView 의 Adapter 속성

안드로이드 어플을 개발중 RecyclerView 를 사용하고 있다.
서버에서 데이터를 불러오고, 데이터를 갱신 해야한다.

또한 메뉴바 또한 RecyclerView 의 Horizontal 기법으로 만들어서
한 화면에 리사이클러뷰가 두개가 있는 상태다.

아이템이 실시간으로 변경되거나, 재갱신해야하는 상황이 오는데
주의사항과 그에 따른 메소드를 설명하고자 한다.

시작하기 전에 앞서, 주의사항

  1. Adapter 의 데이터 변경 메쏘드 (notify) 는 Activity 에서 실행해야한다.

  2. Adpater 안에있는 List 들을 변경해야한다.

내 코드들을 간단하게 보자.

1
2
3
4
5
6
7
8
9
10
11
12
13
@Override
public ArrayList<Posts> getAll() throws InterruptedException, ExecutionException, JSONException {
ArrayList<Posts> newArray = presenter.getAll();
return newArray;
}

@Override
public void setAll() throws InterruptedException, ExecutionException, JSONException {
ArrayList<Posts> setArray = getAll();
postsAdapter.resetAll(setArray);

postsAdapter.notifyDataSetChanged();
}

MVP 패턴으로 짜서 많은 부분을 건드려야 하지만, 코드의 분리가 확실히 된다.
getAll 메쏘드에서 처음의 Posts 를 받아오고, newArray 를 반환해준다.
setAll 메쏘드에서 getAll 메쏘드를 받아오고, postsAdapter의 resetAll 에게 setArray를 받아온다.
그리고 전체 List 가 바뀌었다! 라는 Adapter의 기본 메쏘드를 Activity 안에서 불러준다.

1
2
3
4
5
public void resetAll(ArrayList<Posts> newPosts) {
Setting.list_count = 0;
this.posts = null;
this.posts = newPosts;
}

resetAll 함수를 보면 일단 확실하게 해주기 위해서
posts(ArrayList) 를 null 로 처리해준다음,
파라미터를 통해 받아온 리스트를 넣어준다.

Adpater의 notifyDataSetChanged() 를 불러줌으로써,
내부의 posts ArrayList 가 변경됨을 알려주고 재갱신해준다.

외부 블로그를 통해
Adpater의 데이터 변경시 알려주는 메쏘드들의 설명을 가져왔다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23

notifyDataSetChanged : 데이터가 전체 바뀌었을 때 호출. 즉, 처음 부터 끝까지 전부 바뀌었을 경우



notifyItemChanged : 특정 Position의 위치만 바뀌었을 경우. position 4 번 위치만 데이터가 바뀌었을 경우 사용 하면 된다.

notifyItemRangeChanged : 특정 영역을 데이터가 바뀌었을 경우. position 3~10번까지의 데이터만 바뀌었을 경우 사용 하면 된다.



notifyItemInserted : 특정 Position에 데이터 하나를 추가 하였을 경우. position 3번과 4번 사이에 넣고자 할경우 4를 넣으면 되겠죠

notifyItemRangeInserted : 특정 영역에 데이터를 추가할 경우. position 3~10번 자리에 7개의 새로운 데이터를 넣을 경우

notifyItemRemoved : 특정 Position에 데이터를 하나 제거할 경우.

notifyItemRangeRemoved : 특정 영역의 데이터를 제거할 경우

notifyItemMoved : 특정 위치를 교환할 경우 (Drag and drop에 쓰이겠네요^^)