일 | 월 | 화 | 수 | 목 | 금 | 토 |
---|---|---|---|---|---|---|
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 |
- ktor api call
- Zsh
- exoplayer cache
- ListAdapter DiffUtil
- 독서
- list map
- FastAPI
- DiffUtil.ItemCallback
- 안드로이드
- 유튜브
- ChatGPT
- android ktor
- ktor client
- kotlin list
- getChangePayload
- doc2vec
- kotlin collection
- Python
- ExoPlayer
- video caching
- android exoplayer
- 스피너
- 시행착오
- AWS EC2
- llm
- android
- build with ai
- ListAdapter
- android custom view
- map
- Today
- Total
버튼 수집상
[안드로이드] ListAdapter DiffUtil 제대로 쓰기 - 1 본문
배경
리사이클러뷰에서 부분적으로 UI를 업데이트할 때 (ex: 찜하기)
업데이트한 리스트를 submitList()로 세팅해도 DiffUtil이 제대로 돌아가지 않는 경우가 있었다.
리스트 변경사항을 제대로 감지하는 경우를 알기 위해 샘플 프로젝트를 구성해봤다.
샘플 프로젝트 구조
리스트 아이템을 클릭하면 api를 변경된 index를 호출한 뒤 리스트에 반영.
api의 결과로 새 리스트를 뿌리는 구조는 비효율적이라고 판단했다.
참고한 앱도 변경값만 리턴하고 있었다.
샘플 데이터 클래스
data class SimpleObject(
var name : String,
var isChecked : Boolean = false
)
BaseActivity.kt
abstract class BaseActivity(layout : Int): AppCompatActivity(layout) {
init {
observeEvent()
}
// 공통함수
abstract fun observeEvent()
}
EventBus.kt
// 뷰홀더에서 발생한 이벤트를 액티비티 단에서 관리하기 위한 오브젝트
object EventBus {
private val clickEvent_ = MutableLiveData<ClickEvent>()
val clickEvent: LiveData<ClickEvent>
get() = clickEvent_
fun fire(event: ClickEvent) {
clickEvent_.postValue(event)
}
}
class ClickEvent(
val index: Int,
val message: String
) {
override fun toString(): String {
return "ClickEvent - [${index}] \"${message}\""
}
}
MainActvity.kt
class MainActivity : BaseActivity(R.layout.actvity_main) {
// 기존 리스트
private var originalObjects = List(10) { i -> SimpleObject("item$i") }
private val rv by lazy { findViewById<RecyclerView>(R.id.list) }
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
rv.adapter = SimpleAdapter().apply {
submitList(originalObjects)
}
}
override fun observeEvent() {
EventBus.clickEvent.observe(this) { event ->
// api 호출해서 결과를 받아왔다고 가정
(rv.adapter as? SimpleAdapter)?.submitList(updatedList(event.index))
}
}
private fun updatedList(index: Int) : List<SimpleObject> {
// api 결과값을 리스트 업데이트
// submitList not working!
return originalObjects.toList().mapIndexed{ i, it ->
it.apply { isChecked = i == index }
}
}
}
SimpleAdapter.kt
class SimpleAdapter : ListAdapter<SimpleObject, SimpleAdapter.SimpleHolder>(object : DiffUtil.ItemCallback<SimpleObject>() {
override fun areContentsTheSame(oldItem: SimpleObject, newItem: SimpleObject): Boolean {
return oldItem == newItem
}
override fun areItemsTheSame(oldItem: SimpleObject, newItem: SimpleObject): Boolean {
return oldItem.name == newItem.name
}
// isChecked가 true인 뷰홀더 체크
override fun getChangePayload(oldItem: SimpleObject, newItem: SimpleObject): Any? {
return if (oldItem.isChecked == newItem.isChecked) {
super.getChangePayload(oldItem, newItem)
} else {
newItem.isChecked
}
}
}) {
override fun getItemViewType(position: Int) = position
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): SimpleHolder =
SimpleHolder(LayoutInflater.from(parent.context).inflate(R.layout.item_grid, parent, false))
override fun onBindViewHolder(holder: SimpleHolder, position: Int) {
holder.itemView.findViewById<TextView>(R.id.title).text = currentList[position].name
holder.itemView.setOnClickListener {
// 클릭 이벤트 (api 호출)
EventBus.fire(ClickEvent(position, "isClicked"))
}
}
override fun onBindViewHolder(holder: SimpleHolder, position: Int, payloads: MutableList<Any>) {
if (payloads.isEmpty()) {
super.onBindViewHolder(holder, position, payloads)
} else {
(payloads[0] as? Boolean)?.let {
holder.turnOn(it)
} ?: run {
super.onBindViewHolder(holder, position, payloads)
}
}
}
class SimpleHolder(private val view: View) : RecyclerView.ViewHolder(view) {
// isChecked 일 때 이미지뷰에 빨간색 컬러필터 적용
fun turnOn(isChecked: Boolean) {
val bgColor = if (isChecked) Color.parseColor("#30ff0000") else Color.parseColor("#00ff0000")
view.findViewById<ImageView>(R.id.iv).setColorFilter(bgColor)
}
}
}
현재 이 코드로는 리스트가 제대로 변경되지 않는다.
private fun updatedList(index: Int) : List<SimpleObject> {
// api 결과값을 리스트에 업데이트
// submitList not working!
return originalObjects.toList().mapIndexed{ i, it ->
it.apply { isChecked = i == index }
}
}
toList()를 호출하면 별개의 리스트가 생성되는 줄 알았는데,
리스트 요소 객체가 바뀌지 않으면 리스트의 주소가 달라지지 않는다.
두 리스트의 주소가 같기 때문에
복사본의 map 블럭 안에서 요소 객체에 apply 를 적용하면 원본의 요소도 바뀐다!
두 리스트를 비교하기 위해 equals() 함수를 호출.
private fun updatedList(index: Int) : List<SimpleObject> {
// api 결과값을 리스트에 업데이트
// submitList not working!
val copiedObjects = originalObjects.toList().mapIndexed{ i, it ->
if (i == index) it.apply { isChecked = true }
else it
}
// 리스트 객체 비교
Log.v("check", "${originalObjects.equals(copiedObjects)}") // true
// zip - 두 리스트 요소를 pair 로 묶어서 1차원 리스트로 만들어줌
val changedIndex = copiedObjects.zip(originalObjects).mapIndexedNotNull { i, (item1, item2) ->
// 리스트 요소 객체 비교
if (item1.equals(item2)) null else i
}
Log.v("check", "$changedIndex") // []
return copiedObjects
}
객체의 필드를 수정하는 게 아니라 객체 자체를 갈아치워야 한다.
map 블럭 안에서 객체를 새로 생성하면 리스트의 주소가 달라지므로 toList() 를 다시 호출할 필요가 없다.
private fun updatedList(index: Int) : List<SimpleObject> {
// api 결과값을 리스트에 업데이트
val copiedObjects = originalObjects.mapIndexed{ i, it ->
SimpleObject(
name = it.name,
isChecked = i == index
)
}
// 리스트 객체 비교
Log.v("check", originalObjects.equals(copiedObjects)) // false
return copiedObjects
}
코틀린 data class가 기본으로 제공하는 copy() 함수를 써도 똑같다.
private fun updatedList(index: Int) : List<SimpleObject> {
// api 결과값을 리스트에 업데이트
return originalObjects.mapIndexed{ i, it ->
it.copy().apply { isChecked = it == index }
}
}
그러면 UI 업데이트가 잘 되는 것을 확인할 수 있다.
만약 클릭이벤트를 누적시키고 싶으면
업데이트된 리스트를 원본 리스트 변수에 할당한다.
private fun updatedList(index: Int) : List<SimpleObject> {
// api 결과값을 리스트에 업데이트
val copiedObjects = originalObjects.toList().mapIndexed{ i, it ->
SimpleObject(
name = it.name,
isChecked = if (i == index) !it.isChecked else it.isChecked // on/off
)
}
originalObjects = copiedObjects
return copiedObjects
}
그러면 이전 클릭 기록을 남길 수 있다.
2편에서 DiffUtil.ItemCallback의 함수들을 자세히 살펴보겠다.
참고
'TIL - 안드로이드' 카테고리의 다른 글
[안드로이드] 중복체크 되는 리스트 Preference 저장하고 테스트하기 (0) | 2023.07.20 |
---|---|
[안드로이드] ListAdapter DiffUtil 제대로 쓰기 - 2 (0) | 2023.07.13 |
[안드로이드] 시인성 향상 애니메이션 제거 여부 알기 (0) | 2023.07.03 |
[안드로이드] RecyclerView에 radius 적용하기 (0) | 2023.07.03 |
[안드로이드] ViewPager 내부 웹뷰 가로 스크롤 처리하기 (0) | 2023.06.21 |