Android 공부/Android Library Study

서울시 공공데이터 API를 활용한 Paging Library 사용하기 - 2

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

DataSource를 만드는 부분입니다. 

1.  interface의 getJobCafeList를 통해서 Index 단위로 API에서 데이터를 수신한다.
2. companion object 를 통해서, 페이지의 사이즈, 첫 번째 페이지 인덱스를 설정했습니다.
3. loadInitial(), loadBefore(), loadAfter()를 분기해서 데이터를 가져옵니다.
4. 이 때, callback.onResult()를 통해서 jobCafes 데이터와 페이지가 페이징 라이브러리에 의해서 관리됩니다.
5. basePresnter.addDisposable()은 RxJava를 관리하기 위해서 사용되는 것이므로, 없다고 생각해도 무방합니다.

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+1params.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

DataSource.Factory 부분으로, 위에서 만든 DataSource를 생성해서 create 부분에 넘겨주는 로직입니다.

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. 결과

 페이징을 통해서, 실제 공공데이터의 API를 가져와서 인피니티 스크롤링 리스트를 만들어보는 게시글이었습니다.
일반, 라이브러리처럼 페이징이라는 것이 딱 떨어지는 것인 줄 알았는데, 사용자가 만들려고 하는 리스트를 정해진 구조에 맞게 라이브러리를 제공해준다는 것을 배울 수 있었습니다.
 만들면서, 머터리얼 디자인, Lottie View를 사용하니까, 디자인이 깔끔하게 떨어져서 재밌었던 경험이었습니다. 구글에는 양질의 글이 있고, 해당 토이 프로젝트를 만들면서 참조했던 사이트를 참조합니다.


3. 참조

(7 steps to implement Paging library in Android, Anitaa MurthyJul 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

Sep 26 2018 · Article (30 mins) · Intermediate))


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)