왜 DiffUtil 를 알아보는거야?
우리는 RecyclerView 를 사용할 때 RecyclerView.Adpater 에 notify 변경 메소드로 알려 아이템 변경을 감지하고 갱신할 수 있다는 것을 알고 있다.
보통 아이템 업데이트가 필요할 때, 아래의 메서드를 사용했다.
public final void notifyDataSetChanged() {
mObservable.notifyChanged();
}
전체 아이템을 다시 그리는 용도로 사용되는 메서드이다.
아이템 업데이트 과정에서 지연이 길어지면 UX에 영향을 미치기 때문에 가능한 적은 리소스와 함께 빠른 작업이 이루어져야 한다.
목록의 내용이 변경되면 notifyDataSetChanged()를 호출하여 아이템을 업데이트하지만 비용이 많이든다.
그래서 다른 생각을 한다.
변경된 데이터만 골라서 업데이트를 해주면 안될까?
당연히 된다.
상황에 맞는 데이터 변경을 할 수 있게 아래와 같은 메소드를 호출해주면 된다.
* @see #notifyItemChanged(int)
* @see #notifyItemInserted(int)
* @see #notifyItemRemoved(int)
* @see #notifyItemRangeChanged(int, int)
* @see #notifyItemRangeInserted(int, int)
* @see #notifyItemRangeRemoved(int, int)
메소드 명이 직관적이라 해당 메소드를 상황에 맞게 적용하면 된다. 그렇지만 아이템 별로 골라서 갱신하는 것은 쉬운일은 아니다. 어떤 아이템이 갱신이 필요한지, 아닌지에 대한 처리가 추가적으로 필요했기 때문에 우리는 편하게 notifyDataSetChanged 메서드를 사용했을 것이다.
오늘 블로그에서 설명하고자하는 DiffUtil 클래스를 사용하면, 위에서 말한 불편한 부분을 해소할 수 있다.
DiffUtil
DiffUtil 은 RecyclerView에서 데이터 업데이트 처리를 효율적으로 작업하기위해 만들어진 유틸 클래스이다.
위에서 언급했지만 다시 말을하자면, notifyDataSetChanged() 라는 함수가 있지만 뷰를 업데이트시키는 과정에서 모든 데이터를 다시 그리기 때문에 비효율적이므로 DiffUtil을 사용해 데이터들을 비교후 변경된 부분만 효율적으로 업데이트 할 수 있다
동작은 두 개의 데이터셋 을 받아서 그 차이를 계산해준다. 즉, 차이(변한 부분)만을 파악해서 RecyclerView.Adapter 에 반영할 수 있다.
이는 Eugene W. Myers 의 difference 알고리즘을 이용해서 O(N+D²) 시간복잡도를 갖고 리스트의 비교를 수행한다.
💡 N : 추가 및 제거된 항목의 갯수
💡 D : 스크립트의 길이
구글에 따르면, 넥서스5X 에서 DiffUitl 에 대한 테스트 결과가 아래와 같다고 발표했다.
위 성능 테스트 결과를 보다시피 아이템 갯수와 이동/변경되는 아이템 갯수가 많아지면 수행시간이 상당히 커지는 것을 알 수 있다.
DiffUtil 은 구현상 제약사항으로 인해 최대 사이즈는 2²⁶(67,108,864) 개까지 지원을 한다.
DiffUitl 사용방법
DiffUtil 콜백을 상속 받아서 areItemsTheSame(), areContentsTheSame() 오버라이드 된 함수를 구현해주면된다.
areItemsTheSame() : 비교대상인 두 아이템이 동일한 지 확인
areContentsTheSame() : 두 개의 아이템이 내용까지 동일한 지 확인
AsyncListDiffer
DiffUtil 은 아이템 개수가 많을 경우, 비교 연산하는 시간이 길어질 수 있기 때문에 백그라운드 스레드에서 처리가 되어야 한다.
그래서 AsyncListDiffer 라는 helper 클래스를 소개하겠다.
AsyncListDiffer는 DiffUtil 를 편하게 쓰기 위한 클래스로 사용한다.
DiffUtil 를 자체적으로 스레드 처리를 해주는 클래스다.
AsyncListDiffer 사용방법
구글에서 제공하는 소스이다.
아래와 같이 사용을 하면 된다.
@Dao
interface UserDao {
@Query("SELECT * FROM user ORDER BY lastName ASC")
public abstract LiveData<List<User>> usersByLastName();
}
class MyViewModel extends ViewModel {
public final LiveData<List<User>> usersList;
public MyViewModel(UserDao userDao) {
usersList = userDao.usersByLastName();
}
}
class MyActivity extends AppCompatActivity {
@Override
public void onCreate(Bundle savedState) {
super.onCreate(savedState);
MyViewModel viewModel = new ViewModelProvider(this).get(MyViewModel.class);
RecyclerView recyclerView = findViewById(R.id.user_list);
UserAdapter adapter = new UserAdapter();
viewModel.usersList.observe(this, list -> adapter.submitList(list));
recyclerView.setAdapter(adapter);
}
}
class UserAdapter extends RecyclerView.Adapter<UserViewHolder> {
private final AsyncListDiffer<User> mDiffer = new AsyncListDiffer(this, DIFF_CALLBACK);
@Override
public int getItemCount() {
return mDiffer.getCurrentList().size();
}
public void submitList(List<User> list) {
mDiffer.submitList(list);
}
@Override
public void onBindViewHolder(UserViewHolder holder, int position) {
User user = mDiffer.getCurrentList().get(position);
holder.bindTo(user);
}
public static final DiffUtil.ItemCallback<User> DIFF_CALLBACK
= new DiffUtil.ItemCallback<User>() {
@Override
public boolean areItemsTheSame(
@NonNull User oldUser, @NonNull User newUser) {
// User properties may have changed if reloaded from the DB, but ID is fixed
return oldUser.getId() == newUser.getId();
}
@Override
public boolean areContentsTheSame(
@NonNull User oldUser, @NonNull User newUser) {
// NOTE: if you use equals, your object must properly override Object#equals()
// Incorrectly returning false here will result in too many animations.
return oldUser.equals(newUser);
}
}
}
참고
https://developer.android.com/reference/kotlin/androidx/recyclerview/widget/AsyncListDiffer
'Mobile App' 카테고리의 다른 글
[안드로이드] MVVM 패턴과 안드로이드 MVVM 패턴 (0) | 2023.02.11 |
---|---|
[안드로이드] - ListAdapter (0) | 2023.02.02 |
[안드로이드] - 클린아키텍처 - 3 (0) | 2023.02.01 |
[안드로이드] - 클린아키텍처 - 2 (3) | 2023.02.01 |
[안드로이드] - 클린아키텍처 - 1 (0) | 2023.01.29 |