0. Paging
- Data : Room 혹은 Retrofit과 같은 라이브러리를 이용해서 데이터를 가져오면 된다. Paging 라이브러리는 만능이라기 보다는, 정형화된 틀을 제공해주는데, Data를 가져오는 부분에서도 사용자는 Paging의 규격에 맞게 데이터를 가져올, 인터페이스를 형성해야 한다.
예를 들면, 10개씩 데이터를 가져와서 리스트에 보여준다고 할 경우에는, getFetch(startIndex, endIndex)와 같이 틀을 만들어줘야만 한다. 그렇게 되면, PagedKeyedDataSource를 통해서 loadInitail(), loadBefore(), loadAfter()로 관리할 수 있다.
- PagedList : PagedKeyedDataSource를 통해서 데이터를 획득하면, callBack.onResult()를 통해서 데이터를 수신받을 수 있다. PagedList는 개발자의 취향에 따라서 LiveData, RxJava를 통해서 비동기로 데이터를 수신받는 환경을 제공한다. 그렇게 되면, UI에 수신한 데이터를 넘겨주기 위해서 adapter.submitList(data)라는 별도의 행위를 통해서 Adapter에 알려줘야 한다.
- UI : List를 만들기 위해서는 일반적으로 사용하는 RecyclerView.Adapter<ViewHolder>대신에 PagedListAdapter<Item,ViewHolder>을 이용해서 Adapter를 생성해줘야 한다. 여기에서는 위에서 말한 adapter.submitList()라는 매소드를 통해서 데이터를 갱신받게 되고, 갱신된 데이터를 확인하기 위해서 DiffUtill을 통해서 리스트의 데이터변화를 감지한다.
0-1. 페이징을 사용하기 위한 구조
Retrofit, Local DB, Room 등을 이용해서 Data를 가져오게 되면, PagedList에 데이터를 전달해주고, 받은 데이터를 Submit해서 UI를 변화시킨다. 이러한 과정에서 자신이 만들려고하는 Paging RecyclerView에 맞게 구조를 커스텀해야한다. 현재 사용하는 프로젝트에서는 리사이클러뷰를 스크롤링 하는 경우, 마지막 데이터를 가리키면, 데이터를 추가적으로 받아오는 구조로 토이 프로젝트를 구상했다.
서론의 Paging의 복잡한 말은 그림을 그리면 아래와 같다.
기존의 리사이클러뷰에서는 Retrofit과 Room에서 데이터를 가져오면, 바로 Adapter와 연결시켜주는 구조였을 것이다. 페이징 라이브러리를 사용함에 따라서, DataSource.Factory를 만들게 된다.
이 때, PAGE_SIZE와 FIRST_PAGE를 상수로 하여, load() 메소드들을 이용해서 데이터를 가져온다.
- loadInitial() : 초기 리스트에서 데이터를 가져오는 메소드.
- loadBefore() : 이전 버튼을 누르는 리스트를 만든다면, 이전 데이터를 가져오는 메소드.
- loadAfter() : 다음 버튼을 누르는 리스트, 스크롤링을 하는 경우, 다음 데이터가 필요할 때, 데이터를 가져오는 메소드.
- PagedList.Config.Builder() : 수신하는 데이터의 양, 데이터를 가져오는 시점, placeHolder의 사용 여부등에 대한 자신이 만들려고하는 리스트를 옵션을 설정하는 Builder
- Adapter.submit() : PagedListAdapter에 데이터를 전송하기 위해서 사용하는 메소드.
- DiffUtil : PagedListAdapter의 변화를 감지하는 객체
1. 코드
1-1. JobCafeDataSource - PagedKeyedDataSource
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 | interface JobCafeDataSource { fun getJobCafeList(startIndex: Int, endIndex: Int): Observable<WrappingJobCafeList> } class JobCafeDataSourceImpl( private val jobCafeRepository: JobCafeRepository, private val basePresenter: BaseContract.Presenter? ) : PageKeyedDataSource<Int, JobCafe>(), JobCafeDataSource { companion object { const val PAGE_SIZE = 10 const val FIRST_PAGE = 1 } override fun loadInitial(params: LoadInitialParams<Int>, callback: LoadInitialCallback<Int, JobCafe>) { basePresenter?.addDisposable(getJobCafeList(FIRST_PAGE, FIRST_PAGE + PAGE_SIZE).subscribe { callback.onResult(it.jobCafeList.jobCafes, null, FIRST_PAGE + PAGE_SIZE) }) } override fun loadBefore(params: LoadParams<Int>, callback: LoadCallback<Int, JobCafe>) { basePresenter?.addDisposable(getJobCafeList(params.key, params.key + params.requestedLoadSize).subscribe { val paramKey = if (params.key > PAGE_SIZE) params.key - PAGE_SIZE else 0 callback.onResult(it.jobCafeList.jobCafes, paramKey) }) } override fun loadAfter(params: LoadParams<Int>, callback: LoadCallback<Int, JobCafe>) { basePresenter?.addDisposable(getJobCafeList(params.key+1, params.key + params.requestedLoadSize).subscribe { if (params.key + PAGE_SIZE < it.jobCafeList.listTotalCount) { callback.onResult(it.jobCafeList.jobCafes, params.key + PAGE_SIZE) } else params.key }) } override fun getJobCafeList(startIndex: Int, endIndex: Int): Observable<WrappingJobCafeList> { return jobCafeRepository .getJobCafeList(startIndex,endIndex) } } | cs |
1-2. JobCafeDataFactory - DataSource.Factory
1 2 3 4 5 6 7 | class JobCafeDataFactory(basePresenter : BaseContract.Presenter) : DataSource.Factory<Int, JobCafe>() { private val jobCafeDataSource = JobCafeDataSourceImpl(JobCafeRepositoryImpl(RetrofitProvider.provideSeoulApi()), basePresenter) override fun create(): DataSource<Int, JobCafe> { return jobCafeDataSource } } | cs |
1-3. JobCafePresenter
여기에서 봐야하는 코드는 makeJobCafeList() 입니다.
1. JobCafeDataFactory 객체를 생성한 후, PagedList.Config를 만들어줍니다.
2. PlaceHolder를 사용하고, PageSize를 설정한 합니다.
3. PrefetchDistance(1)을 통해서, 마지막 인덱스에서 데이터를 추가적으로 가져오는 리스트를 만듭니다.
4. RxPagedListBuilder, LiveDataPagedListBuilder를 통해서 return 받고 싶은 타입을 설정해줍니다.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 | class JobCafePresenter( private val jobCafeListAdapter: JobCafeListAdapter, private val jobCafeView: JobCafeContract.View ) : BaseContract.Presenter(), JobCafeContract.Presenter { override fun initView() { jobCafeView.initJobCafeList() } override fun makeJobCafeList(): Observable<PagedList<JobCafe>> { val jobCafeDataSourceFactory = JobCafeDataFactory(this) val pagedListConfig = PagedList.Config.Builder() .setEnablePlaceholders(true) .setPageSize(PAGE_SIZE) .setPrefetchDistance(1) .build() return RxPagedListBuilder(jobCafeDataSourceFactory, pagedListConfig).buildObservable() } override fun getJobCafeList() { addDisposable(makeJobCafeList() .subscribeOn(Schedulers.computation()) .observeOn(AndroidSchedulers.mainThread()).subscribe { jobCafeListAdapter.submitList(it) }) } } | cs |
1-4. JobCafeListAdapter - PagedListAdapter
JobCafeListAdapter는 DiffUtil을 통해서 리사이클러뷰의 변화를 자동으로 처리해줍니다.
저는 이미지를 관리하기 위해서 GlideLoading이라는 인터페이스를 만들어서, 데이터를 수신 할 때, PlaceHolder를 보여주는 로직을 만들었습니다.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 | class JobCafeListAdapter : PagedListAdapter<JobCafe, RecyclerView.ViewHolder>(DIFF_CALLBACK) { interface GlideLoading{ fun onLoad() fun onCompleted() } override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): RecyclerView.ViewHolder { val itemJobcafeListBinding= ItemJobcafeListBinding.inflate( LayoutInflater.from(parent.context), parent, false) return JobCafeViewHolder(itemJobcafeListBinding) } override fun onBindViewHolder(holder: RecyclerView.ViewHolder, position: Int) { (holder as JobCafeViewHolder).apply { itemJobcafeListBinding.jobCafe = getItem(position) itemJobcafeListBinding.loadingInterface = GlideLoadingImpl(itemJobcafeListBinding) itemJobcafeListBinding.loadingInterface?.onLoad() } } inner class JobCafeViewHolder(val itemJobcafeListBinding: ItemJobcafeListBinding) : RecyclerView.ViewHolder(itemJobcafeListBinding.root) class GlideLoadingImpl(private val itemJobcafeListBinding: ItemJobcafeListBinding) : GlideLoading{ override fun onLoad() { itemJobcafeListBinding.lavSkeletonLoading.visibility = View.VISIBLE itemJobcafeListBinding.clContainer.visibility = View.GONE } override fun onCompleted() { itemJobcafeListBinding.lavSkeletonLoading.visibility = View.GONE itemJobcafeListBinding.clContainer.visibility = View.VISIBLE } } companion object { val DIFF_CALLBACK = object : DiffUtil.ItemCallback<JobCafe>() { override fun areItemsTheSame(oldItem: JobCafe, newItem: JobCafe) = oldItem == newItem override fun areContentsTheSame(oldItem: JobCafe, newItem: JobCafe) = oldItem == newItem } } } | cs |
2. 결과
3. 참조
(7 steps to implement Paging library in Android, Anitaa Murthy, Jul 2, 2018 · 4 min read)
https://www.raywenderlich.com/6948-paging-library-for-android-with-kotlin-creating-infinite-lists
(Paging Library for Android With Kotlin: Creating Infinite Lists, By Alex Sullivan
)
https://developer.android.com/topic/libraries/architecture/paging#paged-list
(Android Developers Docs 안내, Paging library overview)
https://material.io/design/components/lists.html#specs
(Material Design, Lists)
https://lottiefiles.com/6739-skeleton-ui
(Lottie , Byengsoo baek, skeleton_ui)
'Android 공부 > Android Library Study' 카테고리의 다른 글
Android Worker - 매일 알람 만들기 (0) | 2019.11.09 |
---|---|
인앱결제 - 구매후 생각해야 할 것 (3) | 2019.11.09 |
안드로이드 인앱결제 - 구매 로직 (2) | 2019.11.09 |
서울시 공공데이터 API를 활용한 Paging Library 사용하기 - 1 (0) | 2019.08.18 |
테스트 코드로 Retrofit 테스트 해보기 (1) | 2019.08.06 |