본 글은 AndroidX Jetpack Paging 사용 시 Paging 한 데이터의 흐름과 최종 존재하는 위치를 살펴보는 글입니다.
테스트로 사용한 소스는 아래의 Paging Sample입니다.
샘플 : https://github.com/android/architecture-components-samples/blob/main/PagingWithNetworkSample/README.md
샘플에 관련된 부분은 아래에서 다루지만, 자세한 Paging의 내용은 공식 문서를 참고해 주세요.
Android 개발자 > 문서 > 가이드 > 페이징 라이브러리 개요 : https://developer.android.com/topic/libraries/architecture/paging/v3-overview
Pager/PagingConfig를 사용하여 Paging의 단위를 구성하고, 외부로 PagingData 객체를 전달하는 Flow 타입의 스트림을 반환합니다. 이 Flow를 UI, 정확하게는 PagingDataAdapter에 전달할 수 있습니다.
class InMemoryByItemRepository(private val redditApi: RedditApi) : RedditPostRepository {
override fun postsOfSubreddit(subReddit: String, pageSize: Int) = Pager(
PagingConfig(
pageSize = pageSize,
enablePlaceholders = false
)
) {
ItemKeyedSubredditPagingSource(
redditApi = redditApi,
subredditName = subReddit
)
}.flow
}
소스 출처 : InMemoryByItemRepository
실제 Paging에 해당하는 데이터 호출은 PagingSource 인터페이스를 구현하는 클래스가 담당합니다.
class ItemKeyedSubredditPagingSource(
private val redditApi: RedditApi,
private val subredditName: String
) : PagingSource<String, RedditPost>() {
override suspend fun load(params: LoadParams<String>): LoadResult<String, RedditPost> {
return try {
val items = /** Suspend 함수 호출 */
Page(
data = items,
prevKey = /** Prev page Key */,
nextKey = /** Next page Key */
)
} catch (e: IOException) {
LoadResult.Error(e)
} catch (e: HttpException) {
LoadResult.Error(e)
}
}
...
}
소스 출처 : ItemKeyedSubredditPagingSource
Pager의 반환값은 PagingData입니다. 이 PagingData를 Paging 전용 Adapter인 PagingDataAdapter에 전달하여 UI를 구축하는 데 사용할 수 있습니다.
class RedditActivity : AppCompatActivity() {
//...
private fun initAdapter() {
lifecycleScope.launchWhenCreated {
model.posts.collectLatest {
adapter.submitData(it)
}
}
}
//...
}
지금까지 간단하게 Paging 사용법을 확인했습니다. 이후는 PagingDataAdapter 내부의 데이터가 실제로 위치하는 곳까지의 흐름을 살펴보겠습니다.
UI에서 Paging와 밀접하게 호출하는 함수 중 첫 진입점은 바로 submitData
입니다. PagingData 등록 시에 사용하는 PagingDataAdapter#submitData
의 내부를 들여다보면 아래와 같은 흐름으로 함수를 호출합니다.
위 흐름의 종착지인 PagingDataDiffer#collectFrom
의 내부는 아래의 형태를 띠고 있습니다.
public abstract class PagingDataDiffer<T : Any>(
...
) {
public suspend fun collectFrom(pagingData: PagingData<T>) {
collectFromRunner.runInIsolation {
receiver = pagingData.receiver
pagingData.flow.collect { event ->
// Paging 관련 이벤트가 처리
}
}
}
}
소스 출처 : PagingDataDiffer#collectFrom
참고로 PagingDataDiffer는 외부로 공개되지 않은 클래스라서 실제 개발자들이 다룰 수는 없습니다.
PagingDataDiffer#collectFrom
에서는 파라미터로 전달된 PagingData의 이벤트를 수집하고 있습니다. 이로써 추가로 발생하는 Paging 이벤트들도 해당 Lambda 내부로 유입되며, submitData은 호출되지 않습니다. 다만, refresh/retry와 같은 이벤트는 PagingDataAdapter#submitData가 호출되는 것으로 보입니다.
먼저 각각의 Paging 데이터는 PagingSource를 통해서 생성된다는 것을 확인했습니다. 그 이후, Paging 이벤트들은 PagingDataDiffer에서 수집됩니다. 결국, 데이터들이 도착하는 곳은 PagingDataDiffer 내부를 확인하면 쉽게 찾을 수 있습니다.
현재 PagingDataDiffer 내부에는 Paging과 관련된 인스턴스로 PagePresenter 클래스를 찾을 수 있습니다.
public abstract class PagingDataDiffer<T : Any>(
...
) {
private var presenter: PagePresenter<T> = PagePresenter.initial()
public suspend fun collectFrom(pagingData: PagingData<T>) {
collectFromRunner.runInIsolation {
receiver = pagingData.receiver
pagingData.flow.collect { event ->
// pagingData의 flow로 넘겨온 event에 따라서 presenter에 데이터가 전달됨
if (event is Insert && event.loadType == REFRESH) {
presentNewList(...)
} else if (event is StaticList) {
presentNewList(...)
} else {
...
// UI에 표시할 이벤트 전달
presenter.processEvent(event, processPageEventCallback)
...
}
}
}
}
// 새로운 PagePresenter 인스턴스 처리
private suspend fun presentNewList(
pages: List<TransformablePage<T>>,
placeholdersBefore: Int,
placeholdersAfter: Int,
dispatchLoadStates: Boolean,
sourceLoadStates: LoadStates?,
mediatorLoadStates: LoadStates?,
) {
...
val newPresenter = PagePresenter(
pages = pages,
placeholdersBefore = placeholdersBefore,
placeholdersAfter = placeholdersAfter,
)
...
}
}
이어서 PagePresenter 내부를 살펴봅니다. 여기에서는 간단하게 get/processEvent 함수만 살펴보겠습니다.
internal class PagePresenter<T : Any>(
pages: List<TransformablePage<T>>,
...
) : NullPaddedList<T> {
private val pages: MutableList<TransformablePage<T>> = pages.toMutableList()
fun get(index: Int): T? {
// PagingDataAdapter#getItem을 호출 시, 최종적으로 PagePresenter#get가 호출
}
// PageEvent에 따라서 데이터의 위치를 처리합니다.
fun processEvent(pageEvent: PageEvent<T>, callback: ProcessPageEventCallback) {
when (pageEvent) {
is PageEvent.Insert -> /** ... */
is PageEvent.Drop -> /** ... */
is PageEvent.LoadStateUpdate -> /** ... */
is PageEvent.StaticList -> /** ... */
}
}
소스 출처 : PagePresenter
참고로 PagePresenter는 internal 클래스라서 실제 개발자들이 다룰 수는 없습니다.
몇 번의 Paging이 되고서 PagePresenter 내부의 pages 정보를 디버깅을 통해서 확인하면, 아래 이미지와 같이 각 Page에 대한 정보와 PagingSource로 전달받은 데이터가 존재합니다.
이것으로 Paging 정보는 PagePresenter 내부에 보관
되는 것을 확인했습니다.
그러므로 PagingDataAdapter에서 Item을 가져올 시에도 PagePresenter의 pages 정보를 통해서 취득합니다.
좀 더 상세하게 Paging 라이브러리를 살펴볼 경우 아래 사이트를 참고해 주세요.
comments powered by Disqus
Subscribe to this blog via RSS.
LazyColumn/Row에서 동일한 Key를 사용하면 크래시가 발생하는 이유
Posted on 30 Nov 2024