안드로이드 Paging3 라이브러리 사용
<개요>
오픈 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)
}
}