Paging Library 사용하기

CmplxN·2020년 8월 16일
0

Paging Library?

어떤 DataSource에서 여러개의 데이터를 RecyclerView에 뿌려야 한다고 하자.
그러면 다음과 같이 구현할 수 있을 것이다.

  1. DataSource의 모든 Data를 가져온다.
  2. 일부분의 Data만 가져온 후, 필요할 때 또 가져온다.

1번 처럼 처음부터 모든 Data를 가져오면 구현하기 편할 것이다. 어떻게든 한번 받기만 하면, UI만 신경쓰면 되기 때문이다.

하지만, 1번처럼 쉽게쉽게 가기 곤란한 경우가 빈번하다.

Room에서 query를 했는데 결과가 예를 들어 10000개를 넘어간다고 치자. 그렇다면 10000개의 item 객체를 만들어서 List로 저장하고 있어야 한다. 그런데 User는 보통 1000개 정도의 Data만 보고 App을 종료한다고 한다. 이러면 9000개의 불필요한 item 객체를 만들어 메모리를 낭비하게 된다.

이외에도 Api에서 한번의 query에 전체 item을 전달하지 않는다던가 하는 경우가 태반이다.

이럴 경우, 해결책으로 Android Jetpack의 구성요소인 Paging을 사용할 수 있다.

아래는 Android Developers에서 말하는 Paging Library의 개요다.

페이징 라이브러리를 사용하면 작은 데이터 청크를 한 번에 로드하여 표시할 수 있습니다.
요청에 따라 일부 데이터를 로드하면 네트워크 대역폭 및 시스템 리소스 사용량을 줄일 수 있습니다.
안드로이드 공식 가이드

Components

DataSource

데이터를 정해진 범위만 불러온 뒤 PagedList로 로드하는 클래스. 이미 로딩된 경우, 데이터를 수정할 수 없으며 invalidate()을 통해 새로운 DataSource와 PagedList의 쌍을 생성해야한다.
DataSource는 3가지 종류로, 로딩 관련 로직을 오버라이딩 해서 사용한다.

DataSource.Factory

DataSource 객체를 만드는 Factory다. 지금까지 갖고 있던 데이터가 무효화 되면(예를 들면 refresh), override한 create()함수에서 새로운 DataSource 객체를 생성한다.

Rx(Live)PagedListBuilder

DataSource.Factory와 PagedList의 Config를 가지고 Observable<PagedList<T>>를 만드는 빌더 클래스다.
LiveData<PagedList<T>>를 만드는 LivePagedListBuilder클래스를 사용할 수도 있다.

PagedList

DataSource에서 페이징 된 데이터를 관리하는 리스트다. DataSource와 마찬가지로 데이터의 임의 수정이 불가능하다. loadAround()를 통해 추가 로드를 요청할 수 있다.

PagedListAdapter

RecyclerView에 로딩하기 위한 Adpater 클래스다. PagedListAdpater의 함수인 submitList(PagedList<T>)가 호출될 때, 변경된 요소만 업데이트하기 위해 DiffUtil.ItemCallback 객체를 super에 넘겨줘야 한다.

PagingWorkflow

DataSource Types

DataSource에는 크게 3가지가 있고, 공통적으로 loadxxx함수를 필수로 오버라이딩해야한다.
DataSource 종류에 맞게 loadxxx함수의 인자인 LoadCallback의 onResult에 결과를 전달한다.
Paging Library는 필요에 따라 loadxxx함수를 이용해 item을 로딩해줄 것이다.
더 자세한 설명은 링크에서 확인할 수 있다.

ItemKeyedDataSource

현재 로딩된 데이터의 양 끝에 있는 데이터의 키를 이용해 데이터를 페이징한다.

  1. 이니셜 로딩(loadInitial)
  2. 키를 이용한 다음 로딩(loadAfter)
  3. 키를 이용한 이전 로딩(loadBefore)
  4. item에서 key를 추출(getKey)

하는 함수를 필수로 오버라이딩 하여 구현한다.
ItemKeyedDataSource

