Notice
Recent Posts
Recent Comments
Link
«   2025/07   »
1 2 3 4 5
6 7 8 9 10 11 12
13 14 15 16 17 18 19
20 21 22 23 24 25 26
27 28 29 30 31
Archives
Today
Total
관리 메뉴

안드로이드 개발일기

[Android] RecyclerView, DiffUtil사용 시 데이터가 갱신되지 않는 이슈(DiffUtil이 데이터 변경을 판단하는 기준) 본문

android

[Android] RecyclerView, DiffUtil사용 시 데이터가 갱신되지 않는 이슈(DiffUtil이 데이터 변경을 판단하는 기준)

lolvlol 2023. 7. 6. 19:07
class MarketDiffUtil(
    private val oldList: List<Person>,
    private val newList: List<Person>
) : DiffUtil.Callback()


data class Person(
	var name: String,
    var age: Int
)

class PersonAdapter: RecyclerView.Adapter() {
	...
    private var personList: List<Person>? = null
    
    fun submitList(list: List<Person>?) {
        val diffUtil = DiffUtil.calculateDiff(
            MarketDiffUtil(
                oldList = this.personList ?: emptyList(),
                newList = list ?: emptyList()
            )
        )
        diffUtil.dispatchUpdatesTo(this)
        this.marketList = list
    }
    ...
}

class PersonViewModel: ViewModel() {
	...
    private val _personList = MutableLiveData<List<Person>?>()
    val personList: LiveData<List<Person>?>
        get() = _personList
    ...
}

class PersonFragment: Fragment() {
	...
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)

        viewModel.personList.observe(this) { list ->
            adapter.submitList(list)
        }
    }
    ...
}

화면에 보여지는 데이터 리스트는 personList이고, LiveData를 이용하여 personList가 변경되면 observe를 타고 DiffUtil이 내부 데이터 변화를 판단하여 화면이 갱신되는 구조입니다.

 

 Q1. personList[1].name 을 "Tom"으로 변경하는 코드를 아래처럼 할 경우 화면이 갱신될까요?

private fun changePersonName() {
	_personList.value = _personList.value?.apply { 
        this.getOrNull(1).name = "Tom"
    }
}

A1. 아닙니다.

DiffUtil의 oldList와 newList가 바라보는 리스트가 동일하여 이미 변경된 원소를 보고 있기 때문에 리스트 갱신이 일어나지 않았다고 판단합니다.

따라서 newList는 oldList(_personList.value)를 카피한 새 리스트여야 합니다. 따라서 새 리스트를 반환하는 toMutableList(), Collection 함수 등(map, filter ...)을 사용할 수 있겠습니다.

 

Q2. oldList를 카피한 newList의 원소를 갱신하면 이제 정상 동작할까요?

private fun changePersonName() {
	_personList.value = _personList.value?.toMutableList?.apply { 
        this.getOrNull(1).name = "Tom"
    }
}

A2. 아닙니다.

리스트가 deepCopy되지 않았으므로 원소들은 동일한 주소값을 바라보고 있기 때문에 Tom으로 변경된 값은 oldList, newList 모두 반영됩니다.

따라서 변경될 원소를 copy하여 새로 만들어야 합니다. 이 경우 oldList[1]과 newList[1]이 바라보는 객체가 달라지므로(주소값이 다르므로) oldList[1]은 Tom을 반영하지 않게 됩니다.

private fun changePersonName() {
	_personList.value = _personList.value?.mapIndexed { index, personData ->
            if (index == 1) {
                personData.copy(name = "Tom")
            } else {
                personData
            }
    }
}

위 코드처럼 데이터 변경 시 화면에 정상적으로 그려짐을 확인할 수 있습니다.

 

결론

1. DiffUtil은 주소값을 기준으로 판단하기 때문에, oldList와 newList가 다른 객체가 맞는지 확인합니다.

2. 리스트의 원소의 파라미터를 변경하는 경우 그 원소는 새 객체가 되어야 합니다.

-> 따라서 원소의 파라미터를 변경할 수 없도록 val로 선언하여 DiffUtil의 리스트, 원소는 Immutable임을 지키는 것이 좋습니다(위 코드 data class Person의 파라미터들을 모두 val로 변경하여 immutable임을 보장할 수 있습니다).

Note that DiffUtil, ListAdapter, and AsyncListDiffer require the list to not mutate while in use. This generally means that both the lists themselves and their elements (or at least, the properties of elements used in diffing) should not be modified directly. Instead, new lists should be provided any time content changes. It's common for lists passed to DiffUtil to share elements that have not mutated, so it is not strictly required to reload all data to use DiffUtil.

https://developer.android.com/reference/androidx/recyclerview/widget/DiffUtil

Comments