Android Paging
오늘은 Android Paging에 대해서 몇가지 적어보려고 합니다!
Paging은 Androidx에서 사용할 수 있습니다. 때문에 꼭 Dependency에 추가하여 확인 해주세요!
우선 Paging의 정의 부터 내려보겠습니다.
만약 Client에서 Server에게 요청을 하나 한다고 가정해 보겠습니다.
파라미터를 총 30을 보내면 Server가 30개의 List를 Response로 주어 Client가 30개의 목록들을 화면에 표시합니다.
사용자가 30개를 다보고 그 다음 목록들이 필요하면 또 30개를 요청하여 목록들을 받아 화면에 표시합니다.
이렇게 받을 데이터들을 특정한 쿼리와 함께 정의해 요청한 것들에 대해 화면에 보여주는 과정 들을 Paging이라고 정의 내릴 수 있겠습니다.
(예 : page : 1, size: 30 -> page : 2, size : 30)
Android에서는 Paging 기능을 라이브러리로 제공하고 있습니다. Paging 라이브러리는 무수히 많겠지만 서드파티 라이브러리 보단 Android에서 제공하는 것을 공부하고 알고 적용해보는게 좋을 것 같다고 생각합니다. 바로 시작해 볼께요
데이터 소스 정의
Androidx의 Paging을 구현하기 위해서 개발자가 PagingSource를 구현해야합니다.
PagingSource에 어떤 함수들이 있는지, 또 어떤것을 구현해야하는지 알아보겠습니다.
class SamplePagingDataSource (
private val query: String,
private val networkAPI: NetworkAPI
): PagingSource<Int, Documents>() {
/**/
}
PagingSource를 구현하기 위해선 Paging Key인 데이터 로드를 위한 식별자와 데이터를 override 함으로써 정의 해야합니다.
저는 예제로 network 통신을 예로 들었으나 DataBase로도 가능합니다.
필수적인 메소드 2가지부터 알아보고 다른 메소드들도 몇가지 알아보겠습니다.
1. getRefreshKey
override fun getRefreshKey(state: PagingState<Int, Documents>): Int? {
return state.anchorPosition?.let { position ->
state.closestPageToPosition(position)?.prevKey?.plus(1)
?: state.closestPageToPosition(position)?.nextKey?.minus(1)
}
}
RecyclerView에서 Refresh 하거나, 데이터 업데이트 등을 현재 목록을 대체할 새 데이터를 로드할때 사용합니다.
즉, Paging이 필요할때 Key 값을 정의 하는 거죠.
특히 PagingState의 anchorPosition이 중요합니다.
public val anchorPosition: Int?,
PagingSource에서 achorPosition은 위와 같이 정의 되어 있습니다.
그리고 주석을 해석해 보면
플레이스홀더를 포함하여 목록에서 가장 최근에 액세스한 인덱스입니다.
페이징 데이터에 아직 액세스하지 않은 경우 null입니다.
예를 들어, 이 스냅샷이 첫 번째 로드 전 또는 로드 중에 생성된 경우.
라고 되어 있네요. 처음 정의 하였을땐 null 을 리턴한다고 되어 있습니다.
public fun closestPageToPosition(anchorPosition: Int): Page<Key, Value>? {
if (pages.all { it.data.isEmpty() }) return null
anchorPositionToPagedIndices(anchorPosition) { pageIndex, index ->
return when {
index < 0 -> pages.first()
else -> pages[pageIndex]
}
}
}
closestPageToPosition 함수 입니다. position 값들을 받고 anchorPositionToPagedIndeices 함수를 활용하여 값들을 리턴하고 있네요.
internal inline fun <T> anchorPositionToPagedIndices(
anchorPosition: Int,
block: (pageIndex: Int, index: Int) -> T
): T {
var pageIndex = 0
var index = anchorPosition - leadingPlaceholderCount
while (pageIndex < pages.lastIndex && index > pages[pageIndex].data.lastIndex) {
index -= pages[pageIndex].data.size
pageIndex++
}
return block(pageIndex, index)
}
각 파라미터인 pageIndex와 index는 위와 같이 정의 되어 있습니다.
pages는 PagingState가 생성자로 받고 있으며 자료형은 Page를 담는 list입니다.
이렇게 정의한 index들을 담고 콜백을 실행 합니다.
즉, anchorPosition을 사용하여 list에서 마지막으로 로드한 인덱스에 가장 가까운 로드된 페이지를 가져오는 함수 입니다.
그리고 페이지가 로드한게 비어있으면 null리턴 하구요.
그 다음은 실질적으로 load를 담당하는 load함수 입니다.
override suspend fun load(params: LoadParams<Int>): LoadResult<Int, Documents> {
val page = params.key ?: 1
return try {
val response = networkAPI.request()
LoadResult.Page(
data = response,
prevKey = if (page == 1) null else page - 1,
nextKey = if (response.isEmpty()) null else page + 1
)
} catch (e: HttpException) {
e.printStackTrace()
LoadResult.Error(e)
} catch (e: Exception) {
e.printStackTrace()
LoadResult.Error(e)
}
}
load 함수는 사용자가 스크롤 할 때마다 데이터를 비동기 적으로 가져옵니다.
파라미터인 LoadParams 객체는 로드 작업과 관련된 정보를 가지고 있습니다.
params가 override 함으로써 정의된 키를 알 수 있습니다.
null 이라면 최초이기 때문에 1로 정의하여 request하고 있습니다. 아까 전에도 말씀 드렸다 시피 Network, Database등 필요한 api를 호출 하면 됩니다.
PagingSource에 LoadResult.Page를 리턴 합니다.
LoadResult.Page 는 로드된 데이터를 담는 data, prev는 이전 키, nextKey는 다음 키 를 정의하면 됩니다.
키는 제가 작성한 코드를 한번 더 참고 부탁드립니다.
이 까지의 과정을 그림으로 표현한것이
위 과정입니다.
즉, getRefreshKey()를 재정의 함으로써 생성된 key 값을 load의 LoadParams 객체에게 전달 되는 것이죠.