오늘도 더 나은 코드를 작성하였습니까?

android paging3 basic codelab 정리1 본문

Android Jetpack Architecture/Paging3

android paging3 basic codelab 정리1

hik14 2022. 10. 10. 19:31

페이징을 해야하는 이유

- ViewModel은 메모리에 로드된 모든 항목을 items StateFlow에 저장한다, 그런데  데이터가 너무 커지면 성능에 영향을 줄 수 있다는 의미이기에 매우 중요한 문제입니다.

-   데이터가 변경되었을 때 List에서 하나 이상 업데이트하는 작업은 List가 클수록 커질수록 비용이 더 많이 생긴다.

 

Paging 라이브러리는 위 문제를 해결하는 동시에 앱에서 점진적으로 데이터를 가져오는(페이지로 나누기) 일관된 API를 제공

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

PagingSource

- 특정 페이지 쿼리의 데이터 청크를 로드하는 기본 클래스입니다.

- 데이터 레이어의 일부이며 일반적으로 DataSource 클래스에서 노출되고 이후에 ViewModel에서 사용하기 위해 Repository에 의해 노출됩니다.

 

PagingConfig

- 페이징 동작을 결정하는 매개변수를 정의하는 클래스입니다.

- 페이지 크기, 자리표시자의 사용 설정 여부 등이 포함됩니다.

 

Pager

- PagingData 스트림을 생성하는 클래스입니다. 

- PagingSource에 따라 다르게 실행되며 ViewModel에서 생성

 

PagingData 

- 페이지로 나눈 데이터의 컨테이너입니다.

- 데이터를 새로고침할 때마다 자체 PagingSource로 지원되는 상응하는 PagingData emit 별도로 생성됩니다.

 

PagingDataAdapter

- RecyclerView에 PagingData를 표시하는 RecyclerView.Adapter 서브클래스입니다.

- PagingDataAdapter는 팩토리 메서드를 사용하여 Kotlin Flow나 LiveData, RxJava Flowable, RxJava Observable 또는 정적 목록에도 연결할 수 있습니다. 

- PagingDataAdapter는 내부 PagingData 로드 이벤트를 수신 대기하고 페이지가 로드될 때 UI를 효율적으로 업데이트

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

  • UI의 데이터 요청을 올바르게 처리하여 동일한 쿼리에 여러 요청이 동시에 트리거되지 않아야 한다.
  • 관리 가능한 양의 가져온 데이터를 메모리에 유지합니다.
  • 이미 가져온 데이터를 보완하기 위해 추가 데이터를 가져오라는 요청을 트리거합니다

PagingSource추가적 데이터를 가져오는 방법을 지정하여 데이터 소스를 정의합니다.

PagingData 객체는 사용자가 RecyclerView에서 스크롤할 때 생성되는 힌트가 로드되면 PagingSource에서 데이터를 가져옵니다.

 

Paging key의 유형

- 추가 데이터를 요청하는 데 사용하는 페이지 쿼리 type의 정의입니다.

- 특정 항목의 ID 앞이나 뒤에 기사를 가져옵니다. ID가 정렬되고 증가한다고 보장된다면, 

 

load된 데이터의 유형

- 각 페이지가 반환하는 List<type> 유형

 

데이터를 가져오는 위치

- 일반적으로 데이터베이스 /  네트워크 리소스 / 미리 페이지로 나누어논 데이터

 

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

load() 함수 

 - 사용자가 스크롤할 때 표시할 더 많은 데이터를 비동기식으로 가져오기 위해 Paging 라이브러리에서 load() 함수를 호출한다.

 - LoadParams 객체에는 다음 항목을 포함하여 로드 작업과 관련된 정보가 저장

 

load할 페이지의 키(params.key)

load()가 처음 호출되는 경우 LoadParams.key는 null

- 여기서는 초기 페이지 키를 정의해야 합니다.

- 이 프로젝트에서는 항목 ID를 키로 사용합니다. 초기 페이지 키의 ArticlePagingSource 파일 상단에 STARTING_KEY 상수 0도 추가해 보겠습니다.

 

load size

- 로드 요청된 항목의 수입니다.

 

load() 함수의 반환값  LoadResult

 

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

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

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

 

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

  • data: 가져온 항목의 List입니다.
  • prevKey: 현재 페이지 앞에 항목을 가져와야 하는 경우 load() 메서드에서 사용하는 키입니다.
  • nextKey: 현재 페이지 뒤에 항목을 가져와야 하는 경우 load() 메서드에서 사용하는 키입니다

선택적 인수 두 개도 있습니다.

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

load key는 Article.id 필드입니다.

이를 키로 사용할 수 있는 이유는 기사마다 Article ID가 1씩 증가하기 때문입니다.

 

상응하는 방향(위/아래 스크롤)으로 로드할 데이터가 더 이상 없는 경우 nextKey(아래방향)또는 prevKey(윗 방향)는 null입니다.

 

prevKey

  • startKey가 STARTING_KEY와 같은 경우 null이 반환됩니다. 이 키 앞에 항목을 더 로드할 수 없기 때문입니다.
  • 그 외의 경우에는 목록의 첫 번째 항목을 가져와 앞에 LoadParams.loadSize를 로드하여 STARTING_KEY보다 작은 키가 반환되지 않도록 합니다.
  • ensureValidKey() 메서드를 정의 
rivate fun ensureValidKey(key: Int) = max(STARTING_KEY, key)

nextKey

  • 무한 항목 로드를 지원하므로 range.last + 1
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
        )
    }

    ...
}

getRefreshKey()

 

- Paging 라이브러리가 UI 관련 항목을 새로고침해야 할 때호출됩니다.

- PagingSource의 데이터가 변경되었기 때문입니다. 

- PagingSource기본 데이터가 변경되었으며 UI에서 업데이트해야 하는 이 상황을 무효화라고 합니다.

- 무효화되면 Paging 라이브러리가 데이터를 새로고침할 새 PagingSource를 만들고 새 PagingData를 내보내 UI에 알립니다. 

 

PagingSource에서 로드할 때는 사용자가 새로고침 후 목록에서 현재 위치를 잃지 않도록

새로운 PagingSource가 로드를 시작해야 하는 키를 제공하기 위해 getRefreshKey()가 호출

 

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

  • PagingAdapter에서 refresh()를 호출
  • PagingSource에서 invalidate()를 호출

return 값인 key(여기서는 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 key를 가져와 load key로 사용합니다.

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