글을 시작하기 전에 Paging에 대해 간단히 설명해보겠습니다.
Paging 라이브러리를 사용하면 로컬 저장소에서나 네트워크를 통해 대규모 데이터 세트의 데이터 페이지를 로드하고 표시할 수 있습니다. 또한 앱에서 네트워크 대역폭과 시스템 리소스를 모두 더 효율적으로 사용할 수 있습니다.
위와 같이 어떠한 장점이 있다라는 글을 보시면서 이해하는 것보다 하나의 예시를 들면 글을 읽으시는 분들께서 더 이해가 쉽게 되실거라고 생각하기 때문에 예시를 하나 들어보겠습니다.
Paging의 경우 RecyclerView와의 연동이 굉장히 잘되어있습니다. Paging을 통해 API에서 가져온 데이터를 바로 RecyclerView에 넣을 수 있도록 PagingDataAdapter라는 확장 클래스가 존재합니다. 그렇기에 공식 문서를 보시면 다음과 같이
class UserAdapter(diffCallback: DiffUtil.ItemCallback<User>) :
PagingDataAdapter<User, UserViewHolder>(diffCallback) {
override fun onCreateViewHolder(
parent: ViewGroup,
viewType: Int
): UserViewHolder {
return UserViewHolder(parent)
}
override fun onBindViewHolder(holder: UserViewHolder, position: Int) {
val item = getItem(position)
// Note that item may be null. ViewHolder must support binding a
// null item as a placeholder.
holder.bind(item)
}
}
데이터를 RecyclerView에 집어넣을 수 있으면 다음과 같은 동작 또한 가능합니다. 화면에 과일 종류를 받아와 RecyclerView에 담아준다고 가정하겠습니다. 이때 화면에 사이즈에 나올만큼의 데이터를 받아와 View에 담아주면 데이터의 요청을 잠시 멈춥니다. 그 후 스크롤을 통해 다음 데이터가 나와야 한다면 그때 다시 통신을 해서 데이터를 가져와줍니다. 다음과 같이 동작하게 된다면 계속해서 데이터를 가지고 올 필요가 없기때문에 resource를 아끼게 되는 것입니다. (물론 다른 방식으로도 가능하겠지만 Paging을 이용한다면 간단하게 구현가능)
Paging에 대해 글을 작성하게 된 이유로는 제가 취업을 위해 여러 회사에 지원을 하였고 그 중 한 회사(지금 재직 중인 회사 ㅎㅎ)에서 받은 과제를 작성 중에 Paging3을 이용하였는데 그 경험이 좋았어서 글로 작성하게 오래 기억하기 위함 입니다.
과제를 설명하자면 API에서 한번에 특정 갯수를 호출해오는 동작을 여러 번 해줘 가지고 온 데이터를 처리하는 그러한 과제 였습니다.
이 특정 갯수를 여러번 가져오는 과정을 저는 Paging으로 구현하였습니다.
Paging의 Page의 값을 증가시키며 Retrofit 통신을 반복할 것이기에 PagingSource를 생성해줍니다.
PagingSource는 네트워크 또는 데이터베이스에서 페이징 데이터를 로드하는 추상 클래스입니다. 이를 구현하려면 페이지 Key 타입을 정의해야 하고 데이터를 검색하는 방법을 정의하는 클래스입니다.
PagingSource에는 구현해주어야 하는 메서드가 2개가 있는데 하나는 load() 메서드이고 다른 하나는 getRefreshKey 입니다. load()메서드 안에서는 데이터를 받아오는 함수를 수행해줍니다. 그 값을 response에 담아줍니다. 이때 Retrofit call 대신에 Response를 사용하여 비동기적으로 데이터를 가져와 줍니다. response의 값을 가져오는 것이 성공적이라면 해당 데이터를 처리해주면 됩니다.
}
override suspend fun load(params: LoadParams<Int>): LoadResult<Int, 데이터 엔티티> {
val page = params.key ?:PAGE_START_INDEX
val apiKey = Key.Decoding
return try {
val response = callApi.getData(apiKey,page)
if (response.isSuccessful) {
// 가져온 데이터를 통해 어떠한 동작을 수행
}
LoadResult.Page(
data = appDao.getRangeRecords()
prevKey = if(page == 1) null else page.minus(1),
nextKey = if(page == 10) null else page.plus(1)
)
} catch (exception: IOException){
return LoadResult.Error(exception)
} catch (exception: HttpException){
return LoadResult.Error(exception)
}
}
다음과 같이 사용하여 주면 됩니다. return인 LoadResult는 Page, Error 두 가지가 있습니다. LoadResult.Page의 경우 정상적인 동작을 수행했을 때의 다음 동작을 정의해주고 Error의 경우 Exception이 발생된다면 해줄 동작을 정의해줍니다.
override fun getRefreshKey(state: PagingState<Int, 데이터 엔티티>): Int? {
return state.anchorPosition?.let { anchorPosition ->
state.closestPageToPosition(anchorPosition)?.nextKey?.minus(1)
?: state.closestPageToPosition(anchorPosition)?.prevKey?.plus(1)
}
getRefreshKey의 경우 PagingState를 인자로 받는데 PagingState는 로드된 페이지 및 마지막으로 액세스 한 위치 등의 Paging 시스템의 스냅샷 상태를 가지고 있습니다. Paging이 동작하던 중 중단이 되었고 다시 재개를 원한다면 getRefresh에서 재실행을 해주면 됩니다.
과제에 대해 자세히 작성은 못하지만... (해당 부분은 허락 받고 작성했습니다!! ㅎㅎ) 과제에서 해당 기능을 어떻게 구현하지라는 생각을 굉장히 많이 했던 그런 추억이 있네요 이번 글은 여기까지 하도록 하겠습니다!!
읽어주셔서 감사합니다.