PageKeyedDataSource

이전 / 다음 데이터의 키(페이지 번호)를 통해 페이징 한다.

  1. 이니셜 로딩(loadInitial)
  2. 페이지를 이용한 다음 로딩(loadAfter)
  3. 페이지를 이용한 이전 로딩(loadBefore)

하는 함수를 필수로 오버라이딩 하여 구현한다.
PageKeyedDataSource

PositionalDataSource

전후 페이지 정보 없이 특정 페이지를 바로 요청할 수 있다.
즉, xxxKeyedDataSource와 달리 순차적으로 요청하지 않아도 된다.
주소록 ㄱ단에서 ㅎ단으로 스크롤바를 이용해 끊김 없이 한번에 이동할 수 있는 것을 생각하면 된다.
참고로 Room은 PositionalDataSource를 반환형으로 자체 지원한다.

  1. 이니셜 로딩(loadInitial)
  2. 범위를 이용한 로딩(loadRange)

하는 함수를 필수로 오버라이딩 하여 구현한다.
PositionalDataSource

Implementation

다음은 PageKeyedDataSource를 사용한 Paging의 뼈대(?)다.
Retrofit, RxJava, Dagger를 사용했지만, 코드를 이해하는 데는 문제가 없을 것 같다.

  • DataSource
class MyDataSource(private val myService: MyService) : PageKeyedDataSource<Int, Item>() {
    override fun loadInitial(params: LoadInitialParams<Int>, callback: LoadInitialCallback<Int, Item>) {
        // initial loading
    }

    override fun loadAfter(params: LoadParams<Int>, callback: LoadCallback<Int, Item>) {
        myService.getData(params.key) // GET Request (Rx + Retrofit)
            .subscribeOn(Schedulers.io())
            .observeOn(AndroidSchedulers.mainThread())
            .subscribe(
                {
                    callback.onResult(it, params.key + 1)
                },
                {
		    // whatever error control
                }
            )
            .addTo(compositeDisposable)
    }

    override fun loadBefore(params: LoadParams<Int>, callback: LoadCallback<Int, Item>) {
        // loading pages that are prior to current list
    }
}
  • DataSource.Factory
class MyDataSourceFactory @Inject constructor(private val myService: MyService)
    : DataSource.Factory<Int, Item>() {
    override fun create(): DataSource<Int, Item> = MyDataSource(myService)
}
  • Repository
@Singleton
class MyRepository @Inject constructor(private val dataSourceFactory: MyDataSourceFactory) {
    fun getPagingRooms(): Observable<PagedList<Item>> =
        RxPagedListBuilder( // Rx version of PagedList Builder
            dataSourceFactory,
            PagedList.Config.Builder()
                .setInitialLoadSizeHint(PAGE_SIZE)
                .setPageSize(PAGE_SIZE)
                .build()
        ).buildObservable()
}
  • PagedListAdapter
val diffItemCallback = object: DiffUtil.ItemCallback<Item>() {
    override fun areItemsTheSame(oldItem: Item, newItem: Item): Boolean =
        oldItem.id == newItem.id // comparing by identifier

    override fun areContentsTheSame(oldItem: Item, newItem: Item): Boolean =  
        oldItem == newItem // comparing by contents
}

class MyAdapter : PagedListAdapter<Item, RecyclerView.ViewHolder>(diffItemCallback) {
    override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): RecyclerView.ViewHolder {
        // Rcv onCreateViewHolder
    }

    override fun onBindViewHolder(holder: RecyclerView.ViewHolder, position: Int) {
        // Rcv onBindViewHolder
    }
}
  • Activity
class MainActivity : AppCompatActivity() {
    // whatever codes for Activity

    fun initPagedList() {
        // PagedList in form of LiveData
        livePagedList.observe(this, Observer(myAdapter::submitList))
    }
}
profile
Android Developer

1개의 댓글

comment-user-thumbnail
2020년 12월 30일

깃헙 소스 참조할 수 있을까요?

답글 달기