참고
'짤막 기록'은 나 혼자만 전후 맥락을 알아볼 수 있는 불친절한 설명의 글에 붙이는 말머리입니다.
나중에 수정되어 사라질 수도 있습니다.

 

Paging library

Paging library는 Android Jetpack에서 추가된 라이브러리다. RecyclerView + LiveData 조합을 사용하면 무한스크롤을 뚝딱 구현할 수 있다. 사용 방법은... 공식 문서나 미디엄 블로그들을 뒤져보면 잘 나와있음 🙃

 

Paging library를 쓰면 아래와 같은 점이 편리하다.

  • AsyncPagedListDiffer를 사용하고 있어서 diffing 처리를 알아서 해줌
  • 무한스크롤 구현시 이전/다음 데이터를 로드할 타이밍 체크(스크롤 위치 변화 감지 등등..)할 필요가 없음
  • 추가 데이터 로드 후 UI 업데이트도 알아서 해줌

데이터를 대략 아래와 같은 순서로 로딩한다.

  1. 현재 RecyclerView에 표시할 데이터 일부만 메모리에 할당하고, 나머지는 null 상태로 냅둠. (즉 모든 데이터 리스트를 한번에 메모리에 적재하지 않음)
  2. 사용자가 스크롤을 움직여서 null인 요소에 접근하면 해당 요소가 속한 chunk를 DataSource에 요청
  3. PagedList는 데이터가 로드될 때까지 그 데이터를 null로 표시함
  4. 데이터 로드가 완료되면 PagedListAdpater의 onBindViewHolder 함수가 호출되면서 itemView를 구성

 

PagedList

RecyclerView에 들어갈 데이터 리스트를 관리하기 위해 보통 ArrayList를 사용했는데, Paging Library를 이용하려면 PagedList라는 지정된 콜렉션을 사용해야 한다. PagedList는 chunk 또는 page 단위로 데이터를 로드하는 페이징 라이브러리의 핵심 요소다.

 

PagedList의 변형

그동안 List<A>를 List<B>로 변형하고 싶다면 kotlin의 map을, LiveData<A>를 LiveData<B>로 변형하고 싶으면 androidx.lifecycle.Transformations의 map을 사용했다.

마찬가지로 LiveData<PagedList<A>>를 LiveData<PagedList<Y>>로 변형해야 할 때가 있는데, 이때는 DataSource.Factory에 정의된 map(), mapByPage()라는 함수를 사용할 수 있다. 사용 방법은 기존의 map과 동일하고, 차이점은 반환 타입이 List가 아닌 PagedList라는 것이다.

 

출처 : https://developer.android.com/reference/androidx/paging/DataSource
출처 : https://developer.android.com/reference/androidx/paging/DataSource

요약하자면,

  • map : map 내부에 item 타입으로 X가 넘어옴
  • mapByPage : map 내부에 item 타입으로 List<X>가 넘어옴

PagedList<X>를 PagedList<Y>로 변환하고 싶으면 DataSource 객체에 map이나 mapByPage를 적용하면 된다.

아래는 PagedList<Movie>를 PagedList<MovieUiModel>로 변환하는 예제다.

 

  • ViewModel
val pageDataSource = MoviePageDataSource.Factory(pagingSuccess, pagingFailed).map { 
	// item마다 개별적으로 호출되는 부분으로, 새로운 type을 반환하도록 코드 작성
	it.toUiModel()
}

// LivePagedListBuilder에 map이 적용된 DataSource를 넣고 PagedList를 만들면 됨
val pagingMovieList: LiveData<PagedList<MovieUiModel>> =
    LivePagedListBuilder(pageDataSource, MoviePageDataSource.moviePageConfig).build()
  • DataSource (코드가 길어져서 서버에서 데이터 불러오는 부분은 생략함)
// 서버 or 데이터베이스로부터 PagedList에 필요한 데이터를 제공하는 역할
class MoviePageDataSource private constructor(
    private val pagingSuccess: () -> Unit,
    private val pagingFailed: (errMsg: String) -> Unit
) : PageKeyedDataSource<Int, Movie>() {
    private var totalPages = FIRST_PAGE

    companion object {
        private const val FIRST_PAGE = 1
        private const val PAGING_UNIT = 1
        private const val PAGE_SIZE = 20

        val moviePageConfig = PagedList.Config.Builder()
            .setPageSize(PAGE_SIZE)
            .setEnablePlaceholders(false)
            .setInitialLoadSizeHint(PAGE_SIZE)
            .setPrefetchDistance(PAGE_SIZE)
            .build()
    }

    class Factory(
        private val pagingSuccess: () -> Unit,
        private val pagingFailed: (errMsg: String) -> Unit
    ) : DataSource.Factory<Int, Movie>() {

        override fun create(): DataSource<Int, Movie> = MoviePageDataSource(pagingSuccess, pagingFailed)
    }

    override fun loadInitial(params: LoadInitialParams<Int>, callback: LoadInitialCallback<Int, Movie>) {
        // 최초에 보여줄 데이터 로딩
    }

    override fun loadAfter(params: LoadParams<Int>, callback: LoadCallback<Int, Movie>) {
        if (params.key > totalPages) {
            return // 마지막 페이지일 경우 로딩하지 않음
        }

        // 추가로 보여줄 데이터 로딩
    }

    override fun loadBefore(params: LoadParams<Int>, callback: LoadCallback<Int, Movie>) {
        // do nothing
    }
}

 

참고