Paging 3 적용하기

김흰돌·2023년 7월 18일
1

RecyclerView

목록 보기
4/4

Paging 3 라이브러리 개요

Paging 3 라이브러리를 사용하면 로컬 저장소에서나 네트워크를 통해 대규모 데이터 세트의 데이터 페이지를 로드 하고 표시할 수 있습니다.

이 방식을 사용하면 앱에서 네트워크 대역폭과 시스템 리소스를 모두 더 효율적으로 사용할 수 있습니다.

Paging 3 라이브러리의 구성요소는 권장 Android 앱 아키텍처에 맞게 설계 되었으며 다른 Jetpack 구성요소와 원활하게 통합되고 최고 수준으로 Kotlin을 지원합니다.


Paging 3 라이브러리를 사용하여 얻을 수 있는 이점

  • 페이징된 데이터의 메모리 내 캐싱. 이렇게 하면 앱이 페이징 데이터로 작업하는 동안 시스템 리소스를 효율적으로 사용할 수 있습니다.
  • 요청 중복 제거 기능이 기본으로 제공되어 앱에서 네트워크 대역폭과 시스템 리소스를 효율적으로 사용할 수 있습니다.
  • 사용자가 로드 된 데이터의 끝까지 스크롤할 때 구성 가능한 RecyclerView 어댑터가 자동으로 데이터를 요청합니다.
  • Kotlin 코루틴 및 Flow뿐만 아니라 LiveData 및 RxJava를 최고 수준으로 지원합니다.
  • 새로고침 및 재시도 기능을 포함하여 오류 처리를 기본으로 지원합니다.

라이브러리 아키텍처

라이브러리의 구성요소는 앱의 세 가지 레이어에서 작동합니다.

  • 저장소 레이어
  • ViewModel 레이어
  • UI 레이어

저장소 레이어

저장소 레이어의 기본 페이징 라이브러리 구성요소는 PagingSource입니다.

각 PagingSource 객체는 데이터 소스와 이 소스에서 데이터를 검색하는 방법을 정의합니다.

PagingSource 객체는 네트워크 소스 및 로컬 데이터베이스를 포함한 단일 소스에서 데이터를 로드할 수 있습니다.

사용할 수 있는 다른 페이징 라이브러리 구성요소는 RemoteMediator입니다. RemoteMediator 객체는 로컬 데이터베이스 캐시가 있는 네트워크 데이터 소스와 같은 계층화된 데이터 소스의 페이징을 처리합니다.

ViewModel 레이어

Pager 구성요소는 PagingSource 객체 및 PagingConfig 구성 객체를 바탕으로 반응형 스트림에 노출되는 PagingData 인스턴스를 구성하기 위한 공개 API를 제공합니다.

ViewModel 레이어를 UI에 연결하는 구성요소는 PagingData입니다. PagingData 객체는 페이지로 나눈 데이터의 스냅샷을 보유하는 컨테이너입니다.

PagingSource 객체를 쿼리하여 결과를 저장합니다.

UI 레이어

UI 레이어의 기본 페이징 라이브러리 구성요소는 페이지로 나눈 데이터를 처리하는 RecyclerView 어댑터인 PagingDataAdapter입니다.


PagingSource 작성

class PagingSource @Inject constructor(
    private val service: HospitalService,
    private val hospitalName: String,
    private val latitude: Double,
    private val longitude: Double
) : PagingSource<Int, Item>() {
    override suspend fun load(params: LoadParams<Int>): LoadResult<Int, Item> {
        val page = params.key ?: 1
        return try {
            val response = service.getHospitalInfo(
                serviceKey = BuildConfig.API_KEY,
                hospitalName = hospitalName,
                pageNo = page.toString(),
                latitude = latitude.toString(),
                longitude = longitude.toString(),
                radius = "5000"
            )

            val items = response.body?.items?.itemList
            LoadResult.Page(
                data = items!!,
                prevKey = if (page == 0) null else page - 1,
                nextKey = page + 1
            )
        } catch (e: Exception) {
            LoadResult.Error(e)
        }
    }
    override fun getRefreshKey(state: PagingState<Int, Item>): Int? {
        return state.anchorPosition?.let {
            state.closestPageToPosition(it)?.prevKey
        }
    }
}

네트워크 또는 데이터베이스에서 페이징 데이터를 로드하는 추상 클래스입니다.

LoadParams 객체에는 실행할 로드 작업에 관한 정보가 포함됩니다. 여기에는 로드할 키와 로드할 항목 수가 포함됩니다.

LoadResult 객체에는 로드 작업의 결과가 포함됩니다. LoadResult는 load() 호출이 성공했는지 여부에 따라 두 가지 형식 중 하나를 취하는 봉인 클래스입니다.

  • 로드에 성공하면 LoadResult.page 객체를 반환합니다.
  • 로드에 실패하면 LoadResult.Error 객체를 반환합니다.

Repository에서 PagingSource 사용

class HospitalInfoRepositoryImpl @Inject constructor(
    private val hospitalService: HospitalService
) : HospitalInfoRepository {

    override suspend fun getHospitalInfo(hospitalName: String, latitude: Double, longitude: Double): Flow<PagingData<Item>> {
        return Pager(PagingConfig(pageSize = PAGE_SIZE)) {
            PagingSource(hospitalService, hospitalName, latitude, longitude)
        }.flow
    }

    companion object {
        private const val PAGE_SIZE = 20
    }
}

