Android/Paging3

[Android] Paging3에서 아이템 삭제하기 + 로딩 처리

O_Gyong 2023. 1. 19.
 

[Android] Paging3, 스크롤 시 로딩 화면 추가하기

[Android] Paging3 + Room + Flow 사용하기 [Android] RecyclerView에서 페이징+삭제 처리하기 #2 (with Room) [Android] RecyclerView에서 페이징 처리하기 #1 RecyclerView에서 리스트를 스크롤하다가 어느 순간에 로딩 화면

ogyong.tistory.com

저번에 했던 스크롤 시 로딩 화면을 추가하는 것에 이어서

Paging3에서 아이템을 삭제하고, 삭제가 처리되는 동안 로딩 화면을 띄우는 것을 해보려고 한다.


Room 삭제 쿼리 추가

@Dao
interface SampleDao {

    ...
    
    /**
     * 아이템 삭제
     */
    @Query("DELETE FROM sample WHERE id = :id")
    fun itemDelete(id: Int)
}

삭제가 요청되면 id 값을 전달받아서 해당 아이템을 삭제하도록 쿼리를 추가한다.


삭제 결과를 LiveData로 받도록 ViewModel 수정

class MainViewModel: ViewModel() {
    ...

    /**
     * Room 삭제 호출
     */
    val itemDeleteObserve: MutableLiveData<Unit> = MutableLiveData()
    
    fun setItemDelete(id: Int) {
        viewModelScope.launch(Dispatchers.IO) {
            itemDeleteObserve.postValue(SampleDatabase.sampleDB!!.getSampleDao().itemDelete(id))
        }
    }
}

RecyclerView 아이템 레이아웃 수정

<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout
    xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:app="http://schemas.android.com/apk/res-auto"
    xmlns:tools="http://schemas.android.com/tools"
    android:layout_width="100dp"
    android:layout_height="wrap_content" >

    <TextView
        android:id="@+id/tv_item"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:textColor="@color/black"
        android:textSize="25dp"
        android:textStyle="bold"
        android:layout_marginTop="10dp"
        android:layout_marginStart="10dp"
        app:layout_constraintStart_toStartOf="parent"
        app:layout_constraintTop_toTopOf="parent"
        tools:text="1번" />

    <ImageView
        android:id="@+id/iv_item_remove"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:src="@drawable/icon_remove"
        android:layout_marginEnd="5dp"
        app:layout_constraintTop_toTopOf="@id/tv_item"
        app:layout_constraintEnd_toEndOf="parent"
        app:layout_constraintBottom_toBottomOf="@id/tv_item" />

    <View
        android:layout_width="match_parent"
        android:layout_height="2dp"
        android:background="#A3A3A3"
        android:layout_marginTop="3dp"
        app:layout_constraintTop_toBottomOf="@id/tv_item" />


</androidx.constraintlayout.widget.ConstraintLayout>

RecyclerView의 레이아웃에 닫기 버튼을 추가로 넣어준다.

이미지는 Vector Asset에서 구했다.


SampleAdapter에 커스텀 리스너 작성

class SampleAdapter: PagingDataAdapter<SampleData, SampleAdapter.SampleViewHolder>(ARTICLE_DIFF_CALLBACK) {
    interface CustomListenerInterface {
        fun removeListener(position: Int, sampleData: SampleData)
    }
    
    private var onRemoveListener: CustomListenerInterface? = null

    fun removeListener(pOnClick: CustomListenerInterface) {
        this.onRemoveListener = pOnClick
    }

    inner class SampleViewHolder(private val mBinding : ListItemMainBinding): RecyclerView.ViewHolder(mBinding.root) {
        fun bind(listData: SampleData) {
            mBinding.ivItemRemove.setOnClickListener {
                ...

                onRemoveListener?.removeListener(bindingAdapterPosition, listData)
            }
        }
    }

    ...
}

닫기 버튼을 눌렀을 때 Activity에서 처리가 가능하도록 커스텀 리스너를 만든다.


