Gemstone's Devlog

Paging 3.0 라이브러리 정리 본문

Kotlin (Android)

Paging 3.0 라이브러리 정리

Gemstone 2022. 6. 14. 23:28

 

https://developer.android.com/topic/libraries/architecture/paging/v3-overview?hl=ko

 

페이징 라이브러리 개요  |  Android 개발자  |  Android Developers

페이징 라이브러리 개요   Android Jetpack의 구성요소 페이징 라이브러리를 사용하면 로컬 저장소에서나 네트워크를 통해 대규모 데이터 세트의 데이터 페이지를 로드하고 표시할 수 있습니다.

developer.android.com

 

Paging 라이브러리 핵심 구성요소

  • PagingSource : 특정 페이지 쿼리의 데이터 청크를 로드하는 기본 클래스. 데이터 레이어의 일부이며 일반적으로 DataSource클래스에서 노출되고 이후에 ViewModel에서 사용하기 위해 Repository에 의해 노출됨.
  • PagingConfig : 페이징 동작을 결정하는 매개변수를 정의하는 클래스. 여기에는 페이지 크기, 자리표시자의 사용 설정 여부 등이 포함됨
  • Pager : PagingData스트림을 생성하는 클래스.PagingSource에 따라 다르게 실행되며 ViewModel에서 만들어야 함.
  • PagingData :  페이지로 나눈 데이터의 컨테이너. 데이터를 새로고침할 때마다자체 PagingSource로 지원되는 상응하는 PagingData 내보내기가 별도로 생성됨.
  • PagingDataAdapter : RecyclerViewPagingData를 표시하는 RecyclerView.Adapter 서브클래스. PagingDataAdapter는 팩토리 메서드를 사용하여 Kotlin FlowLiveData, RxJavaFlowable, RxJavaObservable 또는 정적 목록에도 연결 가능!

 

앱 만들면서 Paging 3.0 적용해보기

 

앱 -> 정적인 기사 목록을 표시해주고, 각 기사에는 제목과 설명, 작성된 날짜가 있음. 

정적 목록은 항목이 적을 때는 유용하지만 데이터 세트가 커질수록 확장성이 떨어지므로, Paging 라이브러리를 사용하여

페이지로 나누기를 구현해 적용할 예정!

 

앱 아키텍처 가이드에서 권장하는 아키텍처

데이터 레이어

  • ArticleRepository : 기사 목록을 제공하고 기사를 메모리에 저장
  • Article : 데이터 레이어에서 가져온 정보의 표현인 데이터 모델을 나타내는 클래스

UI 레이어

  • Activity, RecyclerView.Adapter, RecyclerView.ViewHolder : UI에 목록을 표시하는 클래스
  • ViewModel : UI가 표시해야 하는 상태를 생성하는 상태 홀더

Repository는 Flow의 모든 기사를 articleStream 필드로 노출하고, 그러면 UI 레이어의 ArticleViewModel에서 읽은 후 

state 필드 StateFlow로 ArticleActivity의 UI에서 사용할 수 있도록 준비함.

 

Repository에서 기사를 Flow로 노출하면 시간이 지남에 따라 기사가 변경될 때 제공된 기사를 Repository에서 업데이트할 수 있다. 

예를 들어 기사의 제목이 변경되면 articleStream 수집기에 이러한 변경사항이 쉽게 전달될 수 있다.

ViewModel에서 UI 상태에 StateFlow를 사용하면 UI 상태 수집을 중지하더라도 (예 : 구성 변경 중에 Activity가 다시 생성될 때) 다시 수집을 시작하는 순간에 바로 중단 지점부터 시작 가능하다.

 

Repository의 현재 articleStream은 현재 날짜의 뉴스만 표시한다. 

일부 사용자는 이것으로 충분할 수 있지만 제공되는 모든 현재 날짜의 기사를 스크롤했을 때 오래된 기사를 확인하고자 하는 사용자도 있을 수 있다. 이러한 기대로 인해 기사 표시는 페이지로 나누기의 적합한 후보이다.

 

  • ViewModel은 메모리에 로드된 모든 항목을 items StateFlow에 유지함. 이는 데이터 세트가 너무 커지면 성능에 영향을 줄 수 있다는 의미이기에 매우 중요한 문제!!
  • 변경되었을 때 목록에서 기사를 하나 이상 업데이트하는 작업은 기사 목록이 커질수록 비용이 더 많이 든다...

 

. PagingSource는 증분 청크로 데이터를 가져오는 방법을 지정하여 데이터 소스를 정의하고, 그러면 PagingData 객체는 사용자가 RecyclerView에서 스크롤할 때 생성되는 힌트가 로드되면 PagingSource에서 데이터를 가져오는 방식...

 

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

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

  • 페이징 키의 유형: 추가 데이터를 요청하는 데 사용하는 페이지 쿼리 유형의 정의. 여기서는 특정 기사 ID 앞이나 뒤에 기사를 가져옴. ID가 정렬되고 증가한다고 보장되기 때문!
  • 로드된 데이터의 유형: 각 페이지가 기사 List를 반환하므로 유형은 Article.
  • 데이터를 가져오는 위치: 일반적으로 데이터베이스나 네트워크 리소스, 페이지로 나눈 데이터의 다른 소스. 하지만 여기에서는 로컬에서 생성된 데이터를 사용함.

data 패키지에서 ArticlePagingSource.kt 라는 새 파일에 PagingSource 구현을 만들어보았다.

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() 메서드에서 사용하는 키.

또한 다음과 같은 선택적 인수 두 개도 있다.

  • itemsBefore: 로드된 데이터 앞에 표시할 자리표시자의 수.
  • itemsAfter: 로드된 데이터 뒤에 표시할 자리표시자의 수.

