[Android/Kotlin] Paging3, PagingDataAdapter의 Item에 상태 적용하는 방법에 대해

Falco·2022년 8월 13일
0

Android

목록 보기
18/55

Problem

Pager 3 library을 사용한 RecyclerView에서 사진을 선택하고 스크롤을 진행하고 나면 Item들이 섞이는 문제

RecyclerView의 특성상 뷰를 재사용함에 있어 각각의 뷰에 State를 지정하는데에 있어 문제가 발생했다.

다음과 같은 큐브스테이크 사진을 선택하고 스크롤을 내렸다가 다시 올라오면

큐브 스테이크의 선택된 상태 State가 해제 되어 있음을 볼 수 있다.


시도한 방법 1

Android PagingAdapter

snapshot()
ItemSnapshotList활성화된 경우 자리 표시자를 포함하여 현재 표시된 항목을 나타내는 새 항목을 반환합니다.

PagingAdapter 내에서 snapshot()을 이용해 현재 표시중인 List를 가지고와 선택한 Item의 선택된 State를 true로 바꾸어줌

holder.itemView.setOnClickListener { view ->
   snapshot().map {
   		// 나머지 사진들 false로 설정
   		if (it != null) {	
        	it.isSelected = false
		}
    }
   snapshot()[position]!!.isSelected = true
   notifyDataSetChanged()
}

viewholder내의 bind에서는 isSelected의 값에 따른 분기 렌더링

fun bind(galleryPhoto: GalleryPhoto) {
        if (galleryPhoto.isSelected) {
             ...
        } else {
             ...
        }
}  

이는 스크롤을 내려갔다 올라오면서 Adapter에 SubmitData되는 요소에는 적용되지 않음 즉 새로운 페이징 데이터가 들어오면 State가 사라진다.

시도한 방법 2

흔히 RecyclerView의 재사용으로 데이터가 꼬일 때 사용하는 방법들을 사용해 보았다.

아래와 같은 소스는 PagingAdapter와 호환이 안맞는 듯 하다. 아예 적용되지 않는다.

override fun getItemViewType(position: Int): Int {
        return position
}

그렇다고 Recycleable을 false로 지정하면 "Recycler"View를 사용하는 의미가 없어지고 이또한 적용되지 않는다.

holder.setIsRecyclable(false);

diffUtil Callback 객체도 건드려 보았다.
첫번째로 areItemsTheSame에서 아이템이 같은 아이템인지 체크하고,
두번째로 areContentsTheSame에서 각각의 콘텐츠가 같은 콘텐츠인지 검증하여 callback을 날려준다.

areContentsTheSame에서 isSeleceted를 적용 콘텐츠로 생각하고 소스를 작성해보았지만 이또한 적용되지 않는다.

companion object {
        private val GalleryPhotoDiffCallback = object : DiffUtil.ItemCallback<GalleryPhoto>() {
            override fun areItemsTheSame(oldItem: GalleryPhoto, newItem: GalleryPhoto): Boolean {
                // 이것에서 이전 아이템과 새로운 아이템이 같은지 비교하기 위해 고유 식별자를 비교 합니다.
                return oldItem.id == newItem.id
            }

            override fun areContentsTheSame(oldItem: GalleryPhoto, newItem: GalleryPhoto): Boolean {
                return oldItem.isSelected == newItem.isSelected
            }
        }
    }

Solution

위에서 적어놨듯이 Adapter내에서 아무리 Item을 건드려봤자 Adapter에 SubmitData되는 요소에는 적용되지 않는다.

즉 새로운 페이징 데이터가 들어오면 State가 사라지고 선택된 상태라는 것을 ViewModel 이나 Repository에서 설정함을 알 수 있다.

다음은 Paging3 라이브러리의 구조이다.

Repository -> ViewModel -> UI 순으로 PagingData가 전달되며 중간에 PagingSource로 변환하는 과정이 필요하다.

변환되는 Data class는 다음과 같다.

data class GalleryPhoto(
    val id: Long,
    val uri: Uri,
    val filepath: String,
    val name: String,
    val date: String,
    val size: Int,
    var isSelected: Boolean = false
)

여기서 PagingData는 비저장 스트림이므로 상태를 유지하지 않는다.
PagingData is just a stateless stream of incremental load events, so it does not hold this kind of state.

  • 방법 1
    PagingData로 변환되기 전에 ViewModel에서 전달한 SelectedImgList와 비교하여 isSelected을 바꾸어 주어야 한다

  • 방법 2
    PagingData가 Collect될 때 비교로직을 거쳐서 isSelected를 바꾸어주기

굳이 이렇게까지 복잡하게 진행해야하는지 싶었다.

Adapter내에서 선택된 ImgList를 선언하고, Adapter 내부에서 선택된 이미지를 필터링해주면 되지 않을까? 라는 생각이 들었다.

selectedImgList 선언

val selectedImgList = mutableListOf<GalleryPhoto>()
holder.itemView.setOnClickListener { view ->
	if (selectedImgList.contains(galleryPhoto)) {
           selectedImgList.remove(galleryPhoto)
    } else {
           selectedImgList.add(galleryPhoto)
    }
}

분기 렌더링

if (selectedImgList.contains(galleryPhoto)) {
	...
} else {
    ...
}

올바르게 작동은 하지만, 내가 구성한 이 구조가 MVVM의 패턴에 알맞는가? 라는 생각이 들었다.

비즈니스 로직은 ViewModel에서 처리되어야 한다고 생각하기 때문에 Delegates.observable()을 사용하여 ViewModel에 이를 전달하여 ViewModel에서 로직처리를 담당하게 하였다.


  • 변경된 소스

Delegates.observable()은 프로퍼티가 변화할 때만 작동함으로 var 및 ImutableList로 선언하고 property를 변화시키는 것으로 바꾸었다.

  • Delegates.observable() only observes changes to the variable, not to the object stored in that variable.
var selectedImgList: List<GalleryPhoto> by Delegates.observable(listOf()) { _, _, new ->
        handleChangeSelectedPhoto(new)
    }

var을 사용하였음으로 프로퍼티 자체를 바꾸는 소스로 변형

holder.itemView.setOnClickListener { view ->
	selectedImgList = if (selectedImgList.contains(galleryPhoto)) {
    	val temp = selectedImgList.toMutableList()
    	temp.remove(galleryPhoto)
    	temp.toList()
	} else {
    	val temp = selectedImgList.toMutableList()
    	temp.add(galleryPhoto)
    	temp.toList()
	}
}

초기 갤러리 페이징 어뎁터를 선언할 때 콜백함수를 선언하여 이를 ViewModel과 엮어주었다.

// Adapter
class GalleryPhotoListPagingAdapter(
    private val handleChangeSelectedPhoto: (List<GalleryPhoto>) -> Unit
)

// In Fragment
private val galleryPhotoListPagingAdapter = GalleryPhotoListPagingAdapter {
       galleryViewModel.handleChangeSelectedPhotos(it)
    }

이제 ViewModel에서 선택된 이미지에 대한 로직처리가 가능하다!

// In ViewModel
fun handleChangeSelectedPhotos(galleryPhoto: List<GalleryPhoto>) = viewModelScope.launch {
        Log.d(TAG, galleryPhoto.toString())
    }

profile
강단있는 개발자가 되기위하여

0개의 댓글