Activity에서 아이템 삭제 / UI 갱신 / 로딩 처리

mAdapter.removeListener(object : SampleAdapter.CustomListenerInterface {
    override fun removeListener(position: Int, sampleData: SampleData) {
        mViewModel.setItemDelete(sampleData.id)
    }
})

Activity에서 닫기 버튼을 클릭했을 때 ViewModel의 삭제 메서드를 호출한다.


/**
 * 아이템 삭제 이후 PagingDataAdapter의 refresh를 호출하여 UI 갱신
 */
mViewModel.itemDeleteObserve.observe(this) {
    mAdapter.refresh()
}

데이터가 삭제되면 LiveData의 observe를 이용해 PagingDataAdapter의 refresh 메서드를 호출한다.

 

refresh를 호출하게 되면 PagingData의 새로운 객체로 항목을 만들도록 동작이 수행된다.

이때 PagingSource의 getRefreshKey 메서드가 호출된다.

 

/**
 * 현재 목록을 대체할 새 데이터를 로드할 때 사용
 * - anchorPosition : 가장 최근에 액세스한 인덱스
 * - closestPageToPosition : anchorPosition을 토대로 가장 가까운 페이지를 다시 호출
 */
override fun getRefreshKey(state: PagingState<Int, SampleData>): Int? {
    println("getRefreshKey $state")
    return state.anchorPosition?.let { anchorPosition ->
        state.closestPageToPosition(anchorPosition)?.prevKey?.plus(1)
            ?: state.closestPageToPosition(anchorPosition)?.nextKey?.minus(1)
    }
}

getRefreshKey의 state 값에는 anchorPosition이 있는데, 이 값은 가장 최근에 액세스한 인덱스를 의미한다.

closesPageToPosition은 anchorPosition을 토대로 가장 가까운 페이지를 다시 호출한다.

(처음에 anchorPosition이 리스트의 인덱스인 줄 알았는데, 값을 비교하니 그건 아닌 것 같다.)


/**
 * PagingDataAdapter의 로드 상태를 전달받아 refresh가 로딩 상태이면 로딩 화면 띄우도록 처리
 */
lifecycleScope.launch {
    mAdapter.loadStateFlow.collect { loadState ->
        mBinding.pbMainLoading.isVisible = loadState.refresh is LoadState.Loading
    }
}

PagingDataAdapter의 loadStateFlow는 Adapter의 로드 상태를 전달 받는다.

PagingSource의 getRefreshKey 메서드가 호출되면 로드상태를 loadStateFlow로 받을 수 있다.

 

이것을 사용해서 Activity의  ProgressbBar의 Visible에 값을 부여한다.

 

위의 코드를 통해 앱이 켜지고 데이터를 처음 로드할 때, 데이터를 삭제할 때 로딩 화면이 뜰 것이다.

(아래는 Activity의 레이아웃)

<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout
    xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:app="http://schemas.android.com/apk/res-auto"
    xmlns:tools="http://schemas.android.com/tools"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    tools:context=".MainActivity">

    <androidx.recyclerview.widget.RecyclerView
        android:id="@+id/rv_main"
        android:layout_width="wrap_content"
        android:layout_height="350dp"
        app:layout_constraintBottom_toBottomOf="parent"
        app:layout_constraintEnd_toEndOf="parent"
        app:layout_constraintStart_toStartOf="parent"
        app:layout_constraintTop_toTopOf="parent"
        app:layoutManager="androidx.recyclerview.widget.LinearLayoutManager"
        tools:listitem="@layout/list_item_main"/>

    <ProgressBar
        android:id="@+id/pb_main_loading"
        android:layout_width="64dp"
        android:layout_height="64dp"
        app:layout_constraintBottom_toBottomOf="parent"
        app:layout_constraintEnd_toEndOf="parent"
        app:layout_constraintStart_toStartOf="parent"
        app:layout_constraintTop_toTopOf="parent" />

</androidx.constraintlayout.widget.ConstraintLayout>

2초의 delay 부여

전체 코드

댓글