plzy의 개발 블로그

[Android] paging 라이브러리 어떻게 사용할까? 본문

Android

[Android] paging 라이브러리 어떻게 사용할까?

plzyhappy 2022. 5. 6. 20:33

기존 리사이클러뷰를 사용할 때 리사이클러뷰 어뎁터로 가져왔을 것이다.

데이터가 적다면 상관없지만 리사이클러뷰 아이템의 데이터가 1만 개, 10만 개 등등 커지면 어떻게 될까?

데이터를 불러오는데의 작업이 오래 걸리고 서버에 과부하가 될 수 있다.

이를 해결하기위해 Android에서는 paging이라는 라이브러리가 존재한다.

paging이란?

페이징이란 network나 database에 있는 데이터를 불러올 때 page대로 데이터를 분리해서 가져오는 것이다.
기존 데이터를 한꺼번에 가져왔다면 page대로 가져와 리소스 관리에 효율적이다.

Android에서는?

Android에서 flow, liveData로 비동기 작업까지 쉽게 할 수 있다.
또한 하나의 page의 데이터에 끝에 도달했을 때 자동으로 adapter에 요청해주므로 관리하기 쉽고
refresh, retry 등 오류가 났을 때의 처리를 쉽게 할 수 있도록 하는 함수가 있다.

어떻게 써야 할까?

Android Developer 공식문서에는 이렇게 아키텍처를 짜길 권장하고 있다.

아키텍쳐

먼저 build_gradle에 의존성을 추가해야 한다.

dependencies {
  val 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-alpha14")
}

자신의 입맛대로 삭제하거나 수정하면 된다.

 

그다음 PagingSource를 만들어야 한다.

class AlgorithmAdminPagingSource @Inject constructor(
    private val adminApi: AdminApi,
    private val token: String,
    private val status: String,


    ) : PagingSource<Int, Result>() {

    override suspend fun load(params: LoadParams<Int>): LoadResult<Int, Result> {
        return try {
            val page = params.key ?: 1

            val response = adminApi.getAlgorithmPage(token, 20, page, status)


            val data = response.data?.result
            val responseData = mutableListOf<Result>()
            data?.let { responseData.addAll(it) }

            val prevKey = if (page == 1) null else page - 1
            LoadResult.Page(
                data = responseData,
                prevKey = prevKey,
                nextKey = if (responseData.isEmpty()) null else page.plus(1),
            )


        } catch (e: Exception) {
            Log.d(TAG, "error: $e")
            return LoadResult.Error(e)
        } catch (e: HttpException) {
            Log.d(TAG, "HttpException: $e")
            return LoadResult.Error(e)
        } catch (e: IOException) {
            Log.d(TAG, "IOException: $e")
            return LoadResult.Error(e)
        }


    }


    override fun getRefreshKey(state: PagingState<Int, Result>): Int? {
        Log.d(TAG, "getRefreshKey:${state.anchorPosition} ")
        return state.anchorPosition?.let { anchorPosition ->
            state.closestPageToPosition(anchorPosition)?.prevKey?.plus(1)
                ?: state.closestPageToPosition(anchorPosition)?.nextKey?.minus(1)
        }

    }


}

먼저 Page에는 data, prevKey, nextKey 3가지가 있다.

 

data : 어떤 데이터를 가져올 것인지
prevKey : 이전 page 값을 어떻게 가져올 것인지
nextKey : 다음 page 값을 어떻게 가져올 것인지


이렇게 구성되어 있다.

위의 코드에는
prevKey는 만약 page가 1이라면 이전 값을 불러오지 않는다.이다
만약 이러한 조건문을 쓰지 않은다면 계속해서 데이터를 불러올 것이다.
nextKey는 만약 data가 비어있지 않다면 page를 추가해주는 것이다.

 

만약 이러한 조건문을 쓰지 않은다면 data가 비어있어도 데이터를 호출에 불필요한 리소스가 소요된다.

 

그다음 repository를 만들어야 한다.

