Android/Kotlin

안드로이드 Paging3 라이브러리 사용

easy-1 2021. 12. 11. 20:24

<개요>

오픈 API를 사용하거나 자체 API를 사용하여 많은 양의 데이터를 받아올 경우에

한번에 모든 데이터를 받아오는것 보다 데이터를 필요한 만큼만 나눠서 받아오는것이 퍼포먼스나

사용자가 사용하기에 빠른 데이터 처리 효과를 줄 수 있다.

페이징 방법에 대해서 간략하게 말하자면

1. 페이지넘버와 데이터갯수를 요청

2. 리사이클러뷰를 통하여 보여줌

3. 하단까지 스크롤 시 새로운 데이터 요청함 (페이지넘버 + 1, 데이터갯수)


<적용방법>

1. app gradle에 라이브러리 종속성을 추가해줌

- 페이징처리

    - Paging3

- Api 호출

    - Retrofit2

    - rxjava2

    - okHttp3

    - Gson

// paging 3
def paging_version = "3.1.0"
implementation "androidx.paging:paging-runtime-ktx:$paging_version"
// rx retrofit
implementation 'com.squareup.retrofit2:adapter-rxjava2:2.6.2'
// retrofit
implementation 'com.squareup.retrofit2:retrofit:2.9.0'
// gson
implementation 'com.google.code.gson:gson:2.8.6'
implementation 'com.squareup.retrofit2:converter-gson:2.9.0'
// okhttp
implementation "com.squareup.okhttp3:logging-interceptor:4.5.0"

2. data class 생성

- response 는 정보, result 는 리스트 데이터가 담김

data class MainModel(
    @SerializedName("response")
    var response: ResponseInfo,
    @SerializedName("result")
    var resultsList: ArrayList<Results>
)

data class ResponseInfo(
    var end: Boolean,           // 현재 페이지가 마지막인지 여부
)

data class DocumentsInfo(
    var title: String,           // title 데이터   
    var contents: String,        // contents 데이터   
) : Serializable

3. Api 호출 인터페이스 생성

- size 는 요청 데이터 사이즈

- page 는 1씩 증가 시킬 페이지 번호

- value 는 데이터 호출에 이용되는 구분값

interface RetrofitService {

    companion object {
        const val BASE_URL = "https://sample.com/"   // 호출 URL
        const val REST_API_KEY = BuildConfig.API_KEY // 키노출을 하지않기위해 gradle 이용
    }

    @Headers("Authorization: $REST_API_KEY") // 인증키 헤더추가
    @GET("search") // GET 방식 호출
    fun getResults(
        @Query("size") size: Int,
        @Query("page") page: Int,
        @Query("value") value: String
    ): Call<MainModel>
}

4. 데이터 보여줄 RecyclerView Adapter 생성

class PagingAdapter() : PagingDataAdapter<ResultsInfo, PagingAdapter.PagingViewHolder>(
    diffCallback
) {

    // ViewHolder
    inner class PagingViewHolder(
        val binding: ItemResultsBinding // item_result.xml 바인딩하여 사용
    ) : RecyclerView.ViewHolder(binding.root) {
        fun bind(info: ResultsInfo) {
            // 바인딩
            binding.model = info

            // 아이템 클릭
            itemView.setOnSingleClickListener {
                // ...
            }
        }

    }

    override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): PagingViewHolder {
        val layoutInflater = LayoutInflater.from(parent.context)
        return PagingViewHolder(
            ItemResultsBinding.inflate(layoutInflater, parent, false)
        )
    }


    override fun onBindViewHolder(holder: PagingViewHolder, position: Int) {
        val item = getItem(position)
        if (item != null) {
            // 바인딩
            // 따로 BindingUtils 만들어 xml 에서 데이터 연결해줌
            holder.bind(item)
        }
    }

    // 두 리스트간 차이점을 찾고 업데이트 되어야 할 목록을 반환
    companion object {
        private val diffCallback = object : DiffUtil.ItemCallback<ResultsInfo>() {

            override fun areItemsTheSame(oldItem: ResultsInfo, newItem: ResultsInfo): Boolean {
                return oldItem == newItem
            }

            override fun areContentsTheSame(
                oldItem: ResultsInfo,
                newItem: ResultsInfo
            ): Boolean {
                return oldItem == newItem
            }
        }
    }

}

5. Api 호출과 페이징 처리를 진행할 PagingSource 클래스 생성

class PagingSource (
    private val service: RetrofitService,
) : PagingSource<Int, ResultsInfo>() {

    override suspend fun load(params: LoadParams<Int>): LoadResult<Int, ResultsInfo> {
        return try {
            // 최초 요청 페이지
            val pageIndex = params.key ?: 1
            // Api 호출 결과 리스트
            val response =
                service.getRsults(
                    size = 50,
                    page = pageIndex,
                    value = "value"
                ).awaitResponse().body()
            // 검색 리스트
            val data: List<ResultsInfo> = response?.resultsLits ?: listOf()

            // 페이지 넘버값 증가
            val nextKey =
                // 마지막 페이지, 데이터 여부 확인
                if (response!!.end.is_end || data.isEmpty()) {
                    null
                } else {
                    pageIndex + 1
                }
            // 페이징 처리
            LoadResult.Page(
                data = data,
                prevKey = null,
                nextKey = nextKey
            )

        } catch (e: Exception) {
            LoadResult.Error(e)
        }
    }

    override fun getRefreshKey(state: PagingState<Int, ResultsInfo>): Int? {
        // Try to find the page key of the closest page to anchorPosition, from
        // either the prevKey or the nextKey, but you need to handle nullability
        // here:
        //  * prevKey == null -> anchorPage is the first page.
        //  * nextKey == null -> anchorPage is the last page.
        //  * both prevKey and nextKey null -> anchorPage is the initial page, so
        //    just return null.
        return state.anchorPosition?.let { anchorPosition ->
            state.closestPageToPosition(
                anchorPosition
            )?.prevKey?.plus(1) ?: state.closestPageToPosition(anchorPosition)?.nextKey?.minus(1)
        }
    }


}

6. 받아올 데이터에 대한 Repository 생성

- Flow를 이용한 데이터 처리

interface PagingRepository {

    fun getResultsList(): Flow<PagingData<ResultsInfo>>
}

7. PaingRepository를 상속한 Impl 클래스 생성

class PagingRepositoryImpl(
    private val service: RetrofitService,
) : PagingRepository {

    override fun getResultsList(): Flow<PagingData<ResultsInfo>> {
        return Pager(PagingConfig(pageSize = 50)) {
            PagingSource(service)
        }.flow
    }

}

8. ViewModel 에서 생성된 PagingRepositoryImpl 호출

- cachedIn()은 CoroutineScope를 사용하여 로드된 데이터를 캐시함

 

// 페이징 데이터
fun setPaging(): Flow<PagingData<ResultsInfo>> {
    return PagingRepositoryImpl(
        service
    ).getResultsList().cachedIn(viewModelScope)
}

9. Activity / Fragment에서 어뎁터 연결

// 페이징 데이터
lifecycleScope.launch {
    setPaging().collectLatest { pagingData ->
        // 어뎁터 연결
        adapter.submitData(pagingData)
    }
}