ANDROID/Android Jetpack

[Android/Jetpack] AAC - Paging

주 녕 2022. 1. 19. 22:41
728x90

https://developer.android.com/

Android Jetpack의 AAC는 총 6가지인데, 그중 마지막인 Paing에 대한 포스팅이다.

MVVM 아키텍처를 공부하고 실제로 작은 프로젝트들에 적용하면서 익히다보니 상대적으로 Paging의 중요성을 느끼지 못하고 공부를 미뤄두고 있었다. 곧 필요하게 될 날이 생기게 될 것 같아 미리 공부하려고 한다! 😊

 

Paging

페이징(Paging) : 데이터베이스의 데이터를 일정한 덩어리로 나누어 제공하는 것

→ Android에서는 스크롤을 이용해서 데이터를 점진적으로 불러오는 무한 스크롤링 기법으로 사용할 수 있다!

페이징 라이브러리를 사용하면 로컬 저장소에서나 네트워크를 통해 대규모 데이터 세트의 데이터 페이지를 로드하고 표시할 수 있습니다. 이 방식을 사용하면 앱에서 네트워크 대역폭과 시스템 리소스를 모두 더 효율적으로 사용할 수 있습니다. 페이징 라이브러리의 구성요소는 권장 Android 앱 아키텍처에 맞게 설계되었으며 다른 Jetpack 구성요소와 원활하게 통합되고 최고 수준으로 Kotlin을 지원합니다.

안정적인 최신 버전의 Paging 라이브러리인 Paging3를 사용합니다.

 

Paging 라이브러리를 사용했을 때의 장점

  • 네트워크 대역폭/시스템 리소스의 효율적인 사용
    • 페이징 된 데이터의 메모리 내 캐싱
    • 요청 중복 제거 기능이 기본으로 제공
  • 사용자가 로드된 데이터의 끝까지 스크롤할 대 구성 가능한 RecyclerView Adapter가 자동으로 데이터를 요청
  • Kotlin Coroutine, RxJava, Flow, LiveData를 최고 수준으로 지원
  • 새로고침 및 재시도 기능을 포함한 오류 처리 지원

 


 

Paging 라이브러리 아키텍처

 

라이브러리의 컴포넌트는 앱의 3가지 레이어에서 작동한다.

: Repository 레이어, ViewModel 레이어, UI 레이어

 

Repository 레이어

  • PagingSource : (기본) 페이징 라이브러리 구성요소
    • 각 객체는 데이터 소스와 이 소스에서 데이터를 검색하는 방법 정의
    • 이 객체는 네트워크 소스 및 로컬 데이터베이스를 포함한 단일 소스에서 데이터를 로드할 수 있음
  • RemoteMediator : 페이징 라이브러리 구성요소
    • 이 객체는 로컬 데이터베이스 캐시가 있는 네트워크 데이터 소스와 같은 계층화된 페이징 데이터를 로드
    • 이 방법이 더 안정적이며 오류 발생 가능성이 적음

ViewModel 레이어

  • Pager : ViewModel 레이어의 페이징 라이브러리 구성요소
    • PagingSource/PagingConfig 객체를 바탕으로 반응형 스트림에 노출되는 PagingData 인스턴스를 구성하기 위한 public API를 제공
    • Pager를 통해 Flow, Observable, LiveData 형태로 변환함
    • PagingConfig : PagingSource를 구성하는 방법 정의
  • PagingData : ViewModel 레이어를 UI 레이어에 연결하는 구성요소
    • 이 객체는 페이지로 나눈 데이터의 스냅샷을 보유하는 컨테이너
    • PagingSource 객체를 쿼리하여 결과를 저장

UI 레이어

  • PagingDataAdapter : UI 레이어의 기본 페이지 라이브러리 구성요소
    • 페이지로 나눈 데이터를 처리하는 Recyclerview 어뎁터
  • AsyncPagingDataDiffer : 고유한 맞춤 어댑터를 빌드할 수 있는 페이지 라이브러리 구성요소

 

다시 정리해보자면,

Repository 레이어에서 데이터를 로드해서 받아오고

ViewModel 레이어에서 받아온 데이터를 PagingData 객체로 구성하고

UI 레이어에서 이 변환한 데이터를 페이지 단위로 보여주는 것이라고 이해했다👀

