Android

안드로이드 Paging 3

최데브 2022. 7. 24. 22:30

페이징이라는 개념은 예전부터 있었다.

페이징을 내 멋대로 설명 해보자면

대량의 데이터를 불러올때 한번에 다 불러오면 리소스 낭비가 심하다. 이를 처리하는 전략이
페이징이였는데 100개의 데이터가 있으면 이걸 다 불러오지말고 한 페이지당 10개씩 
다음 페이지가 보고 싶다고 하면 다음 페이지의 10개의 데이터를 보여줘라. 라는 개념이다.

 

그리고 안드로이드에서도 비슷한 니즈가 있었는데 스크롤을 내려서 데이터를 볼때

한번에 다 보여줄 필요없이 스크롤이 어느정도 내려왔을때 다음 데이터를 불러오면 좋지 않을까?

하는 점이다.

 

그리고 그걸 관리하기 쉽게 구현을 도와주는 라이브러리가 Paging3 다.

이게 최선인가에 대해서는 확실히 말하기 어렵지만 일단 내가 구현한 방법대로 설명을 해보려고 한다.

 

해당 예시는 MVVM 으로 작성되었다.

 

View에서는 일반적으로 리사이클러뷰를 선언하고 adapter 를 붙여서 사용했다.

여기서 Adapter를 확인해봐야하는데

class DailyPagingAdapter(private val callback: (DailyPostFeed) -> Unit):
    PagingDataAdapter<DailyPostFeed, DailyPagingAdapter.ImageViewHolder>(
        object : DiffUtil.ItemCallback<DailyPostFeed>() {
            override fun areItemsTheSame(oldItem: DailyPostFeed, newItem: DailyPostFeed): Boolean {
                return oldItem.normalPostId == newItem.normalPostId
            }

            override fun areContentsTheSame(oldItem: DailyPostFeed, newItem: DailyPostFeed): Boolean {
                return oldItem.normalPostId == newItem.normalPostId
            }

        }
    ) {
    override fun onBindViewHolder(holder: DailyPagingAdapter.ImageViewHolder, position: Int) {
        val item = getItem(position) ?: return
        holder.onBind(item)
    }

    override fun onCreateViewHolder(
        parent: ViewGroup,
        viewType: Int
    ): DailyPagingAdapter.ImageViewHolder {
        return ImageViewHolder(binding = ItemDailyfeedBinding.inflate(
            LayoutInflater.from(parent.context), parent, false)
        )
    }

    inner class ImageViewHolder(private val binding: ItemDailyfeedBinding):
        RecyclerView.ViewHolder(binding.root) {
        fun onBind(item: DailyPostFeed) {
     	
            binding.constraintLayout2.setOnClickListener {
                callback(item)
            }
        }
    }
}

ListAdapter 와 매우 흡사하지만 PagingDataAdapter 를 사용해서 구현해준다.

내부 구현은 다른 adapter 들과 크게 다르지 않다.

그러나 데이터에 값을 넣어줄때

pagingAdapter.submitData(it)

이렇게 submitData(it) 라는걸 사용한다.

그리고 저기는 it이라고 적혀있는데 이 곳에는 PagingData<Data> 라는 형태로 값이 들어가야한다.

PagingData 를 만드는 과정을 다음으로 알아보자.

 

Viewmodel 에서 호출한 usecase 에서는

class PagingRepoUseCase @Inject constructor
    (
    private val PagingRepository: PagingRepository
) {

    fun getDataPagingData() = flow {
        emit(UiState.Loding)
        runCatching {
            PagingRepository.getPagingData()
        }.onSuccess { result ->
            emit(UiState.Success(result))
        }.onFailure {
            emit(UiState.Error(it))
        }
    }
}

이렇게 작성해서 view에서 flow로 데이터를 처리하고 싶었기 때문에

flow에 담은채로 view로 값을 보내주도록 처리했다.

paging 에 대한 주요 내용은 아니므로 넘어가겠다.

 

일단 위 코드에 보이는 PagingRepository 의 구현체를 보자.

 

class RepositoryImp
@Inject constructor(private val dailyRemoteSource: RemoteSource) : PagingRepository {
    override suspend fun getPagingData(): Flow<PagingData<DailyPostFeed>> {
        return Pager(PagingConfig(pageSize = 10))
        { DailyPagingSourceImp(dailyRemoteSource)}.flow
    }
}

갑자기 Pager 가 나온다. 이게 뭘까

안을 한번 들여다보면 

