아래는 공식문서의 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
'Programming > Android' 카테고리의 다른 글
2021 Droid Knights 컨퍼런스 후기 (0) | 2021.09.25 |
---|---|
Android Studio에서 Profiler 적용기 (0) | 2021.09.04 |
코루틴은 race condition이 발생하지 않는 것일까? (0) | 2021.06.16 |
[Android] Koin + Unit Test 하기 (0) | 2021.05.21 |
[Android] ViewPager2 + BottomNavigation 으로 Fragment 관리하기 (0) | 2021.05.16 |