DiffUtil(+ListAdapter) 구현 시 고려 사항

14246 단어 Android
이 글은 Android #2 Advent Calendar 2019 의 10일째 글입니다.

입문


DiffUtil과 ListAdapter에서 목록(RecyclerView)을 실현한 후 ListAdapter.submitList()에서 프로젝트 그룹을 보내면 DiffUtil 측에서 데이터 차이를 얻는 데 필요한 프로젝트의 추가, 업데이트, 삭제를 마음대로 할 수 있다.
그렇게 편리한 DiffUtil이지만 자신의 설치에서 그 동작을 오해했기 때문에 전철을 밟는 사람이 생기지 않도록 그 주의점을 공유하고 싶습니다.
참고로 이번 대상 DiffUtil은 다음과 같이 간단하게 제작되었습니다.
SampleDiffUtilListAdapter
class SampleDiffUtilListAdapter :
    ListAdapter<SampleDiffUtilListAdapter.SampleDillUtilItem, RecyclerView.ViewHolder>(DIFF_CALLBACK) {

    companion object {
        private val DIFF_CALLBACK = object : DiffUtil.ItemCallback<SampleDillUtilItem>() {
            override fun areItemsTheSame(
                oldItem: SampleDillUtilItem,
                newItem: SampleDillUtilItem
            ): Boolean {
                return oldItem.id == newItem.id
            }

            override fun areContentsTheSame(
                oldItem: SampleDillUtilItem,
                newItem: SampleDillUtilItem
            ): Boolean {
                return oldItem.count == newItem.count
            }
        }
    }

    // 中略

    data class SampleDillUtilItem(
        val id: Int
    )
}
한편 전체 샘플 코드는 GitHub에서 공개됐다.
SampRa-android/app/src/main/java/io/github/yamacraft/app/sampra/ui/diffutil

areContentsTheSame()에서 데이터 업데이트가 감지되지 않았습니다.


업데이트를 통해 목록에 전송된 항목 그룹submitList()을 보내고, DiffUtil은 차이를 검사하고, 변경된 항목의 표시를 업데이트합니다.
이때 데이터 차분을 진행한 것은 areContentsTheSame() 이다.
예를 들어 Sample Dill Util Item에count라는 변수를 추가하여 목록에서 이 항목이 클릭된 횟수를 기록하고 목록에 반영합니다.
SampleDiffUtilViewModel
private val _items = MutableLiveData<List<SampleDiffUtilListAdapter.SampleDillUtilItem>>()
val items: LiveData<List<SampleDiffUtilListAdapter.SampleDillUtilItem>> = _items

fun itemCountUp(id: Int) {
    _items.value = _items.value?.map {
        if (it.id == id) {
            it.count = it.count + 1
        }
        it
    }
}
SampleDiffUtilActivity
class SampleDiffUtilActivity : AppCompatActivity() {

    private lateinit var viewModel: SampleDiffUtilViewModel
    private lateinit var listAdapter: SampleDiffUtilListAdapter

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_list_adapter)

        viewModel = ViewModelProviders.of(this)[SampleDiffUtilViewModel::class.java]

        viewModel.items.observe(this, Observer {
                listAdapter.submitList(it)
            })
        }

        listAdapter = SampleDiffUtilListAdapter().apply {
            // クリックリスナーの実装部分の解説は省略します
            onItemClickListener = {
                viewModel.itemCountUp(it.id)
            }
        }

        recycler_view.apply {
            adapter = listAdapter
            layoutManager = LinearLayoutManager(context)
        }
    }
}
이 구현에서, 목록에 있는 항목을 클릭할 때마다 submitList() 발송될 때까지 item을 업데이트할 수 있지만, 목록의 표시는 업데이트되지 않습니다.
문제는 itemCountUp() 의 처리입니다.
이 방법은 areContentsTheSame()의oldItem과 newItem이 같은 대상을 인용하기 때문에 비교 결과가 진실로 확정되어 목록 업데이트 처리가 되지 않습니다.
SampleDiffUtilViewModel
fun itemCountUp(id: Int) {
    _items.value = _items.value?.map {
        if (it.id == id) {
            it.copy(count = it.count + 1)
        } else {
            it
        }
    }
}
이렇게 사용copy()하면 해당 항목이 대상에 따라 달라집니다.
이렇게 하면count의 변수 자체도mutable이 필요하지 않습니다.우리는val로 성명합시다.
원래 이러한 오류를 없애기 위해 프로젝트 배열에 사용되는 데이터 클래스의 변수는 모두 Immutable로 선언해야 합니다.

DiffUtil 데이터 추가 사양 정보


DiffUtil이 사용자의 UX를 손상시키지 않도록 현재 표시된 항목에 따라 데이터를 추가하거나 업데이트합니다.
예를 들어 Swipe To Refresh를 구현하여 업데이트할 때마다 무작위로 고정된 데이터를 다시 배열하여 가져옵니다.
SampleDiffUtilViewModel
fun refresh() {
   _items.value = createItems()
}

private fun createItems(): List<SampleDiffUtilListAdapter.SampleDillUtilItem> {
   val items = mutableListOf<SampleDiffUtilListAdapter.SampleDillUtilItem>()
   for (i in 0..100) {
      items.add(SampleDiffUtilListAdapter.SampleDillUtilItem(i, 0))
   }
   return items.toList().shuffled()
}
움직이면 이렇게 움직인다.

현재 상단에 표시된 항목은 시각적 이동이 아니라 목록에 있는 데이터를 추가하거나 이동한 것을 볼 수 있습니다.
이 동작은 DiffUtil 사양명세입니다.
따라서 업데이트된 후에 목록을 맨 위로 스크롤하려면 두 가지 방법이 있습니다.
업데이트 데이터를 보내기 전에 null을 submitList()에 보내서 목록을 비우는 방법도 있고,listAdapter의 Adapter DataObserver를 사용하여 매번 데이터 업데이트scrollToPosition()를 하는 방법도 있습니다.
[참조] android - Recycler view not scrolling to the top after adding new item at the top, as changes to the list adapter has not yet occurred - Stack Overflow
한 번에 목록을 비우는 방법은 한 순간에 목록이 하얗게 변하는 순간이 아니라 쉽게 실현할 수 있다.
Adapter DataObserver에서 제어하는 방법은 깜박임이 없는 것이 아니라 설치의 난이도를 높입니다. (적당히 설치하면 무심코 업데이트해도 스크롤이 이동합니다.)

총결산

  • 목록에 사용된 항목(data class), 모두 Immutable로 설명하십시오
  • DiffUtil이 데이터를 추가할 때의 행동을 파악한 후 구현하십시오
  • 좋은 웹페이지 즐겨찾기