[Android] Paging 3.0 Library 알아보기 - 1
Android

[Android] Paging 3.0 Library 알아보기 - 1

728x90

 

 

 

Paging이란?

 페이징이란 데이터를 가져올 때 한 번에 모든 데이터를 가져오는 것이 아니라 일정한 덩어리로 나눠서 가져오는 것을 뜻합니다. 예를 들어, 구글에서 어떤 키워드로 검색하게 되면 모든 데이터를 한 번에 가져오는 것이 아니라 10페이지씩 데이터를 가져오게 됩니다. 페이징을 사용하면 성능, 메모리, 비용 측면에서 굉장히 효율적입니다.

 

 

 

 

Jetpack Paging Library

 Android Jetpack에서는 페이징을 위한 Paging3 라이브러리를 제공합니다. Paging3 라이브러리는 로컬 저장소에서나 네트워크를 통해 데이터를 나누어 효율적으로 로딩할 수 있게 도와줍니다. Paging3는 구글에서 권장하는 Android 앱 아키텍처에 맞게 설계되었으며, 다른 Jetpack 컴포넌트와 잘 동작할 수 있도록 설계되었습니다.

 

 

 

 

Paging3의 장점

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

 

 

 

 

Paging3 아키텍처

Paging3 라이브러리는 총 3개의 layer로 구성됩니다.

  1. Repository layer
  2. ViewModel layer
  3. UI layer

 

Repository layer

 Repository layer의 페이징 라이브러리 구성요소는 PagingSource입니다. 각 PagingSource 객체는 데이터 소스와 이 소스에서 데이터를 검색하는 방법을 정의합니다. PagingSource 객체는 네트워크 소스 및 로컬 데이터베이스를 포함한 단일 소스에서 데이터를 로드할 수 있습니다.

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

 

 

ViewModel layer

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

 ViewModel 레이어를 UI에 연결하는 구성요소는 PagingData입니다. PagingData 객체는 페이징 된 데이터의 스냅샷을 보유하는 컨테이너입니다. PagingSource 객체를 쿼리 하여 결과를 저장합니다.

 

UI layer

 UI 레이어의 기본 페이징 라이브러리 구성요소는 페이지로 나눈 데이터를 처리하는 RecyclerView 어댑터인 PagingDataAdapter입니다. PagingDataAdapter 대신 AsyncPagingDataDiffer 구성요소를 사용하여 고유한 Custom Adapter를 사용할 수 있습니다.

 

 

 

 

Paging3 주요 클래스

 

PagingSource

 네트워크 또는 데이터베이스에서 페이징 데이터를 로드하는 추상 클래스입니다. 이를 구현하려면 페이지 key 타입을 정의해야 합니다. 데이터를 검색하는 방법을 정의하는 클래스입니다.

 PagingSource에서 로드된 데이터는 PagingData 인스턴스로 관리됩니다. PagingData는 데이터가 추가적으로 로드됨에 따라 커질 수 있습니다. 하지만 기존에 로드된 데이터는 갱신되지 않습니다. 

 

RemoteMediator

 네트워크 및 로컬 데이터베이스에서 페이징 데이터를 로드하는 역할이 있습니다. 로컬 데이터베이스를 데이터 소스로 활용하는 경우에 페이징을 구현하는 대표적인 좋은 방법입니다. 이 방법은 훨씬 더 안정적이며 오류 발생 가능성이 적습니다.

 RemoteMediator는 Pager의 생성자로 선택적으로 전달되어 다음과 같은 이벤트를 제어할 수 있습니다.

  • 스트림 초기화
  • UI에서 전달받는 LoadType.REFRESH 신호
  • PagingSource는 현재 데이터의 경계를 알려주는 신호인 PagingSource.LoadResult를 반환합니다. 예를 들어, 첫 번째 페이지에 도달하면 LoadType.PREPEND를 반환하고, 마지막 페이지에 도달하면 LoadType.APPEND를 반환합니다.

 

Pager

 PagingSource 객체 및 PagingConfig 객체를 바탕으로 반응형 스트림을 생성합니다. 각 PagingData는 페이징 된 데이터 스냅샷을 나타내며, Pager로부터 Flow, Observable, LiveData 형태를 반환합니다.

 

PagingConfig

 Pager 객체를 생성할 때 필수적으로 필요한 요소로써, 페이징을 구성하는 방법을 정의합니다. 페이징 하는 데이터의 크기, placeholder 사용 유무 등 PagingSource를 구성하는 방법을 정의합니다.

 

