일 | 월 | 화 | 수 | 목 | 금 | 토 |
---|---|---|---|---|---|---|
1 | ||||||
2 | 3 | 4 | 5 | 6 | 7 | 8 |
9 | 10 | 11 | 12 | 13 | 14 | 15 |
16 | 17 | 18 | 19 | 20 | 21 | 22 |
23 | 24 | 25 | 26 | 27 | 28 |
- 백준 5582
- flow buffering
- 1806 투포인터
- 6588 파이썬
- 5582 파이썬
- 5582 DP
- 백준 10819
- 코루틴 플로우
- 안드로이드 hilt
- 1644 파이썬
- 투포인터 알고리즘
- Jetpack Room
- 10819 파이썬
- 2096 파이썬
- java
- 1753 다익스트라
- 1806 파이썬
- android hilt
- 1003 파이썬
- git local remote
- 백준 1644
- 자료구조
- Android mvp
- 1806 백준
- 자바
- 백준 2096
- Android Room
- Coroutine Flow
- 1753 파이썬
- 이진 탐색
- Today
- Total
Gemstone's Devlog
Paging 3.0 라이브러리 정리 본문
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 : RecyclerView에 PagingData를 표시하는 RecyclerView.Adapter 서브클래스. PagingDataAdapter는 팩토리 메서드를 사용하여 Kotlin Flow나 LiveData, 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.
- 데이터를 가져오는 위치: 일반적으로 데이터베이스나 네트워크 리소스, 페이지로 나눈 데이터의 다른 소스. 하지만 여기에서는 로컬에서 생성된 데이터를 사용함.
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 |