그럼 이어서 어떻게 무한 스크롤을 구현할 수 있는지 공식문서를 통해 알아보자.

 

Paging 데이터 로드 및 표시

1. 데이터 소스 정의

데이터 소스를 식별하기 위해서 PagingSource 구현을 정의해야 함

상응하는 데이터 소스에서 페이징된 데이터를 검색하는 방법을 나타내기 위해 load() 메서드를 재정의해야 함

 

1-1. key-value 타입 선택

  • PagingSource<Key, Value> 
    • Key → 데이터를 로드하는데 사용되는 식별자
    • Value → 데이터 자체의 유형

ex) Int형의 페이지 번호를 Retrofit에 전달하여 네트워크에서 User 객체의 페이지를 로드한다면 

→ Key type : Int , Value type : User

 

1-2. Paging Source 정의

class ExamplePagingSource(
    val backend: ExampleBackendService, val query: String) : PagingSource<Int, User>() {
  override suspend fun load(params: LoadParams<Int>): LoadResult<Int, User> {
    try {
      val nextPageNumber = params.key ?: 1 // 정의되지 않았다면 페이지 1에서 refresh 시작
      val response = backend.searchUsers(query, nextPageNumber)
      return LoadResult.Page(
        data = response.users,
        prevKey = null, // Only paging forward.
        nextKey = response.nextPageNumber
      )
    } catch (e: Exception) {
      // 이 블록의 오류를 처리하고 LoadResult 반환
      // 여기서 오류는 expected error (네트워크 장애)
    }
  }

  override fun getRefreshKey(state: PagingState<Int, User>): Int? {
    // prevKey 또는 nextKey에서 anchorPosition에 가장 가까운 페이지의 페이지 키를 찾자
    // 하지만 여기서 nullability를 처리해야 함
    //  * prevKey == null -> anchorPage is the first page.
    //  * nextKey == null -> anchorPage is the last page.
    //  * both prevKey and nextKey null -> anchorPage is the initial page, so just return null.
    return state.anchorPosition?.let { anchorPosition ->
      val anchorPage = state.closestPageToPosition(anchorPosition)
      anchorPage?.prevKey?.plus(1) ?: anchorPage?.nextKey?.minus(1)
    }
  }
}
  • PagingSource 클래스의 매개변수 : backend, query
    • backend : 데이터를 제공하는 백엔드 서비스의 인스턴스
    • query : backend 서비스에 전송할 쿼리문(검색어)
    • 이 매개변수들을 load() 메서드에 전달하여 쿼리에 적절한 데이터를 로드
  • LoadParams 객체 : 실행할 로드 작업에 대한 정보 & 로드 작업의 결과
    • load할 key & 로드할 항목 수를 포함
    • load() 메서드 호출이 성공했는지에 따라 두가지 형태 중 하나를 취하는 봉인 클래스
      • 로드에 성공 → LoadResult.Page 객체 반환
      • 로드에 실패 → LoadResult.Error 객체 반환
  • getRefreshKey() 메서드*
    • PagingState 객체를 매개변수로 하여 데이터의 첫 로드 후 새로고침/무효화되었을 때, key를 반환하여 load()로 전달
    • Paging 라이브러리는 다음 데이터를 새로고침할 때 자동으로 이 메서드를 호출함

 

1-3. 오류 처리

데이터 로드 request는 특히 네트워크를 통해 로드하는 경우 여러 가지 이유에 의해 실패할 수 있음

== load() 메서드에서 LoadResult.Error 객체를 반환하여 발생한 오류

catch (e: IOException) {
  // IOException for network failures.
  return LoadResult.Error(e)
} catch (e: HttpException) {
  // HttpException for any non-2xx HTTP status codes.
  return LoadResult.Error(e)
}

PagingSource는 개발자가 조취를 취할 수 있도록  LoadResult.Error 객체를 수집하여 UI에 제공

 

로드 상태 관리 및 표시하기  |  Android 개발자  |  Android Developers

Paging 라이브러리를 사용하여 페이징된 데이터의 로드 상태를 추적하고 표시하는 방법을 알아봅니다.

developer.android.com

 

2. PagingData 스트림 설정

PagingSource에 의해 페이징된 데이터의 스트림이 필요함 (일반적으로 ViewModel에서 데이터 스트림을 설정)