class AdminRepositoryImpl @Inject constructor(
    private val dataSource: AdminDataSource,
    private val api: AdminApi
) : AdminRepository {
    override fun getAlgorithmPagingSource(
        token: String,
        status: String
    ): Flow<PagingData<ResultEntity>> {
        return Pager(config = PagingConfig(pageSize = 20),
            pagingSourceFactory = { AlgorithmAdminPagingSource(api, token, status) }
        ).flow.map { it ->
            it.map { it.toDomain() }
        }
    }

Pager를 만들 때 여러 가지 속성을 사용할 수 있는데, 위의 pageSize는 하나의 page당 몇 개의 data를 불러올 것인지 선언한 것이다.

 

그다음 viewModel를 만들어야 한다.

@HiltViewModel
class AdminViewModel @Inject constructor(

    private val algorithmAdminUseCase: AlgorithmAdminUseCase,
    private val statusUpdateUseCase: StatusUpdateUseCase

    ) : BaseViewModel() {

    fun getAlgorithm(token: String, status: String): Flow<PagingData<ResultEntity>> {
        return algorithmAdminUseCase.getAlgorithmPagingSource(token, status)
            .cachedIn(viewModelScope)
    }
}

cachedIn은 데이터 스트림을 공유하여 어떤 CoroutineScope를 사용할지 넣어준 것이다.
viewModel 안에서 쓰여서 viewModelScope를 넣어준다.

 

그다음으로는 pagingAdapter를 만들어줘야 한다.

class GroupListAdapter(
    val onClickListener: RecyclerViewItemClickListener<SearchGroupResponseItem>,
    val onLayoutClickListener: RecyclerViewItemClickListeners<SearchGroupResponseItem>
) :
    PagingDataAdapter<SearchGroupResponseItem, GroupListAdapter.PartnerRecyclerAdapterViewHolder>(
        diffCallback
    ) {
    companion object {

        private val diffCallback = object : DiffUtil.ItemCallback<SearchGroupResponseItem>() {
            override fun areItemsTheSame(
                oldItem: SearchGroupResponseItem,
                newItem: SearchGroupResponseItem
            ): Boolean {
                return oldItem == newItem
            }

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

    override fun onCreateViewHolder(
        parent: ViewGroup,
        viewType: Int
    ): PartnerRecyclerAdapterViewHolder {
        val layoutInflater = LayoutInflater.from(parent.context)

        val binding = DataBindingUtil.inflate<CommunityRecyclerViewItemBinding>(
            layoutInflater,
            R.layout.community_recycler_view_item,
            parent,
            false
        )

        return PartnerRecyclerAdapterViewHolder(binding)
    }


    override fun onBindViewHolder(holder: PartnerRecyclerAdapterViewHolder, position: Int) {
        val item = getItem(position)
        if (item != null) {
            holder.bind(item)

        }
    }


    inner class PartnerRecyclerAdapterViewHolder(val binding: CommunityRecyclerViewItemBinding) :
        RecyclerView.ViewHolder(binding.root) {

        fun bind(bind: SearchGroupResponseItem) {
            binding.data = bind
            binding.owner = bind.owner
            binding.executePendingBindings()
            binding.groupSetImg.setOnClickListener {
                onClickListener.onclick(bind)
            }
            itemView.setOnClickListener {
                onLayoutClickListener.onclicks(bind)
            }

        }
    }


}

기존의 recyclerView Adapter와 다르게 PagingAdapter를 상속해줘야 한다.

눈여겨봐야 할 것은 data를 binding 할 때 getItem으로 binding 해야 한다.

 

view에서 쓰일 때다.

    lifecycleScope.launch {
                    viewModel.getAlgorithm(authViewModel.getToken(), STATUS.ACCEPTED.toString())
                        .collectLatest {
                            adminAdapter.submitData(it)
                        }


                }

flow로 비동기 처리를 했으므로 lifecycleScope 안에서 만들어 줘야 하며
flow를 실행시키려면 collectLatest함수를 써야 한다.

마무리

소스코드를 함께 첨부했기 때문에 난잡해 보일 수 있지만 어떻게 쓰는지 대략 감만 잡으면 될 것 같다.
불러오는 데이터의 양에 따라 paging을 쓸지 안쓸지 결정하면 될 것 같다.

소스코드

https://github.com/joog-lim/bamboo-android

 

GitHub - joog-lim/bamboo-android: 🎋 광주 소프트웨어 마이스터 고등학교 전용 대나무 숲입니다.

🎋 광주 소프트웨어 마이스터 고등학교 전용 대나무 숲입니다. Contribute to joog-lim/bamboo-android development by creating an account on GitHub.

github.com

레퍼런스

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

 

페이징 라이브러리 개요  |  Android 개발자  |  Android Developers

페이징 라이브러리 개요   Android Jetpack의 구성요소 페이징 라이브러리를 사용하면 로컬 저장소에서나 네트워크를 통해 대규모 데이터 세트의 데이터 페이지를 로드하고 표시할 수 있습니다.

developer.android.com