본문 바로가기
Kotlin

[Paging] Paging 사용법

by 명훈스토리 2023. 2. 5.
SMALL

1. 데이터 소스 정의

 페이지로 나누기를 구현할 때 다음 조건을 충족하는지 확인해야 한다.

- UI의 데이터 요청을 올바르게 처리하여 동일한 쿼리에 여러 요청이 동시에 트리거되지 않도록 한다.

- 관리 가능한 양의 가져온 데이터를 메모리에 유지한다.

- 이미 가져온 데이터를 보완하기 위해 추가 데이터를 가져오라는 요청을 트리거한다.

 

PagingSource를 사용하면 이 작업을 모두 실행할 수 있다. PagingSource는 데이터를 가져오는 방법을 지정하여 데이터 소스를 정의한다. 그러면 PagingData 객체는 사용자가 RecyclerView에서 스크롤할 때 생성되는 힌트가 로드되면 PagingSource에서 데이터를 가져온다.

 

data class Article(
    val id: Int,
    val title: String,
    val description: String,
    val created: LocalDateTime
)

 

PagingSource를 빌드하려면 다음 항목을 정의해야 한다.

- 페이징 키의 유형 : 추가 데이터를 요청하는데 사용하는 페이지 쿼리 유형의 정의이다. 여기서는 특정 기사 ID 앞이나 뒤에 기사를 가져온다.

- 로드된 데이터의 유형 : 각 페이지가 기사 List를 반환하므로 유형은 Article이다.

- 데이터를 가져오는 위치 : 일반적으로 데이터베이스나 네트워크 리소스, 페이지로 나눈 데이터의 다른 소스이다.

 

data 패키지에서 ArticlePagingSource.kt 파일에 PagingSource를 구현한다.

class ArticlePagingSource: PagingSource<Int, Article>() {
    
    override fun getRefreshKey(state: PagingState<Int, Article>): Int? {
        TODO("Not yet implemented")
    }

    override suspend fun load(params: LoadParams<Int>): LoadResult<Int, Article> {
        TODO("Not yet implemented")
    }

}

 

PagingSource에서는 두 가지 함수(load() 및 getRefreshKey())를 구현해야 한다.

 

사용자가 스크롤할 때 표시할 더 많은 데이터를 비동기식으로 가져오기 위해 Paging 라이브러리에서 load() 함수를 호출한다. LoadParams 객체에는 다음 항목을 포함하여 로드 작업과 관련된 정보가 저장된다.

- 로드할 페이지의 키 : load()가 처음 호출되는 경우 LoadParams.key는 null이다. 여기서는 초기 페이지 키를 정의해야 한다. 이 프로젝트에서는 기사 ID를 키로 사용한다. 초기 페이지 키의 ArticlePagingSource 파일 상단에 STARTING_KEY 상수 0도 추가한다.

로드 크기 : 로드 요청된 항목의 수이다.

 

load() 함수는 LoadResult를 반환한다. LoadResult는 다음 유형 중 하나일 수 있다.

- LoadResult.Page : 로드에 성공한 경우

- LoadResult.Error : 오류가 발생한 경우

- LoadResult.Invalid : PagingSource가 더 이상 결과의 무결성을 보장할 수 없으므로 무효화되어야 하는 경우

 

LoadResult.Page에는 다음과 같은 세 가지 필수 인수가 있다.

- data : 가져온 항목의 List이다.

- prevKey : 현재 페이지 앞에 항목을 가져와야 하는 경우 load() 메소드에서 사용하는 키이다.

- nextKey : 현재 페이지 뒤에 항목을 가져와야 하는 경우 load() 메소드에서 사용하는 키이다.

 

다음과 같은 선택적 인수 2개도 있다.

- itemsBefore : 로드된 데이터 앞에 표시할 자리 표시자의 수이다.

- itemsAfter : 로드된 데이터 뒤에 표시할 자리 표시자의 수이다.

 

로드 키는 Article.id 필드이다. 이를 키로 사용할 수 있는 이유는 기사마다 Article ID가 1씩 증가하기 때문이다. 즉, 기사 ID는 연속적으로 일정하게 증가하는 정수이다.

 

상응하는 방향으로 로드할 데이터가 더 이상 없는 경우 nextKey 또는 prevKey는 null이다. 여기서 prevKey의 경우는 다음과 같다.

- startKey가 STARTING_KEY와 같은 경우 null이 반환된다. 이 키 앞에 항목을 더 로드할 수 없기 때문이다.

- 그 외의 경우에는 목록의 첫 번째 항목을 가져와 앞에 LoadParams.loadSize를 로드하여 STARTING_KEY보다 작은 키가 반환되지 않도록 합니다. 이렇게 하려면 ensureValidKey() 메소드를 정의한다.

 