Pager는 PagingSource 객체 및 PagingConfig 객체를 바탕으로 반응형 스트립을 생성합니다.

PagingConfig 함수는 데이터를 페이징하는 데 필요한 구성 옵션을 설정하는 데 사용됩니다.

  1. pageSize: 한 페이지에 로드 할 항목 수를 지정합니다. 예를 들어, PageSize를 20으로 설정하면 한 번에 20개의 항목이 로드됩니다.

  2. initialLoadSize: 초기 페이지 로드 시 로드 할 항목 수를 지정합니다. 일반적으로 이 값은 pageSize와 동일하거나 더 크게 설정됩니다.

  3. prefetchDistance: 현재 페이지에서 미리 로드 해야 할 추가 항목 수를 지정합니다. 사용자가 페이지를 스크롤할 때 미리 로드 된 항목이 표시되기 전에 로드 됩니다. 이를 통해 부드러운 스크롤 경험을 제공할 수 있습니다.

  4. enablePlaceholders: 데이터를 로드 할 때 비어 있는 항목의 표시 여부를 설정합니다. 이 값을 true로 설정하면 아직 로드 되지 않은 항목의 플레이스 홀더가 표시되고, 로드 되는 동안 해당 항목에 대한 데이터가 업데이트 됩니다.

PagingConfig 함수는 이러한 옵션을 설정하여 페이징 라이브러리가 데이터를 효율적으로 로드하고 관리할 수 있도록 도와줍니다.

Repository에서 PagingSource 클래스에 인자를 넘겨주고 Flow 방식으로 데이터를 받습니다.

ViewModel에서 Repository를 이용한 데이터 조회

@HiltViewModel
class HomeViewModel @Inject constructor(
    private val hospitalInfoRepository: HospitalInfoRepositoryImpl
) : ViewModel() {

    suspend fun getHospitalInfo(
        hospitalName: String,
        latitude: Double,
        longitude: Double
    ): Flow<PagingData<Item>> {
        return hospitalInfoRepository.getHospitalInfo(hospitalName, latitude, longitude)
            .cachedIn(viewModelScope)
    }
}

.cachedIn(viewModelScope)은 페이징 데이터를 메모리에 캐시하여 중복된 로드를 방지하는 데 사용됩니다.

ViewModel의 생명주기에 따라 캐시 된 데이터가 관리됩니다.

Activity / Fragment에서 데이터 받기

lifecycleScope.launch {
    mainViewModel.getHospitalInfo(
        binding.editTextSearch.text.toString(),
        latitude,
        longitude
    ).collect() { pagingData ->
        adapter.submitData(lifecycle, pagingData)
    }
}

코루틴을 실행시키기 위해 lifecycleScope.lauch 블록 내에서 getHospital 함수를 실행시킵니다.

collect() 함수를 사용하여 Flow에서 pagingData를 수집합니다.

submitData 함수를 호출하여 수집 된 pagingData를 어댑터에 보냅니다. 이를 통해 RecyclerView는 새로운 데이터를 화면에 표시하고 페이징 기능을 지원합니다.

PagingDataAdapter

package com.example.lifesemantics.ui.home

import android.view.LayoutInflater
import android.view.ViewGroup
import androidx.paging.PagingDataAdapter
import androidx.recyclerview.widget.DiffUtil
import androidx.recyclerview.widget.RecyclerView
import com.example.lifesemantics.data.entity.Item
import com.example.lifesemantics.databinding.ItemBinding
import com.example.lifesemantics.listener.ItemClickListener

class RecyclerViewAdapter() : PagingDataAdapter<Item, RecyclerViewAdapter.MyViewHolder>(diffUtil) {

    class MyViewHolder(private val binding: ItemBinding) :  RecyclerView.ViewHolder(binding.root) {
        fun bind(item: Item) {
            binding.data = item
        }
    }

    override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): MyViewHolder {
        val binding: ItemBinding = ItemBinding.inflate(LayoutInflater.from(parent.context), parent, false)
        return MyViewHolder(binding)
    }

    override fun onBindViewHolder(holder: MyViewHolder, position: Int) {
        val currentItem = getItem(position)
        if (currentItem != null) {
            holder.bind(currentItem)
        }
    }

    companion object {

        val diffUtil = object : DiffUtil.ItemCallback<Item>() {

            override fun areItemsTheSame(
                oldItem: Item,
                newItem: Item
            ): Boolean {
                return (oldItem.addr == newItem.addr)
            }

            override fun areContentsTheSame(
                oldItem: Item,
                newItem: Item
            ): Boolean {
                return oldItem == newItem
            }

        }
    }
}

PagingDataAdapter를 상속받습니다. PagingDataAdapter는 페이징 된 데이터를 처리하고 RecyclerView에 바인딩하는 데 도움을 주는 기능을 제공합니다.

diffUtil은 PagingDataAdpater에서 사용되는 DiffUtil.ItemCallback의 구현체입니다. diffUtil은 두 항목이 동일한지 확인하는 데 사용됩니다.

출처 : https://developer.android.com/topic/libraries/architecture/paging/v3-overview?hl=ko

1개의 댓글

comment-user-thumbnail
2023년 7월 18일

아주 유익한 내용이네요!

답글 달기