PagingData

 PagineData는 페이징 된 데이터를 담아두는 컨테이너입니다. PagingSource 객체를 쿼리 하여 결과를 저장하며, 최종적으로 반환되는 데이터는 일반적으로 UI layer의 PagingDataAdapter가 전달받게 됩니다.

 

PagingDataAdapter

 RecyclerView에 데이터를 표시하는 주된 UI 구성 요소입니다. PagingData를 입력받아 내부적으로 언제 데이터를 추가해야 할지 관찰하게 됩니다. PagingDataAdapter는 백그라운드 스레드에서 DiffUtil을 사용하여 데이터를 정제한 뒤에 데이터를 로드하기 때문에 UI 스레드에서 새로운 항목을 추가할 때 부드럽게 나타낼 수 있습니다.

 

 

 

 

Paging3 맛보기

의존성 추가하기

dependencies {
  def paging_version = "3.0.0"

  implementation "androidx.paging:paging-runtime:$paging_version"

  // alternatively - without Android dependencies for tests
  testImplementation "androidx.paging:paging-common:$paging_version"

  // optional - RxJava2 support
  implementation "androidx.paging:paging-rxjava2:$paging_version"

  // optional - RxJava3 support
  implementation "androidx.paging:paging-rxjava3:$paging_version"

  // optional - Guava ListenableFuture support
  implementation "androidx.paging:paging-guava:$paging_version"

  // optional - Jetpack Compose integration
  implementation "androidx.paging:paging-compose:1.0.0-alpha09"
}

 

PagingSource

class SamplePagingSource @Inject constructor(
    private val service: SampleBackendService
) : PagingSource<Int, String>() {

    override suspend fun load(params: LoadParams<Int>): LoadResult<Int, String> {
        return try {
            val next = params.key ?: 0
            val response = service.getPagingData(next)

            LoadResult.Page(
                data = response.data,
                prevKey = if (next == 0) null else next - 1,
                nextKey = next + 1
            )
        } catch (e: Exception) {
            LoadResult.Error(e)
        }
    }

    override fun getRefreshKey(state: PagingState<Int, String>): Int? {
        return state.anchorPosition?.let { anchorPosition ->
            state.closestPageToPosition(anchorPosition)?.prevKey?.plus(1)
                ?: state.closestPageToPosition(anchorPosition)?.nextKey?.minus(1)
        }
    }
}

 PagingSource를 구현하기 위해 추상 클래스인 PagingSource를 상속받아서 SamplePagingSource를 만들어줍니다. 구현해야 할 메서드는 load()와 getRefreshKey()가 있습니다. load()는 params을 바탕으로 페이지의 데이터를 반환하게 됩니다. getRefreshKey()는 refresh시 다시 시작할 key를 반환해주면 됩니다.

 load()의 인자로 넘어오는 params의 key 값이 페이지 정보입니다. 추가적으로 loadSize도 가지고 있습니다.

 return인 LoadResult는 Page, Error 두 가지가 있습니다. LoadResult.Page는 정상적인 경우일 때 사용하면 됩니다. 만약 다음 데이터를 더 이상 호출하지 않으려면 nextKey 값을 null로 주면 됩니다. LoadResult.Error는 Exception 발생이나 데이터가 문제가 있을 경우 사용하면 됩니다. 

 getRefreshKey()는 PagingState를 인자로 받습니다. PagingState는 로드된 페이지 및 마지막으로 액세스 한 위치 등의 페이징 시스템의 스냅샷 상태를 가지고 있습니다.

 

Repository

class PagingRepository @Inject constructor(
    private val service: SampleBackendService
) {

    fun getPagingData(): Flow<PagingData<String>> {
        return Pager(PagingConfig(pageSize = 10)) {
            SamplePagingSource(service)
        }.flow
    }
}

 Repository에서 Pager와 PagingSource를 사용하여 PagingData로 반환해줍니다. PagingConfig로 페이저의 기본 설정을 정의해준 뒤 Pager 객체를 생성합니다. 추가적으로 Pager 생성 시 초기 키 값을 지정해줄 수도 있습니다.

 Pager를 생성 후 Flow 형태로 반환해줍니다. 반환 형태는 Observable, LiveData로도 가능합니다.

 

ViewModel