로드 키는 Article.id 필드이고, 이를 키로 사용할 수 있는 이유는 기사마다 Article ID가 1씩 증가하기 때문!!!

(즉, 기사 ID는 연속적으로 일정하게 증가하는 정수)

 

상응하는 방향으로 로드할 데이터가 더 이상 없는 경우 nextKey 또는 prevKey null이다.

 여기서 prevKey의 경우는 다음과 같다.

  • startKey STARTING_KEY와 같은 경우 null이 반환되는데 왜냐하면 이 키 앞에 항목을 더 로드할 수 없기 때문!
  • 그 외의 경우에는 목록의 첫 번째 항목을 가져와 앞에 LoadParams.loadSize를 로드하여 STARTING_KEY보다 작은 키가 반환되지 않도록 해야한다. 이렇게 하려면 ensureValidKey() 메서드를 정의해야함.

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

class ArticlePagingSource : PagingSource<Int, Article>() {
   ...
   /**
     * 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()

class ArticlePagingSource : PagingSource<Int, Article>() {
   ...
}

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

import kotlin.math.max
...

private val firstArticleCreatedTime = LocalDateTime.now()

class ArticlePagingSource : PagingSource<Int, Article>() {
   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 items 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 = "Article $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
        )
    }

    ...
}

 

load() 함수를 구현했으니 이제 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))
    }

.UI가 PagingData에서 항목을 읽으려고 하면 특정 색인에서 읽으려고 한다.

데이터를 읽은 경우 이 데이터가 UI에 표시되지만,  데이터가 없으면 Paging 라이브러리는 실패한 읽기 요청을 처리하기 위해 데이터를 가져와야 한다는 것을 인식한다.

읽을 때 데이터를 성공적으로 가져온 마지막 색인은 anchorPosition이다.

 

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

 


 

이제 구현할 내용에서는 ArticleRepository Flow<List<Article>>을 사용하여 ViewModel에 로드된 데이터를 노출한다. 그러면 ViewModel은 UI에 노출하기 위해 stateIn 연산자를 사용하여 항상 사용할 수 있는 데이터 상태를 유지한다.

 

Paging 라이브러리를 사용하면 ViewModel Flow<PagingData<Article>>을 대신 노출할 수 있다.

PagingData는 로드된 데이터를 래핑하고 Paging 라이브러리가 추가 데이터를 가져올 시기를 결정하는 데 도움을 주는 유형이며 동일한 페이지를 두 번 요청하지 않도록 한다.

 

PagingData를 구성하기 위해 PagingData를 앱의 다른 레이어에 전달하는 데 사용할 API에 따라 Pager 클래스의 여러 빌더 메서드 중 하나를 사용한다.

 

앱에서 이미 Flow를 사용하고 있으므로 이 방법을 계속 사용하겠다.

 Flow<List<Article>> 대신 Flow<PagingData<Article>>를 사용!!

 

사용하는 PagingData 빌더에 관계없이 다음 매개변수를 전달해야 한다.

 

  • PagingConfig. 이 클래스는 로드 대기 시간, 초기 로드의 크기 요청 등 PagingSource에서 콘텐츠를 로드하는 방법에 관한 옵션을 설정한다. 정의해야 하는 유일한 필수 매개변수는 각 페이지에 로드해야 하는 항목 수를 가리키는 페이지 크기이다. 기본적으로 Paging은 로드하는 모든 페이지를 메모리에 유지한다. 사용자가 스크롤할 때 메모리를 낭비하지 않으려면 PagingConfig에서 maxSize 매개변수를 설정하면 된다. 기본적으로 Paging은 로드되지 않은 항목을 집계할 수 있고 enablePlaceholders 구성 플래그가 true인 경우 아직 로드되지 않은 콘텐츠의 자리표시자로 null 항목을 반환한다. 이렇게 하면 어댑터에 자리표시자 뷰를 표시할 수 있다.
  • PagingSource를 만드는 방법을 정의하는 함수. 여기서는 ArticlePagingSource을 만들므로 Paging 라이브러리에 이 작업을 실행하는 방법을 알려주는 함수가 필요하다.

ArticleRepository 를 수정해보겠다.

ArticleRepository 업데이트

  • articlesStream 필드를 삭제한다.
  • 방금 만든 ArticlePagingSource를 반환하는 articlePagingSource()라는 메서드를 추가한다.
class ArticleRepository) {

    fun articlePagingSource() = ArticlePagingSource()
}

ArticleRepository 정리

Paging 라이브러리는 다양한 작업을 실행한다.

  • 메모리 내 캐시를 처리.
  • 사용자가 목록의 끝에 가까워지면 데이터를 요청.

즉, articlePagingSource()를 제외한 ArticleRepository의 모든 값이 삭제될 수 있다.

이제 ArticleRepository 파일이 다음과 같이 표시된다.

package com.example.android.codelabs.paging.data

import androidx.paging.PagingSource

class ArticleRepository {
    fun articlePagingSource() = ArticlePagingSource()
}

이제 ArticleViewModel에 컴파일 오류가 발생하는데, 어떻게 변경해야 하는지 알아보겠다.

 

 

'Kotlin (Android)' 카테고리의 다른 글

Jetpack Navigation 공부  (0) 2022.06.17
GSON 대신 Moshi?  (0) 2022.06.15
GDG Android I/O Extended Korea Android 2022 참가 후기  (0) 2022.06.11
Coroutine 정리  (0) 2022.06.07
[의존성 주입] Hilt 정리  (0) 2022.04.05