본문 바로가기

Android + Kotlin

[Android + Kotlin] PagingDataAdapter에서 "좋아요"를 해보자. (payload update)

반응형

예전에 만들었던 PagingDataAdapter를 이용한 리스트 프로젝트에 아이템 업데이트 기능을 구현해보기로 하겠습니다.

 

예제로 "좋아요" 기능을 구현합니다.

 

이전에 작업했던 게시물은 아래 링크를 확인해주세요.

 

Paging 3.0 + MVVM + Flow를 이용하여 리스트 구현하기

앱을 개발하면 빠지지않고 사용하는 페이징 리스트를 구현해 보도록 하겠습니다. 자동으로 페이징을 해주는 Paging 3.0 라이브러리를 이용하여 MVVM 모델과 Flow를 사용하여 연동할 예정입니다. 지

heeeju4lov.tistory.com

 


 

리스트의 아이템 업데이트를 위하여 payload 를 이용합니다. 

 

/**
 * Notify any registered observers that the item at <code>position</code> has changed with
 * an optional payload object.
 *
 * <p>This is an item change event, not a structural change event. It indicates that any
 * reflection of the data at <code>position</code> is out of date and should be updated.
 * The item at <code>position</code> retains the same identity.
 * </p>
 *
 * <p>
 * Client can optionally pass a payload for partial change. These payloads will be merged
 * and may be passed to adapter's {@link #onBindViewHolder(ViewHolder, int, List)} if the
 * item is already represented by a ViewHolder and it will be rebound to the same
 * ViewHolder. A notifyItemRangeChanged() with null payload will clear all existing
 * payloads on that item and prevent future payload until
 * {@link #onBindViewHolder(ViewHolder, int, List)} is called. Adapter should not assume
 * that the payload will always be passed to onBindViewHolder(), e.g. when the view is not
 * attached, the payload will be simply dropped.
 *
 * @param position Position of the item that has changed
 * @param payload  Optional parameter, use null to identify a "full" update
 * @see #notifyItemRangeChanged(int, int)
 */
public final void notifyItemChanged(int position, @Nullable Object payload) {
    mObservable.notifyItemRangeChanged(position, 1, payload);
}

 

우리가 리스트 업데이트 시에 호출하는 notifyItemChanged() 함수의 2번째 파라메터로 payload를 넘겨주게 되고, adapter의 onBindViewHolder() 함수가 호출되어 아이템을 업데이트하는 구조입니다.

 

override fun onBindViewHolder(
    holder: ImageViewHolder,
    position: Int,
    payloads: MutableList<Any>
)

 

그럼, 구현을 시작하겠습니다.

 


 

가장 먼저 해야할 것은 인터넷 사이트를 뒤져서 좋아요 이미지 아이콘을 찾는 일입니다.

여기저기 기웃거려서 일반 상태의 좋아요 / 선택된 상태의 좋아요 아이콘 2개를 찾았습니다.

벡터이미지(SVG) 로 찾았고, 프로젝트에 추가해줍니다.(아래 게시물을 보시면 추가 방법에 대해 설명해 놓았습니다.)

 

[Flutter] Flutter에서 SVG 이미지 사용하기

SVG: Scalable Vector Graphics 는 앱개발에서 주로 사용되는 이미지 파일 형식입니다. 기존에 bitmap, jpg, png 파일에 비해 여러가지 장점을 가지고 있습니다. 특히나 파일 사이즈가 직고, 이미지 크기 조

heeeju4lov.tistory.com

 


다음으로 item data class(ImageInfo.kt)에 좋아요 여부를 저장하는 변수를 추가합니다.

 

data class ImageInfo (
	...
    var isLike: Boolean? = false
)

 

그리고, 위의 값으로 좋아요 icon이 변경되도록, item layout(list_item_image.xml)에 좋아요 버튼을 추가합니다.

 

<androidx.appcompat.widget.AppCompatImageButton
    android:id="@+id/ibtn_like"
    android:layout_width="24dp"
    android:layout_height="24dp"
    app:layout_constraintStart_toStartOf="@id/tv_author"
    app:layout_constraintBottom_toBottomOf="@id/thumbnail"
    android:padding="0dp"
    android:src="@{item.isLike ? @drawable/ic_like_sel : @drawable/ic_like}"
    android:scaleType="centerInside"
    android:background="@null" />

 

위의 android:src 에 item.isLike 값에 따라서 좋아요 아이콘이 선택적으로 표시됩니다.

 


 

위에서 추가한  ibtn_like 위젯에 이벤트 연동 작업을 진행하겠습니다.

adapter(LoremPicsumListAdapter.kt)에서 클릭이벤트 인터페이스에 좋아요 클릭에 대한 인터페이스를 추가합니다.

 

 

interface OnImageItemClickListener<T> {
	...
    fun onLikeClicked(item: T?)
}

 

다음으로 ViewHolder에서 위젯에 클릭리스트너와 연동을 시켜줍니다.

 

