Paging3

홍승범·2023년 4월 22일
0

Android

목록 보기
7/9

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

페이징 라이브러리의 이점

  • 페이징된 데이터의 메모리 캐싱 지원
  • 요청 중복 제거 기능이 기본으로 제공됨
  • RecyclerView의 어댑터를 통해 스크롤의 끝에 도달할 때 자동으로 데이터를 요청함
  • 코루틴, Flow, Rx, LiveData를 지원
  • 새로고침 및 재시동 기능을 지원

build.gradle 설정

dependencies {
  def paging_version = "3.1.1"

  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-alpha18"
}

라이브러리 아키텍쳐

총 3개의 계층으로 구성된다

  1. 레포지토리
    기본 구성요소는 PagingSource로서 이 객체는 데이터 소스와 이 소스에서 데이터를 검색하는 방법을 정의 한다. 단일 소스에서 데이터를 로드하는 책임을 가진다
    RemoteMediator는 로컬 데이터베이스에 캐시가 있는 네트워크 데이터 소스와 같은 계층화된 데이터소스의 페이징을 처리한다.

  2. ViewModel레이어
    PagerPagingSourcePagingConfig구성 객체를 바탕으로 반응형 스트림에 노출되는 PagingData인스턴스를 구성하기 위한 공개 api를 제공하는 역할을 한다
    PagingData는 페이지로 나눈 데이터의 스냅샷을 보유하는 컨테이너로서 PagingSource객체를 쿼리하여 결과를 나타냄

  3. UI 레이어
    PagingDataAdapter를 통해 페이지로 나눈 데이터를 처리할 수 있음

데이터로드 및 표시

여기서는 TMDB API 를 통해 영화 검색 결과를 표시하는 동작으로 코드를 구현한다.

PagingSource 의 정의.

먼저 페이지를 나타낼 키와, 데이터형식을 정의한다.
영화검색 를 참조하면, 해당 api는 페이지를 나타내는 값이 있으므로 사용하기 적당하다.

PagingSource를 아래와 같이 구현한다.

class SearchPagingSource(val service: TMDBService, private val queryString: String): PagingSource<Int, Movie>() {
    override fun getRefreshKey(state: PagingState<Int, Movie>): Int? {
        return state.anchorPosition?.let { anchorPosition ->
            val anchorPage = state.closestPageToPosition(anchorPosition)
            anchorPage?.prevKey?.plus(1) ?: anchorPage?.nextKey?.minus(1)
        }
    }
    override suspend fun load(params: LoadParams<Int>): LoadResult<Int, Movie> {
        try {
            val nextPageNumber = params.key ?: 1
            val response = service.service.searchMovie(queryString, nextPageNumber)
            val prevKey = if (nextPageNumber == 1) {
                null
            } else {
                nextPageNumber - 1
            }
            val nextKey = if (response.totalPages <= response.page) {
                null
            } else {
                nextPageNumber + 1
            }
            return LoadResult.Page(
                data = response.results,
                prevKey = prevKey, nextKey = nextKey
            )
        } catch (e: Exception) {
            return LoadResult.Error(e)
        }
    }
}

load() 메서드는 데이터를 로드하는 역할을 하며, 로드를 위해 필요한 키와 항목의 수가 LoadParams객체를 통해 전달된다. 키가 없는경우 첫번째임을 의미하고, 키값 및 쿼리 결과에 따라 이전 키 다음 키를 정의해서 리턴할 수 있다.

load()의 반환값인 LoadResult는 로드 작업의 결과물이 포함되며, 성공시. LoadResult.Page를 실패시 LoadResult.Error를 반환한다.

다음 이미지는 load() 함수가 각 로드시의 키를 수신 및 후속 로드용 키를 제공하는 방법을 보여준다.

PagingSource의 구현은 getRefreshKey()메서드도 반드시 구현해야함. 이 메서드는 데이터가 첫 로드 후 새로고침되거나, 무효화 되었을 때 키를 반환하여 load()로 전달하고, 페이징 라이브러리는 다음에 데이터를 새로고침할 때 자동으로 이 메서드를 호출함.

데이터 스트림 구현

로드된 데이터를 이용하기 위해서는 페이징된 데이터의 스트림이 필요하다. 일반적으로 ViewModel에서 스트림을 설정한다

