AndroidX Jetpack ~ Paging 데이터의 위치 살펴보기

AndroidX Jetpack ~ Paging 데이터의 위치 살펴보기

Jan 2, 2022. | By: pluulove

본 글은 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 정의

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

PagingSource 정의

실제 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

Adapter에 데이터 전달

Pager의 반환값은 PagingData입니다. 이 PagingData를 Paging 전용 Adapter인 PagingDataAdapter에 전달하여 UI를 구축하는 데 사용할 수 있습니다.

class RedditActivity : AppCompatActivity() {
  //...

  private fun initAdapter() {
    lifecycleScope.launchWhenCreated {
      model.posts.collectLatest {
        adapter.submitData(it)
      }
    }
  }

  //...
}

지금까지 간단하게 Paging 사용법을 확인했습니다. 이후는 PagingDataAdapter 내부의 데이터가 실제로 위치하는 곳까지의 흐름을 살펴보겠습니다.

Paging 데이터의 위치 살펴보기

submitData 호출시 흐름

UI에서 Paging와 밀접하게 호출하는 함수 중 첫 진입점은 바로 submitData입니다. PagingData 등록 시에 사용하는 PagingDataAdapter#submitData의 내부를 들여다보면 아래와 같은 흐름으로 함수를 호출합니다.

  1. PagingDataAdapter#submitData
  2. AsyncPagingDataDiffer#submitData
  3. PagingDataDiffer#collectFrom

위 흐름의 종착지인 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 한 데이터가 보관되는 곳

먼저 각각의 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 정보를 통해서 취득합니다.

  1. PagingDataAdapter#getItem
  2. AsyncPagingDataDiffer#getItem
  3. PagingDataDiffer#get

Etc

좀 더 상세하게 Paging 라이브러리를 살펴볼 경우 아래 사이트를 참고해 주세요.

comments powered by Disqus

Currnte Pages Tags

Android AndroidX Paging

About

Pluu, Android Developer Blog Site

이전 블로그 링크 :네이버 블로그

Using Theme : SOLID SOLID Github

Social Links