정보를 점진적으로 가져오는 이 프로세스를 페이징이라고 하며 각 페이지는 가져올 데이터 청크에 상응합니다.
페이지를 요청하려면 페이징 되는 데이터 소스에는 필요한 정보를 정의하는 쿼리가 필요한 경우가 많습니다. 이 Paging 라이브러리는 앱에서 빠르고 효율적으로 페이징을 할 수 있도록 도와줍니다.
Paging 라이브러리의 핵심 구성요소는 다음과 같습니다.
- PagingSource: 특정 페이지 쿼리의 데이터 청크를 로드하는 기본 클래스입니다. 데이터 레이어의 일부이며 일반적으로 DataSource 클래스에서 노출되고 이후에 ViewModel에서 사용하기 위해 Repository에 의해 노출됩니다.
- PagingConfig: 페이징 동작을 결정하는 매개변수를 정의하는 클래스입니다. 여기에는 페이지 크기, 자리표시자의 사용 설정 여부 등이 포함됩니다.
- Pager: PagingData 스트림을 생성하는 클래스입니다. PagingSource에 따라 다르게 실행되며 ViewModel에서 만들어야 합니다.
- PagingData: 페이지로 나눈 데이터의 컨테이너입니다. 데이터를 새로고침할 때마다 자체 PagingSource로 지원되는 상응하는 PagingData 내보내기가 별도로 생성됩니다.
- PagingDataAdapter: RecyclerView에 PagingData를 표시하는 RecyclerView.Adapter 서브클래스입니다. PagingDataAdapter는 팩토리 메서드를 사용하여 Kotlin Flow나 LiveData, RxJava Flowable, RxJava Observable 또는 정적 목록에도 연결할 수 있습니다. PagingDataAdapter는 내부 PagingData 로드 이벤트를 수신 대기하고 페이지가 로드될 때 UI를 효율적으로 업데이트합니다.
이구성 요소들을 만들어보며 알아가 봅시다
페이징을 구현할 때는 다음 조건을 충족하는지 확인해야 합니다.
- UI의 데이터 요청을 올바르게 처리하여 동일한 쿼리에 여러 요청이 동시에 트리거 되지 않도록 합니다.
- 관리 가능한 양의 가져온 데이터를 메모리에 유지합니다.
- 이미 가져온 데이터를 보완하기 위해 추가 데이터를 가져오라는 요청을 트리거합니다.
저장소 레이어 : PagingSource
네트워크 또는 데이터베이스에서 페이징 데이터를 로드하는 추상 클래스입니다. 이를 구현하려면 페이지 key 타입을 정의해야 합니다. 데이터를 검색하는 방법을 정의하는 클래스입니다.
PagingSource를 빌드하려면 다음 항목을 정의해야 합니다.
class SamplePagingSource : PagingSource<Int, Sample>() {
override suspend fun load(params: LoadParams<Int>): LoadResult<Int, Sample> {
TODO("Not yet implemented")
}
override fun getRefreshKey(state: PagingState<Int, Sample>): Int? {
TODO("Not yet implemented")
}
}
위의 SamplePagingSource에서 보아야 할 점은 페이징 키와 로드될 데이터 오버라이드 해줘야 할 load 및 getRefreshKey입니다.
위에서 말했다시피 키는 데이터를 가져올 때 사용되는 키입니다. 예를 들어 정렬된 데이터에서 n번째부터의 데이터를 가져오는 RestfulApi가 있다고 해봅시다. 30개의 데이터를 가져온다고 할 때 다음 데이터를 가지고 오려면 30번 이후의 데이터를 가지고 와야겠죠 이 값이 키가 됩니다. 그렇다면 이 값은 해당 페이지를 가지고 올 수 있는 유형의 데이터로 정의해야 할것입니다. 그렇다면 여기서 Sample 로드될 데이터는 페이징을 해서 가져올 데이터를 의미하게 됩니다.
PagingSource에서는 두 가지 함수(load() 및 getRefreshKey())를 구현해줘야 합니다.
override suspend fun load(params: LoadParams<Int>): LoadResult<Int, Sample> {
TODO("Not yet implemented")
}
load 같은 경우 패이징을 하여 다음페이지를 가지고 올 때 호출되는 함수입니다. 말 그대로 다음데이터를 load 하는 거죠.
이 함수에서는 특이한 점이 3가지 있습니다. 중단(suspend) 함수라는 것과 LoadParams <T>의 형태를 가지는 매개변수를 가지며 LoadResult <T, F>라는 값을 반환을 해줘야 한다는 것입니다. 중단함수의 경우 이해가 갑니다. 이러한 중단함수들은 데이터를 가지고 올 때 자주사용이 됩니다. 그 이유는 여러 이유가 있지만 호출되는 코루틴 스코프를 블로킹하여 이 로직을 수행한 뒤 해당함수를 사용한 곳에서 받아온 결과를 온전히 사용하기 위해서입니다.
LoadParams 같은 경우 Key를 load메서드에서 사용하기 위해 필요합니다.
크게 키와 로드할 페이지의 크기로 나눌 수 있습니다.
반환값인 LoadResult는 크게 Error, Invaild, Page이 3가지로 나뉩니다. LoadResult.Page는 로드에 성공한 경우, LoadResult.Error는 오류가 발생한 경우, LoadResult.Invalid는 PagingSource가 더 이상 결과의 무결성을 보장할 수 없으므로 무효화되어야 하는 경우 사용됩니다.
정상적인 반환을 하는 경우인 LoadResult.Page는 반환하는 값인 data의 리스트, 이전에 사용된 키, 다음에 사용할 키를 필수적으로 넣어줘야 합니다. 이외에도 itemsBefore이나 itemsAfter라는 로드된 데이터 앞과 뒤에 표시할 자리표시자의 수를 넣어줄 수도 있습니다.
override fun getRefreshKey(state: PagingState<Int, Sample>): Int? {
TODO("Not yet implemented")
}
getRefreshKey()이 메서드는 Paging 라이브러리가 UI 관련 항목을 새로고침해야 할 때 호출됩니다. 지원 PagingSource의 데이터가 변경되었기 때문입니다. PagingSource의 기본 데이터가 변경되었으며 UI에서 업데이트해야 하는 이 상황을 무효화라고 합니다. 무효화되면 Paging 라이브러리가 데이터를 새로고침할 새 PagingSource를 만들고 새 PagingData를 내보내 UI에 알립니다. getRefreshKey()에서 반환되는 키는 PagingSource의 무효화로 인해 다음 PagingSource의 초기 로드에 사용되는 키를 제공합니다. 이러한 무효화가 발생하는 이유는 다음 두 가지 중 하나입니다. PagingAdapter에서 refresh()를 호출했습니다. PagingSource에서 invalidate()를 호출했습니다.
getRefreshKey()의 경우 PagingState형태의 매개변수를 가집니다.
PagingState는 현재 PaginSource의 상태를 가집니다. 그렇기에 현재 몇 번째로 Paging을 했는지 알 수 있으며 이를 통해 초기화할 때의 값을 지정할 수 있습니다.
// 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
val key = article.id - (state.config.pageSize / 2)
return max(STARTING_KEY, key)
}
override fun getRefreshKey(state: PagingState<Int, String>): Int? {
// Try to find the page key of the closest page to anchorPosition, from
// either the prevKey or the nextKey, but you need to handle nullability
// here:
// * prevKey == null -> anchorPage is the first page.
// * nextKey == null -> anchorPage is the last page.
// * both prevKey and nextKey null -> anchorPage is the initial page, so
// just return null.
return state.anchorPosition?.let { anchorPosition ->
val anchorPage = state.closestPageToPosition(anchorPosition)
anchorPage?.prevKey?.plus(1) ?: anchorPage?.nextKey?.minus(1)
}
}
위코드는 getRefreshKey의 구현한 예시입니다.
class SampleRepository {
fun samplePagingSource() = SamplePagingSource()
}
ViewModel 레이어 : Pager & Flow<PagingData<Value>>
private const val ITEMS_PER_PAGE = 50
class SampleViewModel(
private val repository: SampleRepository,
) : ViewModel() {
val items: Flow<PagingData<Sample>> = Pager(
config = PagingConfig(pageSize = ITEMS_PER_PAGE, enablePlaceholders = false),
pagingSourceFactory = { repository.samplePagingSource() }
)
.flow
.cachedIn(viewModelScope)
}
Pager 구성요소는 PagingSource 객체 및 PagingConfig 구성 객체를 바탕으로 반응형 스트림에 노출되는 PagingData 인스턴스를 구성하기 위한 공개 API를 제공합니다. ViewModel 레이어를 UI에 연결하는 구성요소는 PagingData입니다. PagingData 객체는 페이지로 나눈 데이터의 스냅샷을 보유하는 컨테이너입니다. PagingSource 객체를 쿼리하여 결과를 저장합니다.
ViewModel 레이어 : PagingDataAdapter
class SampleAdapter : PagingDataAdapter<Sample, ViewHolder>(DIFF_CALLBACK) {
// body is unchanged
}
이는 리사이클러뷰에서 사용되는 PagingDataAdapter입니다. 이는 ListAdapter와 똑같이 구현하면 됩니다.
PagingDataAdapter는 리사이클러뷰에 PagingData를 바인딩을 하게되면 리사이클러뷰가 PagingData 콘텐츠가 로드될 때마다 PagingDataAdapter에서 알림을 받은 다음 RecyclerView에 업데이트하라는 신호를 보냅니다.
class SampleActivity : AppCompatActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
val binding = ActivitySamplesBinding.inflate(layoutInflater)
val view = binding.root
setContentView(view)
val viewModel by viewModels<SampleViewModel>(
factoryProducer = { Injection.provideViewModelFactory(owner = this) }
)
val items = viewModel.items
val sampleAdapter = SampleAdapter()
binding.bindAdapter(sampleAdapter = sampleAdapter)
lifecycleScope.launch {
repeatOnLifecycle(Lifecycle.State.STARTED) {
items.collectLatest { pagingData:PagingData<Sample> ->
sampleAdapter.submitData(pagingData)
}
}
}
}
}
private fun ActivitySamplesBinding.bindAdapter(
sampleAdapter: SampleAdapter
) {
list.adapter = sampleAdapter
list.layoutManager = LinearLayoutManager(list.context)
val decoration = DividerItemDecoration(list.context, DividerItemDecoration.VERTICAL)
list.addItemDecoration(decoration)
}
로딩관련
PagingDataSource의 경우 load가 suspend 즉 중단함수이기 때문에 로딩에 관한 프로그레스바가 필요할 때가 있습니다. 이러한 상태를 LoadState라는 형태로 Loading, NotLoading, Error인지를 확인할 수 있습니다. 구현에서는 PagingDataAdapter의 loadStateFlow를 이용하여 CombinedLoadStates에 액세스 할 수 있습니다. CombinedLoadStates는 인스턴스는 데이터를 로드하는 Paging 라이브러리에 있는 모든 구성요소의 로드 상태를 설명합니다. CombinedLoadStates.source는 LoadStates 유형으로, 세 가지 유형의 LoadState 필드가 있습니다. LoadStates.append 사용자의 현재 위치 후에 가져오는 항목의 LoadState, LoadStates.prepend 사용자의 현재 위치 전에 가져오는 항목의 LoadState, LoadStates.refresh 초기 로드의 LoadState라는 세 가지 유형이 있어 이를 통하여 로딩상태인지 확인이 가능합니다.
lifecycleScope.launch {
repeatOnLifecycle(Lifecycle.State.STARTED) {
sampleAdapter.loadStateFlow.collect {
binding.prependProgress.isVisible = it.source.prepend is Loading
binding.appendProgress.isVisible = it.source.append is Loading
}
}
}
참고
https://developer.android.com/codelabs/android-paging-basics?hl=ko#0
https://leveloper.tistory.com/202
'안드로이드 > AAC(Android Architecture Components)' 카테고리의 다른 글
[AAC] Navigation - setupWithNavController (1) | 2024.06.15 |
---|---|
[AAC] Navigation 기능 구현 - 딥 링크 (0) | 2024.06.12 |
[AAC] Navigation의 정의와 적용 (0) | 2024.06.10 |
[AAC] Lifecycle 정의와 활용 (0) | 2024.06.07 |
[AAC] LiveData의 정의와 사용 (0) | 2024.06.06 |