그런데 데이터가 변경되는 방식을 확인하고 그때마다 이렇게 notify를 일일이 해 주는것은 번거롭기도 하고, 또 사용하기에 따라서는 갱신이 필요없는 ViewHolder를 같이 갱신하는 불필요한 작업이 생길수도 있습니다.
DiffUtil
DiffUtil은 두 데이터셋을 받아서 그 차이를 계산해주는 클래스입니다. DiffUtil을 사용하면 두 데이터 셋을 비교한 뒤 그중 변한부분만을 파악하여 Recyclerview에 반영할 수 있습니다.
DiffUtil은 Eugene W. Myers의 difference 알고리즘을 이용해서 O(N + D^2)시간 안에 리스트의 비교를 수행하는데 넥서스 5X에서 테스트를 수행한 결과는 다음과 같았다고 합니다. 이때 N은 추가 및 제거된 항목의 갯수이고, D는 스크립트의 길이입니다.
1
2
3
4
5
6
7
100 items and 10 modifications: avg: 0.39 ms, median: 0.35 ms
100 items and 100 modifications: 3.82 ms, median: 3.75 ms
100 items and 100 modifications without moves: 2.09 ms, median: 2.06 ms
1000 items and 50 modifications: avg: 4.67 ms, median: 4.59 ms
1000 items and 50 modifications without moves: avg: 3.59 ms, median: 3.50 ms
1000 items and 200 modifications: 27.07 ms, median: 26.92 ms
1000 items and 200 modifications without moves: 13.54 ms, median: 13.36 ms
DiffUtil은 아이템 개수가 많을 경우 비교연산에 필요한 시간이 길어질 수 있기 때문에 백그라운드 스레드에서 처리되어야 합니다. AsyncListDiffer는 DiffUtil을 편하게 쓰기 위해서 만들어진 클래스로, DiffUtil에 대해 자체적으로 스레드 처리를 해 줍니다.
코드를 사용하기 위해서는 우선 어댑터 내부로 DiffUtil 콜백을 전달받은 AsyncListDiffer 객체를 만들어 줍니다. 그리고 currentList로 데이터를 참조하고 submitList 로 리스트 데이터를 갱신하면 됩니다.
// Google Developers에서 제공하는 코드classUserAdapterextendsRecyclerView.Adapter<UserViewHolder>{privatefinalAsyncListDiffer<User>mDiffer=newAsyncListDiffer(this,DIFF_CALLBACK);@OverridepublicintgetItemCount(){returnmDiffer.getCurrentList().size();}publicvoidsubmitList(List<User>list){mDiffer.submitList(list);}@OverridepublicvoidonBindViewHolder(UserViewHolderholder,intposition){Useruser=mDiffer.getCurrentList().get(position);holder.bindTo(user);}publicstaticfinalDiffUtil.ItemCallback<User>DIFF_CALLBACK=newDiffUtil.ItemCallback<User>(){@OverridepublicbooleanareItemsTheSame(@NonNullUseroldUser,@NonNullUsernewUser){// User properties may have changed if reloaded from the DB, but ID is fixedreturnoldUser.getId()==newUser.getId();}@OverridepublicbooleanareContentsTheSame(@NonNullUseroldUser,@NonNullUsernewUser){// NOTE: if you use equals, your object must properly override Object#equals()// Incorrectly returning false here will result in too many animations.returnoldUser.equals(newUser);}}}
ListAdapter
ListAdapter는 AsyncListDiffer를 더 쓰기 편하도록 랩핑한 클래스로 Recyclerview 어댑터를 만들때 ListAdapter를 상속하도록 하면 됩니다. 초기화할때 DiffUtil 콜백 객체를 받도록 하면 나머지는 AsyncListDiffer와 같이 currentList로 현재 데이터를 불러올 수 있고, submitList로 데이터를 갱신할 수 있습니다.
// Google Developers에서 제공하는 코드
classUserAdapterextendsListAdapter<User,UserViewHolder>{publicUserAdapter(){super(User.DIFF_CALLBACK);}@OverridepublicvoidonBindViewHolder(UserViewHolderholder,intposition){holder.bindTo(getItem(position));}publicstaticfinalDiffUtil.ItemCallback<User>DIFF_CALLBACK=newDiffUtil.ItemCallback<User>(){@OverridepublicbooleanareItemsTheSame(@NonNullUseroldUser,@NonNullUsernewUser){// User properties may have changed if reloaded from the DB, but ID is fixed
returnoldUser.getId()==newUser.getId();}@OverridepublicbooleanareContentsTheSame(@NonNullUseroldUser,@NonNullUsernewUser){// NOTE: if you use equals, your object must properly override Object#equals()
// Incorrectly returning false here will result in too many animations.
returnoldUser.equals(newUser);}}}
다시말해 Recyclerview 어댑터를 ListAdapter로 구현하면 데이터가 어떻게 바뀌든간에 submitList로 전체 리스트를 넘겨주기만 하면 어댑터가 알아서 백그라운드 스레드를 사용해 리스트 차이를 계산하여 화면을 갱신시켜주게 됩니다.
Referential equality (===- two references point to the same object)
areItemsTheSame에서 비교대상인 두 객체가 Referential equality를 갖는지 판정하는데에는 ===를 이용하였습니다. 그리고 areContentsTheSame 에서 두 객체의 아이템이 Structural equality를 갖는지 판정하는데에는 equals를 사용했습니다. Monster가 데이터클래스이기 때문에 데이터클래스의 정의에 따라 ==을 equals 대신 사용할 수 있습니다.
ListAdapter를 상속받는 Recyclerview Adapter를 작성합니다. onCreateViewHolder에서 뷰바인딩을 사용하도록 설정합니다. 데이터의 개수는 ListAdapter가 관리하므로 기존의 Recyclerview 어댑터에서 정의했던 getItemCount는 오버라이드하지 않아도 됩니다.
그리고 아이템을 이동했을때 실행할 moveItem, 지울때 실행할 removeItem 메소드를 작성해줍니다. 이 때 ListAdapter에서는 데이터를 직접 조작할 수 없고 비교만 할수 있기 때문에 변형된 데이터셋은 새로운 객체로 만들어 기존의 데이터셋과 비교도록 submitList를 수행하게 합니다.