Pager클래스는 PagingConfig를 기반으로 PagingSourceFactory 메서드를 호출하여 PagingSource의 구현체를 생성하고, PagingData객체의 반응형 스트림을 노출 할 수 있도록 한다.

	var bindingQuery: String = ""
    // paging flow
    val flow = Pager(PagingConfig(pageSize = 10, prefetchDistance = 50)) {
        SearchPagingSource(TMDBService, bindingQuery)
    }.flow.map {
        data ->
        data.map { movie ->
            if (this::config.isInitialized) {
                movie.posterPath?.let {
                    movie.copy(posterPath = config.getImageUri(it, ImageCategory.POSTER, ImageSizeType.IN_LIST).toString())
                } ?: movie
            } else {
                movie
            }
        }
    }.cachedIn(viewModelScope)

Pager클래스의 팩토리메소드를 통해 페이징 소스를 생성하고, 그 결과인 PagingData를 map 함수를 통해 변경하여 제공할 수 있다.

cachedIn() 연산자는 데이터스트림의 공유를 가능하게 하며, 제공된 코루틴스콥을 사용해 로드된 데이터를 캐시한다. 캐시된 데이터는 중복 호출의 염려 없이 사용될 수 있다는 것

UI를 위한 어댑터 정의

Paging 데이터를 다루기 위한 PagingDataAdapter클래스가 제공된다. DiffUtil을 사용하는 등 ListAdpater와 비슷하게 사용하면 된다.

object DiffUtils: DiffUtil.ItemCallback<Movie>() {
    override fun areItemsTheSame(oldItem: Movie, newItem: Movie): Boolean {
        return oldItem == newItem
    }

    override fun areContentsTheSame(oldItem: Movie, newItem: Movie): Boolean {
        return oldItem.id == newItem.id
    }
}

class SearchMovieAdapter: PagingDataAdapter<Movie, SearchMovieAdapter.ItemHolder>(DiffUtils) {
    inner class ItemHolder(val binding: SearchMovieItemBinding):ViewHolder(binding.root) {
        fun bind(movie: Movie) {
            with(binding) {
                title.text = movie.title
                desc.text = movie.overview
                Glide.with(this.movieThumbnail).load(Uri.parse(movie.posterPath ?: ""))
                    .placeholder(R.drawable.poster)
                    .fallback(R.drawable.poster)
                    .into(this.movieThumbnail)
            }
        }
    }

    override fun onBindViewHolder(holder: ItemHolder, position: Int) {
        holder.bind(getItem(position)!!)
    }

    override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ItemHolder {
        return ItemHolder(SearchMovieItemBinding.inflate(LayoutInflater.from(parent.context), parent, false))
    }
}

ListAdpater와의 차이점이 있다면, currentList로 접근불가능하고, 데이터를 제출할때는 submitList가 아닌 submitData를 사용해야 한다는점 정도.

주의: submitData() 메서드는 PagingSource가 무효화되거나 어댑터의 새로고침 메서드가 호출될 때까지 중단되며 반환하지 않습니다. 즉 submitData() 호출 이후의 코드가 의도한 것보다 훨씬 늦게 실행될 수 있습니다.

라고 한다

refresh 및 retry를 통해 데이터를 다시 로드할 수 있는데, refresh시 주의사항이 있다.
리프레시 후 로드를 위해서는 새로운 PagingSource 객체가 필요하다. 또는 invalidate 상태여야 한다. 이를 위해 invalidate후 생성해주는 InvalidatingPagingSourceFactory 도 제공을 해주긴하더라.

var bindingQuery: String = ""
    // paging flow
    val flow = Pager(PagingConfig(pageSize = 10, prefetchDistance = 50)) {
        SearchPagingSource(TMDBService, bindingQuery)
    }
위와 같이 팩토리 메소드에 매번 새로운 소스를 생성하는 코드를 추가한 것이 그때문이다. 이것땜에 삽질좀 했다....

https://developer.android.com/topic/libraries/architecture/paging/v3-overview?hl=ko
https://www.charlezz.com/?p=44562

https://stackoverflow.com/questions/66803759/refresh-in-paging-3-library-when-using-rxjava

profile
그냥 사람

0개의 댓글