페이징 키가 유효한지 확인하는 다음 함수를 추가한다.

 

/**
 * Makes sure the paging key is never less than [STARTING_KEY]
 */
private fun ensureValidKey(key: Int) = max(STARTING_KEY, key)

 

nextKey의 경우는 다음과 같다.

-  무한 항목 로드를 지원하므로 range.last + 1을 전달한다.

 

또 각 기사에는 created 필드가 있으므로 이에 관한 값도 생성해야 한다. 파일 상단에 다음 줄을 추가한다.

private val firstArticleCreatedTime = LocalDateTime.now()

모든 코드를 올바르게 작성했으므로 이제 load() 함수를 구현할 수 있다.

 

override suspend fun load(params: LoadParams<Int>): LoadResult<Int, Article> {
    // Start paging with the STARTING_KEY if this is the first load
    val start = params.key ?: STARTING_KEY
    // Load as many times as hinted by params.loadSize
    val range = start.until(start + params.loadSize)

    return LoadResult.Page(
        data = range.map { number ->
            Article(
                // Generate consecutive increasing numbers as the article id
                id = number,
                title = "Ariticle $number",
                description = "This describes article $number",
                created = firstArticleCreatedTime.minusDays(number.toLong())
            )
        },

        // Make sure we don't try to load items behind the STARTING_KEY
        prevKey = when (start) {
            STARTING_KEY -> null
            else -> ensureValidKey(key = range.first - params.loadSize)
        },
        nextKey = range.last + 1
    )
}

 

다음으로 getRefreshKey()를 구현해야 한다. 이 메소드는 Paging 라이브러리가 UI 관련 항목을 새로고침해야 할 때 호출된다. 지원 PagingSource의 데이터가 변경되었기 때문이다. PagingSource의 기본 데이터가 변경되었으며 UI에서 업데이트해야 하는 이 상황을 무효화라고 한다. 무효화되면 Paging 라이브러리가 데이터를 새로고침할 새 PagingSource를 만들고 새 PagingData를 내보내 UI에 알린다.

 

새 PagingSource에서 로드할 때는 사용자가 새로고침 후 목록에서 현재 위치를 잃지 않도록 새 PagingSource가 로드를 시작해야 하는 키를 제공하기 위해 getRefreshKey()가 호출된다.

 

Paging 라이브러리에서 무효화가 발생하는 이유는 다음 두 가지 중 하나이다.

- PagingAdapter에서 refresh()를 호출했다.

- PagingSource에서 invalidate()를 호출했다.

 

반환된 키(여기서는 Int)는 LoadParams 인수를 통해 PagingSource의 다음 load() 메소드 호출에 전달된다. 무효화 후 항목이 이동하지 않도록 하려면 반횐된 키가 화면을 채울만큼 충분한 항목을 로드하도록 해야 한다. 이렇게 하면 새 항목 집합에 무효화된 데이터에 있던 항목이 포함될 가능성이 커지므로 현재 스크롤 위치를 유지하는데 도움된다.

// The refresh key is used for the initial load of the next PagingSource, after invalidation
override fun getRefreshKey(state: PagingState<Int, Article>): Int? {
    // In our case we grab the item closest to the anchor position
    // then return its id - (state.config.pageSize / 2) as a buffer
    val anchorPosition = state.anchorPosition ?: return null
    val article = state.closestItemToPosition((anchorPosition)) ?: return null
    return ensureValidKey(key = article.id - (state.config.pageSize / 2))
}

위 스니펫에서는 PagingState.anchorPosition을 사용한다. UI가 PagingData에서 항목을 읽으려고 하면 특정 index에서 읽으려고 한다. 데이터를 읽은 경우 이 데이터가 UI에 표시된다. 하지만 데이터가 없으면 Paging 라이브러리는 실패한 읽기 요청을 처리하기 위해 데이터를 가져와야 한다는 것을 인식한다. 읽을 때 데이터를 성공적으로 가져온 마지막 index는 anchorPosition이다.

 

새로고침할 때는 anchorPosition에 가장 가까운 Article 키를 가져와 로드 키로 사용한다. 이렇게 하면 새 PagingSource에서 로드를 다시 시작할 때 가져온 항목 집합에 이미 로드된 항목이 포함되므로 원활하고 일관된 사용자 환경이 보장된다.

LIST

'Kotlin' 카테고리의 다른 글

[Paging] ViewModel에서 PagingData 요청 및 캐시  (0) 2023.02.20
[Paging] UI용 PagingData 생성  (0) 2023.02.07
[Paging] Paging이란?  (0) 2023.02.03
[ViewModel] ViewModel이란?  (0) 2023.02.03
[LifeCycles] LifeCycles란?  (0) 2023.02.03

댓글