Paging3
페이징은 데이터를 가져올 때 한 번에 모든 데이터를 가져오는 것이 아니라 일정한 페이지로 나눠서 가져오는 것을 뜻합니다.
이러한 페이징 방식을 사용하면 앱에서 네트워크 대역폭과 시스템 리소스를 더 효율적으로 사용하기에 성능, 메모리, 비용 측면에서 굉장히 효율적입니다.
Android Jetpack에서는 페이징을 위한 Paging3 라이브러리를 제공합니다.
Paging3는 로컬 저장소 또는 네트워크를 통해 데이터를 나누어 효율적으로 로딩할 수 있게 도와줍니다.
Paging3는 구글에서 권장하는 Android 앱 아키텍처에 맞게 설계되었으며, 다른 Jetpack 컴포넌트와 잘 동작할 수 있도록 설계되었습니다.
Paging3의 장점
Paging3 라이브러리에는 다음과 같이 다양한 기능이 존재합니다.
- 페이징 된 데이터의 메모리 내 캐싱을 통해 앱이 페이징 데이터로 작업하는 동안 시스템 리소스를 효율적으로 사용할 수 있습니다.
- 요청 중복 제거 기능을 통해 앱에서 네트워크 대역폭과 시스템 리소스를 효율적으로 사용할 수 있습니다.
- 사용자가 로드된 데이터의 끝까지 스크롤할 때 구성 가능한 RecyclerView 어댑터가 자동으로 데이터를 요청합니다.
- Kotlin 코루틴 및 Flow 뿐만 아니라 LiveData 및 RxJava를 최고 수준으로 지원합니다.
- 새로고침 및 재시도 기능을 포함하여 오류 처리를 기본으로 지원합니다.
Paging3의 아키텍처
Paging3 라이브러리의 구성요소는 앱의 세 가지 레이어에서 작동합니다.
Repository 레이어
Repository 레이어의 기본이 되는 구성요소는 PagingSource입니다.
각 PagingSource 객체는 데이터 소스와 해당 소스에서 데이터를 검색하는 방법을 정의합니다.
PagingSource 객체는 네트워크 소스 및 로컬 데이터베이스를 포함하여 전체 데이터로부터 부분적으로 단일 데이터를 로드할 수 있습니다.
또 다른 구성요소로는 RemoteMediator가 있습니다.
RemoteMediator 객체는 네트워크로부터 받은 데이터를 로컬 데이터베이스를 통해 캐시하는 경우 페이징하는데 함께 사용할 수 있습니다.
ViewModel 레이어
ViewModel 레이어의 기본이 되는 구성요소는 Pager입니다.
Pager는 PagingSource 객체 및 PagingConfig 구성 객체를 바탕으로 반응형 스트림에서 사용되는 PagingData 인스턴스를 구성하기 위한 공개 API를 제공합니다.
이 때, ViewModel 레이어를 UI에 연결하는 구성요소는 PagingData입니다.
PagingData 객체는 페이징 된 데이터의 스냅샷을 보유하는 컨테이너로, PagingSource 객체를 쿼리하여 결과를 저장합니다.
UI 레이어
UI 레이어의 기본이 되는 구성요소는 PagingDataAdapter입니다.
PagingDataAdapter는 페이지로 나눈 데이터를 처리하는 RecyclerView 어댑터입니다.
PagingDataAdapter 대신 AsyncPagingDataDiffer 구성요소를 사용하여 고유한 커스텀 어댑터를 사용할 수 있습니다.
Paging3 구현하기
의존성 추가
Paging3를 사용하기 위해서 build.gradle에 Paging3 라이브러리를 추가합니다.
implementation "androidx.paging:paging-runtime:3.2.1"
PagingSource
추상 클래스인 PagingSource를 상속받아 PagingSource을 구현합니다.
load() 메서드에서는 params를 바탕으로 페이지의 데이터를 반환합니다.
class MainPagingSource(
private val mainService: MainService
) : PagingSource<Int, ListItem>() {
override suspend fun load(params: LoadParams<Int>): LoadResult<Int, ListItem> {
return try {
val page = params.key ?: 1
val size = params.loadSize
val result = mainService.getList(page, size).data
LoadResult.Page(
data = result.list,
prevKey = null,
nextKey = result.page.nextPage
)
} catch (e: Exception) {
LoadResult.Error(e)
}
}
override fun getRefreshKey(state: PagingState<Int, ListItem>): Int {
return 0
}
}
Repository
Repository에서는 Pager와 PagingSource를 사용하여 PagingData로 반환해줍니다.
PagingConfig로 Pager의 기본 설정을 정의해준 뒤 Pager 객체를 생성합니다.
Pager를 생성한 후 Flow, Observable, LiveData 등의 형태로 반환해줍니다.
class MainRepository @Inject constructor(
private val mainService: MainService
) {
fun loadList(): Flow<PagingData<ListItem>> {
return Pager(
config = PagingConfig(
pageSize = 20,
enablePlaceholders = false
),
pagingSourceFactory = {
MainPagingSource(mainService)
}
).flow
}
}
ViewModel
ViewModel에서는 Repository에서 PagingData를 가져옵니다.
cachedIn(viewModelScope)를 사용하여 캐싱을 해줄 수 있습니다.
@HiltViewModel
class MainViewModel @Inject constructor(
private val mainRepository: MainRepository
) : ViewModel() {
private val _pagingData = MutableStateFlow<PagingData<ListItem>?>(null)
val pagingData: StateFlow<PagingData<ListItem>?> = _pagingData
init {
getList()
}
private fun getList() {
viewModelScope.launch {
mainRepository.loadList()
.cachedIn(this)
.collectLatest { list ->
_pagingData.value = list
}
}
}
}
PagingDataAdapter
PagingDataAdapter는 기존에 RecyclerView.Adapter를 구현했던 것과 유사하게 구현해주면 됩니다.
abstract class BindingViewHolder<VB : ViewDataBinding>(
private val binding: VB,
) : RecyclerView.ViewHolder(binding.root) {
protected var item: ListItem? = null
open fun bind(item: ListItem) {
this.item = item
binding.setVariable(BR.item, this.item)
}
}
class PageListAdapter :
PagingDataAdapter<ListItem, BindingViewHolder<*>>(DiffCallback()) {
override fun getItemViewType(position: Int): Int {
val item = getItem(position)
return item?.viewType?.ordinal ?: -1
}
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): BindingViewHolder<*> {
return ViewHolderGenerator.get(parent, viewType)
}
override fun onBindViewHolder(holder: BindingViewHolder<*>, position: Int) {
val item = getItem(position)
if (item != null) {
holder.bind(item)
}
}
}
MainActivity
MainActivity에서 PagingDataAdapter의 submitData()을 사용하여 데이터를 처리합니다.
이 때, submitData()는 suspend 함수이므로 코루틴을 사용해야 합니다.
@AndroidEntryPoint
class MainActivity : AppCompatActivity() {
private lateinit var binding: ActivityMainBinding
private val viewModel: MainViewModel by viewModels()
private val adapter by lazy { PageListAdapter() }
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
binding = ActivityMainBinding.inflate(layoutInflater).apply {
setContentView(root)
recyclerView.adapter = adapter
}
observeViewModel()
}
private fun observeViewModel() {
lifecycleScope.launch {
viewModel.pagingData.collectLatest {
if (it != null) {
adapter.submitData(lifecycle, it)
}
}
}
}
}
'안드로이드 > 활용' 카테고리의 다른 글
[Android] Hilt를 통한 의존성 주입 (0) | 2023.11.21 |
---|---|
[Android] Coil 사용하기 (0) | 2023.10.24 |
[Android] CountDownTimer 사용하기 (0) | 2023.10.11 |
[Android] SMS Retriever API를 통해 SMS 자동으로 읽어오기 (0) | 2023.10.11 |
[Android] BindingAdapter 사용하기 (0) | 2023.10.09 |