val flow = Pager(
  // PagingConfig에 prefetchDistance와 같은 추가 속성을 전달하여
  // 데이터를 로드하는 방법을 구성함
  PagingConfig(pageSize = 20)
) {
  ExamplePagingSource(backend, query)
}.flow
  .cachedIn(viewModelScope)
  • Pager 클래스
    • PagingSource에서 PagingData 객체의 반응형 스트림을 노출하는 메서드 제공
    • Paging 라이브러리는 Flow, LiveData 등의 여러 스트림 유형을 사용할 수 있도록 지원
    • 이 객체를 생성하여 반응형 스트림을 설정할 때는 PagingConfig 객체, PagingSource 인스턴스를 어떻게 가져올지를 Pager에 알려주는 함수를 제공해야 함
  • cachedIn() 메서드
    • 데이터 스트림을 공유가능하게 하고, 제공된 CouroutineScope를 사용하여 로드된 데이터를 캐시함
    • 해당 예시에서는 ViewModelScope을 사용함

→ Pager 객체는 PagingSource 객체에서 load()를 호출하여 LoadParams 객체를 제공하고, 반환되는 LoadResult 객체 수신

 

3. RecyclerView Adapter 정의

Paging 라이브러리는 데이터를 RecyclerView 목록에 수신하는 Adapter인 PagingDataAdpater 클래스를 제공함

PagingDataAdapter를 확장하는 클래스를 정의해서 사용함

 

아래 예시에서는 UserApdater가 PagingDataAdapter를 확장하여 사용함

→ User의 리스트 항목에 RecyclerView Adapter를 제공하고 UserViewHolder를 뷰홀더로 사용함

class UserAdapter(diffCallback: DiffUtil.ItemCallback<User>) : PagingDataAdapter<User, UserViewHolder>(diffCallback) {
  override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): UserViewHolder {
    return UserViewHolder(parent)
  }

  override fun onBindViewHolder(holder: UserViewHolder, position: Int) {
    val item = getItem(position)
    // item이 null일 수 있음
    // ViewHolder는 null 항목을 placeholder로 바인딩하는 기능을 지원해야 함
    holder.bind(item)
  }
  
  object UserComparator : DiffUtil.ItemCallback<User>() {
      override fun areItemsTheSame(oldItem: User, newItem: User): Boolean {
        // Id is unique.
        return oldItem.id == newItem.id
      }

      override fun areContentsTheSame(oldItem: User, newItem: User): Boolean {
        return oldItem == newItem
      }
    }
}
  • onCreateViewHolder(), onBinidViewHolder() 메서드를 정의하여 사용함
  • DiffUtil을 사용하므로 DiffUtil.ItemCallback을 지정해야 함

DiffUtil에 대한 지식은 아래 포스팅에 정리해두었다

 

[Android/Jetpack] Recyclerview Adpater 대신 ListAdapter 적용하기

Room + LiveData를 이용한 MVVM 패턴 실습을 진행하던 중, RecyclerView의 Adapter에 ListAdapter를 적용하는 예제를 참고하게 되어 보다 자세히 알아보기 위해 포스팅 하게 되었습니다! ListAdpater는 구글 I/O..

junyoung-developer.tistory.com

 

4. UI에 페이징된 데이터 표시

  1. PagingDataAdapter 클래스의 인스턴스 생성
  2. 페이징된 데이터를 표시할 RecyclerView 목록에 PagingDataAdpater 인스턴스를 전달
  3. PagingData 스트림을 확인하고 생성된 각 값을 어댑터의 submitData() 메서드에 전달

→ RecyclerView 목록에 데이터 소스에서 페이징된 데이터가 표시되고, 필요한 경우에 자동으로 다른 페이지가 로드됨

val viewModel by viewModels<ExampleViewModel>()

val pagingAdapter = UserAdapter(UserComparator)
val recyclerView = findViewById<RecyclerView>(R.id.recycler_view)
recyclerView.adapter = pagingAdapter

// Activity는 lifecycleScope을 바로 사용할 수 있지만
// Fragment는 viewLifecycleOwner.lifecycleScope를 대신 사용해야 함
lifecycleScope.launch {
  viewModel.flow.collectLatest { pagingData ->
    pagingAdapter.submitData(pagingData)
  }
}

 


reference >

728x90