Programming/Android

Android Jetpack Paging 3 적용하기

YK Choi 2021. 9. 3. 02:37

아래는 공식문서의 Paging 3 개요 일부이다.

 

`Repository` 에서는 PagingSource와 RemoteMediator 둘 중 하나를 쓰면 된다고 이해했다. 이중에서 나는 PagingSource를 사용했다. 하지만 만약 네트워크 연결이 불안정하거나 오프라인 상태에서도 데이터를 로드하기 위해서라면 RemoteMediator가 적절하다.

 

`ViewModel`에서는 Pager() 를 통해 PagingSource 객체를 생성하고 이를 UI(Activity등) 에서 반응할 수 있게 한다.

'PagingDataAdapter`는 RecyclerView에서의 Adapter + ListAdapter 를 상상하면 된다.

 

 

1. PagingSource 코드는 아래와 같다.

load()에서는 네트워크 통신을 포함한다.

Adapter.refresh() 를 했을 때/ 무효화 되었을때 getRefreshKey() 가 호출되는데 다시 시작할 key를 반환해주면 된다.

class BoardPagingSource(private val boardType: Int, private val boardService: BoardService) : PagingSource<Int, BoardInfo>() {
    override suspend fun load(params: LoadParams<Int>): LoadResult<Int, BoardInfo> {
        return try{
            val nextPage = params.key ?: 0
            val response = when(boardType){
                1 -> boardService.getFreeBoard(nextPage).body()
                2 -> boardService.getInfoBoard(nextPage).body()
                3 -> boardService.getEmployeeBoard(nextPage).body()
                else -> null
            }
            val data = response?.result as List<BoardInfo>

            /**
             * 서버에서 한 페이지당20개씩이 아닌, LIMIT을 20으로 두고 OFFSET을 매개변수 nextPage로 받게 해두어서 nextPage + 20을 하게 했다.
             * android 공식 문서에는 nextPage는 0부터 시작하고, API 호출이 끝나면 +1을 한다.
             */
            LoadResult.Page(
                    data = data,
                    prevKey = null,
                    nextKey = if(data.isEmpty()) null else nextPage + 20
            )
        } catch (e: Exception) {
            LoadResult.Error(Throwable("Board Paging Error"))
        }
    }

    override fun getRefreshKey(state: PagingState<Int, BoardInfo>): Int? { //refresh할 때 자동으로 호출된다.
        return state.anchorPosition?.let { anchorPosition ->
            val anchorPage = state.closestPageToPosition(anchorPosition)
            anchorPage?.prevKey?.plus(20) ?: anchorPage?.nextKey?.minus(20)
        }
    }
}

 여기서 20 이라는 숫자는 상황에 따라 1로 생각하고 보는것이 맞을 수도 있다.(offset이 아니라 page 단위로 세는 경우)

서버에 api를 요청할 때 보려는 페이지를 매개변수로 전달해야하는데, 나는 OFFSET을 매개변수로 주었기 때문에 위와 같이 20이 들어간 것이다.

 

2. ViewModel의 코드는 아래와 같다.

class BoardViewModel(private val repository:BoardRepository) : ViewModel(){
    val boardFlow = Pager(PagingConfig(pageSize = 20)) {
        BoardPagingSource(boardType, repository.boardService)
    }.flow.cachedIn(viewModelScope)
}

ViewModel에서 이렇게 작성하고 Activity단에서는 아래와 같이 boardFlow를 observe한다.

 

lifecycleScope.launch {
    boardViewModel.boardFlow.collectLatest { pagingData ->
        boardAdapter.submitData(pagingData)
    }
}

 

3. 위의 boardAdapter.submitData() 한 데이터는 UI로 보여주기 위해 아래와 같이 PagingDataAdapter를 만들어야한다.

class BoardPagingAdapter(private val itemClick: (BoardInfo) -> Unit) : PagingDataAdapter<BoardInfo, BoardPagingViewHolder>(BoardDiffCallback) {

    override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): BoardPagingViewHolder {
        val binding: ItemBoardListBinding = DataBindingUtil.inflate(LayoutInflater.from(parent.context), R.layout.item_board_list, parent,false)
        return BoardPagingViewHolder(binding, itemClick)
    }

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

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

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

 이 부분은 ListAdapter와 너무나 비슷하므로 추가 설명은 생략하겠다.

 

 

Paging 써보니까 느낀 장점

 

1. 코드수가 상당히 줄었다. (특히 ViewModel쪽에서)

- RecyclerView의 아이탬 데이터를 관리하는 것을 PagingSource의 별도 클래스에서 담당해주고, 다음 페이지 load 를 Adapter나 Activity에서 신경쓰지 않아도 된다. 

- 이전에는 ViewModel의 LiveData에 데이터를 넣었었는데 이부분이 생략되면서 코드 수가 더욱 줄었다.

 

2. Pager의 PagingConfig에서 커스텀을 손쉽게 할 수 있다..

- prefetchDistance로 스크롤 어디서 부터 다음 페이지를 로드할 지, 메모리에 올라갈 수 있는 최대 아이탬 개수 등을 PagingConfig에서 인자를 통해 커스텀할 수 있다.

 

3. 1번과 같은 맥락으로 데이터 처리에 대한 클래스를 별도로 만들었기 때문에 가독성이 높아진다.

 

그 외에 메모리 캐싱, 효율적인 시스템 리소스, 디폴트 오류처리 등이 있다고 한다.

 

내가 적용해본 Paging3의 기능들은 극히 일부의 기능들인것 같고, 앞으로도 더 써보고 이슈들을 만나보면서 최적화할 예정이다. 

 

 

참고한 자료

https://developer.android.com/topic/libraries/architecture/paging/v3-paged-data?hl=ko 

 

페이징된 데이터 로드 및 표시  |  Android 개발자  |  Android Developers

페이징된 데이터 로드 및 표시 Paging 라이브러리는 대규모 데이터 세트에서 페이징된 데이터를 로드하고 표시하는 강력한 기능을 제공합니다. 이 가이드에서는 Paging 라이브러리를 사용하여 네

developer.android.com

https://two22.tistory.com/5

 

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

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

two22.tistory.com