public class Pager<Key : Any, Value : Any>
// Experimental usage is propagated to public API via constructor argument.
@ExperimentalPagingApi constructor(
    config: PagingConfig,
    initialKey: Key? = null,
    remoteMediator: RemoteMediator<Key, Value>?,
    pagingSourceFactory: () -> PagingSource<Key, Value>
) {
    // Experimental usage is internal, so opt-in is allowed here.
    @JvmOverloads
    @OptIn(ExperimentalPagingApi::class)
    public constructor(
        config: PagingConfig,
        initialKey: Key? = null,
        pagingSourceFactory: () -> PagingSource<Key, Value>
    ) : this(config, initialKey, null, pagingSourceFactory)

요런 코드가 나온다. PagingConfig 로 pageSize 를 10으로 준것처럼 페이징에 대해서 설정값을 넣어 줄 수 있고

핵심구현체인 pagingSourceFactory 을 고차함수로 만들어서 return을 PagingSoucre 라는 데이터로 만들어준다.

그럼 우리는 이 데이터를 .flow 를 적어주어 객체의 반응형 스트림을 노출하는 PagingData 객체로 만들어줄 수 있게 된다.

 

라고 공식문서에 아래와 같이 적혀있다.

그런 다음 PagingSource 구현에서 페이징된 데이터의 스트림이 필요합니다. 일반적으로 ViewModel에서 데이터 스트림을 설정해야 합니다. Pager 클래스는 PagingSource에서 PagingData 객체의 반응형 스트림을 노출하는 메서드를 제공합니다. Paging 라이브러리는 Flow, LiveData, RxJava의 Flowable 유형과 Observable 유형을 비롯한 여러 스트림 유형을 사용할 수 있도록 지원합니다.Pager 스트림을 만들어 반응형 스트림을 설정할 때는PagingConfig
구성 객체와PagingSource 구현 인스턴스를 가져오는 방법을Pager에 지시하는 함수를 인스턴스에 제공해야 합니다.

 

abstract class DailyPagingSource () : PagingSource<Int, DailyPostFeed>(){
    abstract override suspend fun load(params: LoadParams<Int>): LoadResult<Int, DailyPostFeed>
    abstract override fun getRefreshKey(state: PagingState<Int, DailyPostFeed>): Int?
}

위 코드는 핵심적으로 paging 처리에 필요한 load 와 getRefreshKey 메소드다.

아래 코드에 구현이 되어있는데 한번 봐보자.

class DailyPagingSourceImp @Inject constructor(private val dailyRemoteSource: DailyRemoteSource) :
    DailyPagingSource() {

    override suspend fun load(params: LoadParams<Int>): LoadResult<Int, DailyPostFeed> {
        return try {
            val next = params.key ?: 0
            val size = params.loadSize
            val response = dailyRemoteSource.dailyAllFeed(next, size)
            try {
                Log.v("ssssfsfsf2" ,response.toDomain().dailyFeedEntitiy.get(0).normalPostId)
            }catch (e : Exception){
                Log.v("ssssfsfsf3" ,e.stackTraceToString())
            }
            LoadResult.Page(
                data = response.toDomain().dailyFeedEntitiy,
                prevKey = if (next == 0) null else next - 1, nextKey = next + 1
            )
        } catch (e: Exception) {
            Log.v("ssssssssssssss" , e.message.toString())
            LoadResult.Error(e)
        }


    }

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

load 는 실제로 데이터를 어떻게 가져올지 정의하는 곳이다.

LoadParams 에 데이터를 어떻게 불러올지 저장하고 현재 가져올 Page의 key와 항목수가 포함된다.

로드에 성공하면 LoadResult.Page를 반환하고 실패하면 LoadResult.Error를 반환한다.

LoadResult.Page에는 데이터와 previousKey, nextKey로 생성하는데

previousKey는 Paging에서 정하기 때문에 null를 전달하고

next 현재 페이지 기준으로 다음 페이지의 Key값을 전달한다.

getRefreshKey 는 LoadParams에 PageKey를 전달할 때 사용하는 함수다.

previousKey가 null이면 첫번째 페이지를 반환하고 nextKey가 null이면 마지막 페이지를 반환한다.

만약 둘 다 null이면 null을 반환한다.

 

22-09-24 수정사항

페이징으로 보여주려는 화면에서 데이터가 적으면 마지막으로 불러온 데이터가 중복해서스크롤 할때마다 불려오는 오류가 있었다.이는 prevKey 와 nextKey 를 제대로 관리해주지 않아서인데백엔드에서 해당 페이지가 마지막 페이지라는걸 알려주는 데이터를 줘야한다.

 

위에 내가 작성한 LodeResult.Page 코드 부분이

            if(response.last){
                LoadResult.Page(
                    data = response.toDomain().commentEntitiy,
                    prevKey = null,
                    nextKey = null
                )
            }else{
                LoadResult.Page(
                    data = response.toDomain().commentEntitiy,
                    prevKey = null,
                    nextKey = next+1
                )
            }

현재는 이렇게 수정 됐다. response에 지금 요청하는 데이터가 DB에서 가지고 있는 마지막 데이터라는걸

api response 로 받아오고 있고 해당 값은 last 라는 변수를 통해 마지막 값이면 true 아니라면 false 로

받아온다.

 

마지막 페이지일때 prevKey 와 nextKey 를 null 로 바꿔주면 더이상 불러올 데이터가 없다면

페이징 요청을 하지 않게 된다.

반응형