inner class ImageViewHolder(private val binding: ListItemImageBinding):
    RecyclerView.ViewHolder(binding.root) {

        init {
        	...
            binding.ibtnLike.setOnClickListener {
                listener?.onLikeClicked(binding.item)
            }
        }
        
        ...
}

 

다음으로 adapter를 생성한 Activity(LoremPicsumListActivity.kt)에 onLikeClicked 를 구현해줍니다.

 

override fun onLikeClicked(item: ImageInfo?) {
}

 


 

여기까지해서 UI적인 부분은 어느정도 완료 했습니다.

이제부터는 좋아요 클릭 이벤트를 받아서 서버로 좋아요 값을 보내고, 응답을 받아서 리스트의 좋아요 아이콘을 업데이트 하는 것을 해보겠습니다.

 

ViewModel(LoremPicsumViewModel.kt)에 좋아요 데이터를 서버로 보내는 함수를 만듧니다.

 

private val _likePayload: MutableLiveData<Bundle> = MutableLiveData()
val likePayload: LiveData<Bundle> get() = _likePayload
    
fun postLike(itemId:String, isLike: Boolean) {
    viewModelScope.launch {
        // 실제는 서버의 좋아요 데이터를 업데이트해야 하지만, 이건 샘플 코드기때문에 delay로 서버와 갔다 온것처럼 함.
        delay(100)

        _likePayload.postValue(Bundle().apply {
            putString("itemId", itemId)
            putBoolean("isLike", !isLike)
        })
    }
}

 

delay(100) 대신에 실제로 서버로 좋아요 데이터를 전송하고 응답받는 코드를 넣으시면 됩니다.

그리고, 응답을 받는 곳에 _likepayload.postValue() 부분을 넣으시면 되고요.

 

함수의 파라메터인 itemId 는 추후 adater에서 좋아요를 한 아이템의 position을 찾을때 사용합니다.

isLike 역시 어떤 값이 설정되었는 응답 데이터 될거구요.

 

그리고, 응답 LiveData는 data class도 되고, 저처럼 Bundle를 사용해도 무방합니다.

 


 

위의 Activity(LoremPicsumListActivity.kt)에서 구현했던 onLikeClicked() 함수에 postLike를 호출합니다.

 

override fun onLikeClicked(item: ImageInfo?) {
    item?.id?.let {
        viewModel.postLike(it, item.isLike ?: false)
    }
}

 

그리고, LiveData응답을 받을수 있는 observe를 등록합니다.

 

viewModel.likePayload.observe(this) {
    imageListAdapter.updateLike(it)
}

 

위의 코드에는 adapter에 updateLike() 함수를 호출하고 있는데, (현재 게시물에서는 미구현 상태이니) 구현을 하러 가보겠습니다.

 


 

adapter(LoremPicsumListAdapter.kt)에 itemId로 position을 찾는 함수를 구현합니다.

 

fun updateLike(payload: Bundle?) {
    payload?.getString("itemId")?.let { id ->
        snapshot().items.indexOfFirst { item ->
            item.id == id
        }.also { position ->
            if (position >= 0) {
                notifyItemChanged(position, payload)
            }
        }
    }
}

 

itemId로 position을 찾았다면 notifyItemChanged()를 실행하여, 해당 position에 payload가 전달이 되도록 실행합니다.

 


 

위의 notifyItemChanged()를 실행하면, onBindViewHolder() 함수가 호출됩니다.

 

override fun onBindViewHolder(
    holder: ImageViewHolder,
    position: Int,
    payloads: MutableList<Any>
) {
    if(payloads.isEmpty()) super.onBindViewHolder(holder, position, payloads)

    payloads.forEach { payload ->
        if(payload is Bundle) {
            when {
                payload.containsKey("isLike") -> {
                    getItem(position)?.let {
                        it.isLike = payload.getBoolean("isLike")
                        holder.onBind(it)
                    }
                }
            }
        }
    }
}

 

payloads 파라메터는 arrray로 전달되기 때문에 각각의 데이터를 확인하여 처리해주면 됩니다.

좋아요 기능은 Bundle 데이터에 "isLike"가 있는지 여부로 판단하여 데이터를 업데이트 합니다.

(여러 데이터가 업데이트 되었다면, 다른 방법을 사용하시기 바랍니다.)

 

getItem(position)으로 리스트의 데이터를 찾아서 isLike를 응답 데이터로 업데이트 해주고 onBind()를 통해서 아이템의 UI가 업데이트 되도록 해줍니다.

 


 

여기까지 하셨다면 구현이 완료된 것입니다.

이제 실행해보면 아래와 같이 동작하는걸 보실수 있으십니다.

 

 

 

전체 소스 코드는 아래 Github 링크를 참고해주세요.

 

 

GitHub - rcbuilders/RemindSampleApp: https://heeeju4lov.tistory.com/ 블로그에서 Android + Kotlin 강좌에서 사용함.

https://heeeju4lov.tistory.com/ 블로그에서 Android + Kotlin 강좌에서 사용함. - GitHub - rcbuilders/RemindSampleApp: https://heeeju4lov.tistory.com/ 블로그에서 Android + Kotlin 강좌에서 사용함.

github.com

 

반응형