[Android] Paging3 사용해보기
어플을 사용하다 보면 리스트로 나열된 데이터들을 자주 볼 것이다.
이런 데이터들은 보통 DB나 네트워크 통신을 통해 내용을 불러와 리스트에 띄워주는 형식으로 만들어진다.
게시판이 대표적이라고 할 수 있다.
게시판의 원리에 대해 간단하게 설명하면, 게시판에서 스크롤을 내리면 데이터들이 순서대로 불러와진다.
Rest API로 통신을 하고 있으면 JSON 데이터를 넣을텐데 보통은 페이지 번호도 제공해서 데이터를 끊어서 보여주게 된다.
그래서 게시판을 모바일에서 만들 때 다양한 방법이 있는데 제일 쉬운 방법은 버튼을 누르면 다음 페이지를 보여주는 방법이 있다
하지만 이 방법은 다음 데이터를 보기 위해서는 버튼을 눌러야 하는 불편함이 생긴다.
그래서 사용하는 방법이 RecyclerView Scroll Event이다.
사용자가 스크롤을 하면 해당 위치에 맞게 데이터를 동적으로 로드하거나, 뷰의 상태를 변경할 수 있다. 하지만 이 방법은 목록의 전체 데이터를 한 번에 가져오기 때문에, 대량의 데이터를 다룰 때는 성능 문제가 발생할 가능성이 있다.
다음은 Paging 라이브러리이다.
페이징은 RecyclerView 스크롤 이벤트와는 달리, 일정한 크기의 페이지 단위로 데이터를 로드하고 보여준다. 이를 통해 페이지 단위로 데이터를 처리하므로, 대규모 데이터를 처리할 때 성능 문제를 해결할 수 있다.
Paging3?
안드로이드의 Paging 3는 RecyclerView를 사용하는 앱에서 데이터를 페이징 처리하기 위한 라이브러리다. Paging 3는 이전 버전인 Paging 2와 달리 LiveData나 RxJava와 같은 리액티브 라이브러리를 지원하며, 페이징 데이터를 캐시로 관리하기 위한 빌트인 캐시 기능도 제공한다.
페이징 라이브러리의 장점은 다음과 같다.
- 페이징된 데이터의 메모리 내 캐싱. 이렇게 하면 앱이 페이징 데이터로 작업하는 동안 시스템 리소스를 효율적으로 사용할 수 있습니다.
- 요청 중복 제거 기능이 기본으로 제공되어 앱에서 네트워크 대역폭과 시스템 리소스를 효율적으로 사용할 수 있습니다.
- 사용자가 로드된 데이터의 끝까지 스크롤할 때 구성 가능한 RecyclerView 어댑터가 자동으로 데이터를 요청합니다.
- Kotlin 코루틴 및 Flow뿐만 아니라 LiveData 및 RxJava를 최고 수준으로 지원합니다.
- 새로고침 및 재시도 기능을 포함하여 오류 처리를 기본으로 지원합니다.
페이징 라이브러리 구조
Repository
PagingSource - 데이터 소스와 데이터를 검색하는 방법을 정의한다.
RemoteMediator - 로컬 데이터베이스 캐시가 있는 네트워크 데이터 소스와 같은 계층화된 데이터 소스의 페이징을 처리한다.
둘 중에 하나만 사용하면 되고, 기본은 PagingSource이다.
만약 이미 로드된 데이터를 캐시에 저장해서 네트워크 효율을 높이고 싶으면 RemoteMediator를 사용해서 제작하면 된다.
ViewModel
Pager - PagingSource를 바탕으로 PagingData를 구성하기 위한 API이다.
Paging Data - ViewModel 레이어를 UI에 연결하는 요소이다.
UI
PagingDataAdapter - 페이지로 나눈 데이터를 처리하는 RecyclerView 어댑터이다.
페이징 라이브러리 구현
PagingSource
class PagingSource(private val retrofitInterface: RetrofitInterface, private val query: String) : PagingSource<Int, Items>() {
override fun getRefreshKey(state: PagingState<Int, Items>): Int? {
return state.anchorPosition
}
//페이징
override suspend fun load(params: LoadParams<Int>): LoadResult<Int, Items> {
val page = params.key ?: 1
val response = retrofitInterface.getBook(query, page) //데이터 요청
val items = response.items
val prevKey = if (page == 1) null else page - 10 //이전 페이지
val nextKey = if (items.isEmpty()) null else page + 10 //다음 페이지
return LoadResult.Page(
data = items,
prevKey = prevKey,
nextKey = nextKey
)
}
}
여기서 레트로핏과 통신을 해서 데이터를 요청하고 Rest API 파라미터에 담겨있는 페이지 정보를 가져와 데이터를 로드해주는 기능을 한다.
RemoteMediator를 사용해 데이터를 캐시에 저장해도 되지만 Room DB를 익혀야 할 수 있기에 여기선 PagingSource를 사용했다.
얼른 Room DB 배우고 올게요
ViewModel
class MainActivityViewModel : ViewModel() {
private val retrofitInterface: RetrofitInterface = RetrofitInterface.create()
//페이징 데이터 스트림 설정
fun getSearchData(query: String) = Pager(
PagingConfig(pageSize = 10)
) {
PagingSource(retrofitInterface, query)
}.flow.cachedIn(viewModelScope) //데이터 스트림 CoroutineScope 사용
}
ViewModel에서 PagingSource를 불러와 PagingData를 제작하고 Flow에 담아 데이터를 액티비티로 보내주는 역할을 하게 된다.
Activity
class MainActivity : AppCompatActivity() {
...
//검색
private fun searchAction() {
val searchText = binding.searchEdt.text.toString()
if (searchText.isNotEmpty()) {
lifecycleScope.launch() {
viewModel.getSearchData(searchText).collect {
recyclerViewAdapter.submitData(it) //아이템 어댑터 전달
}
}
}
}
...
}
액티비티에서 ViewModel의 PagingData를 Flow를 통해 가져오고 어댑터로 아이템을 전달해준다.
PagingDataAdapter
class RecyclerViewAdapter(private val context: Context) :
PagingDataAdapter<Items, RecyclerViewAdapter.ViewHolder>(ItemDiffCallback) {
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ViewHolder {
val binding = RecyclerviewLayoutBinding.inflate(LayoutInflater.from(parent.context), parent, false)
return ViewHolder(binding)
}
override fun onBindViewHolder(holder: RecyclerViewAdapter.ViewHolder, position: Int) {
holder.bind(getItem(position))
}
inner class ViewHolder(private val binding: RecyclerviewLayoutBinding) : RecyclerView.ViewHolder(binding.root) {
fun bind(item: Items?) {
item?.let {
Glide.with(itemView).load(item.image).into(binding.image)
binding.title.text = item.title
binding.pubdate.text = item.pubdate
binding.discount.text = item.discount
binding.root.setOnClickListener {
val intent = Intent(Intent.ACTION_VIEW, Uri.parse(item.link))
context.startActivity(intent)
}
}
}
}
object ItemDiffCallback : DiffUtil.ItemCallback<Items>() {
override fun areItemsTheSame(oldItem: Items, newItem: Items): Boolean {
return oldItem.link == newItem.link
}
override fun areContentsTheSame(oldItem: Items, newItem: Items): Boolean {
return oldItem == newItem
}
}
}
RecyclerViewAdapter랑 똑같이 생겼지만 PagingDataAdapter를 상속받는게 다르다.
여기서 액티비티에서 보낸 PagingData를 받아와 RecyclerView에 뿌려주는 역할을 하게 된다.
위처럼 코드를 짜면 아래와 같은 결과를 얻게 된다.
게시판 제작에 앞서 미리 페이징을 구현해봤다.
저번 프로젝트에서는 스크롤 이벤트로 구현해서 로딩도 느리고 어색했지만 이번엔 페이징을 사용해 게시판을 구현할 생각이다.