@HiltViewModel
class PagingViewModel @Inject constructor(
    private val repository: PagingRepository
) : ViewModel() {

    val pagingData = repository.getPagingData().cachedIn(viewModelScope)
}

 Repository에서 PagingData를 가져옵니다. cachedIn(viewModelScope)를 사용하여 캐싱을 해줄 수 있습니다.

 

PagingDataAdapter

class PagingAdapter : PagingDataAdapter<String, PagingViewHolder>(diffCallback) {

    override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): PagingViewHolder {
        val layoutInflater = LayoutInflater.from(parent.context)
        return PagingViewHolder(
            ItemSampleBinding.inflate(layoutInflater, parent, false)
        )
    }

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

    companion object {
        private val diffCallback = object : DiffUtil.ItemCallback<String>() {
            override fun areItemsTheSame(oldItem: String, newItem: String): Boolean {
                return oldItem == newItem
            }

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

class PagingViewHolder(
    private val binding: ItemSampleBinding
): RecyclerView.ViewHolder(binding.root) {

    fun bind(value: String) {
        binding.textView.text = value
    }
}

 PagingDataAdapter는 기존 RecyclerView.Adapter를 구현했던 것과 유사하게 구현해주면 됩니다. 차이가 하나 있다면 diffUtil을 구현해줘야 합니다. 

 

MainActivity

@AndroidEntryPoint
class MainActivity : AppCompatActivity() {

    private val viewModel: PagingViewModel by viewModels()

    private lateinit var recyclerView: RecyclerView

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_main)

        recyclerView = findViewById(R.id.recyclerView)
        recyclerView.adapter = PagingAdapter()

        lifecycleScope.launch {
            viewModel.pagingData.collectLatest {
                (recyclerView.adapter as PagingAdapter).submitData(it)
            }
        }
    }
}

 Activity에서 PagingData를 PagingDataAdapter에 넣어줍니다. 한 가지 유의할 점이 있다면, PagingDataAdapter의 submitData()는 suspend 함수이기 때문에 코루틴을 사용하여 호출하여야 합니다.

 

 

 

 

끝으로

 Android Jetpack에서 제공하는 Paging3 라이브러리를 알아보았습니다. 기존의 Paging2와 비교해보면 코드가 좀 더 간단해지고 깔끔하게 구현이 가능해졌습니다. 이번 포스팅에서는 설명하지 않았지만 Paging3에는 Paging2에는 없는 기능들이 여러 가지 있습니다. 이와 관련한 내용은 다음 포스팅에서 이어서 하도록 하겠습니다. 이번 포스팅에서 사용된 예제 소스는 아래에서 확인하실 수 있습니다.

 

[Android] Paging 3.0 Library 알아보기 - 2

 이번 포스팅에는 이전 글에서 언급했었던 Paging2에 없던 Paging3.0만의 기능을 설명해보려고 합니다. Paging3.0의 개념과 기본적인 사용 방법은 이전 글을 참고해주세요. [Android] Paging 3.0 Library 알아

leveloper.tistory.com

 

 

 

 

예제 소스

https://github.com/tkdgusl94/blog-source/tree/master/paging3

 

tkdgusl94/blog-source

https://leveloper.tistory.com/ 에서 제공하는 예제 source. Contribute to tkdgusl94/blog-source development by creating an account on GitHub.

github.com

 

참고

https://two22.tistory.com/5

 

안드로이드 Paging 3.0 #1 - 맛보기

Paging 라이브러리란? 페이징을 쉽게 구현하기 위해 안드로이드에서 제공하는 라이브러리이다. 최근 3.0 Alpha 버전이 릴리즈 되었단 걸 알게 되었다. Paging2와 변한 점 DataSource 관련 코드가 PagingSource

two22.tistory.com

https://www.charlezz.com/?p=44562

 

안드로이드 Paging3 컴포넌트 정복하기 – Part1 (페이징 된 데이터를 로드하고 화면에 나타내기) |

안드로이드 Paging3 컴포넌트 정복하기 - Part1 Paging3는 Jetpack 라이브러리 중 하나로 다양한 데이터 소스로 부터 데이터를 나누어 효과적으로 로딩할 수 있게 한다. Paging3는 네트워크 또는 로컬 데이

www.charlezz.com

https://developer.android.com/topic/libraries/architecture/paging/v3-overview

 

페이징 라이브러리 개요  |  Android 개발자  |  Android Developers

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

developer.android.com

 

728x90