Paging3

매일 수정하는 GNOSS LV5·2021년 10월 25일
1

AndroidStudio

목록 보기
28/83

Room을 이용한 로컬 저장소나 API통신을 거친 네트워크의 대규모 데이터 세트를 로드하기 위해 사용한다.

예를 들면 무슨 네이버에서 무슨 검색어를 입력했을때 해당되는 모든 데이터를 가져오는 것이 아니라 10페이지씩 가져오게 됩니다.

Paging3의 장점

  • 페이징 된 데이터의 메모리 내 캐싱. 이렇게 하면 앱이 페이징 데이터로 작업하는 동안 시스템 리소스를 효율적으로 사용할 수 있습니다.

  • 요청 중복 제거 기능이 기본으로 제공되어 앱에서 네트워크 대역폭과 시스템 리소스를 효율적으로 사용할 수 있습니다.

  • 사용자가 로드된 데이터의 끝까지 스크롤할 때 구성 가능한 RecyclerView 어댑터가 자동으로 데이터를 요청합니다.

  • Kotlin 코루틴 및 Flow뿐만 아니라 LiveData 및 RxJava를 최고 수준으로 지원합니다.

  • 새로고침 및 재시도 기능을 포함하여 오류 처리를 기본으로 지원합니다.

Paging3 아키텍처

Paging3 라이브러리는 총 3개의 layer로 구성됩니다.

  1. Repository layer
  2. ViewModel layer
  3. UI layer

  1. Repository layer

구성요소는 PagingSource와 RemoteMediator입니다.

  • PagingSource
    데이터 소스와 데이터를 검색하는 방법을 정의한다.
    데이터를 가져옵니다. ( 예를들면 레트로핏에서 GET을 이용하여 Response값을 가지고 있습니다.)

-RemoteMediator
로컬 데이터베이스(예를들면 Room라이브러리 )와 함께 사용하여 캐시를 이용해 데이터 소스의 페이징을 처리 할 수 있습니다.

  1. ViewModel layer

Pager의 구성 요소는 PagingSource, PagingConfig입니다.
PagingSource는 객체를 바탕으로 PagingData를 구성합니다.
PagingConfig은 데이터를 받아들이는 방법을 정할수 있습니다.

  1. UI layer

Recyclerview의 어댑터와 동일한 역할을 합니다.
PagingDataAdapter와 AsyncPagingDataDiffer를 사용할 수 있스며 Custom Adapter도 사용할 수 있습니다.


Paging3 주요 클래스

PagingSource

네트워크 또는 데이터베이스에서 페이징 데이터를 로드하는 추상 클래스입니다. 이를 구현하려면 페이지 key 타입을 정의해야 합니다. 데이터를 검색하는 방법을 정의하는 클래스입니다.
주 역할은 key관리 입니다. data, prevkey, nextKey를 다루게 됩니다.
load 메서드를 통하여 data를 구현하고 nextKey를 통하여 cursor관리도 가능합니다.

RemoteMediator

네트워크 및 로컬 데이터베이스에서 페이징 데이터를 로드하는 역할이 있습니다. 로컬 데이터베이스를 데이터 소스로 활용하는 경우에 페이징을 구현하는 대표적인 좋은 방법입니다. 이 방법은 훨씬 더 안정적이며 오류 발생 가능성이 적습니다.

RemoteMediator는 Pager의 생성자로 선택적으로 전달되어 다음과 같은 이벤트를 제어할 수 있습니다.

  • 스트림 초기화
  • UI에서 전달받는 LoadType.REFRESH 신호
  • PagingSource는 현재 데이터의 경계를 알려주는 신호인 PagingSource.LoadResult를 반환합니다. 예를 들어, 첫 번째 페이지에 도달하면 LoadType.PREPEND를 반환하고, 마지막 페이지에 도달하면 LoadType.APPEND를 반환합니다.

Pager

PagingSource 객체 및 PagingConfig 객체를 바탕으로 flow를 생성합니다. 각 PagingData는 페이징 된 데이터 스냅샷을 나타내며, Pager로부터 Flow, Observable, LiveData 형태를 반환합니다.

PagingConfig

Pager 객체를 생성할 때 필수적으로 필요한 요소로써, 페이징을 구성하는 방법을 정의합니다. 페이징 하는 데이터의 크기, placeholder 사용 유무 등 PagingSource를 구성하는 방법을 정의합니다.

PagingData

PagineData는 페이징 된 데이터를 담아두는 컨테이너입니다. PagingSource 객체를 쿼리 하여 결과를 저장하며, 최종적으로 반환되는 데이터는 일반적으로 UI layer의 PagingDataAdapter가 전달받게 됩니다.

PagingDataAdapter

RecyclerView에 데이터를 표시하는 주된 UI 구성 요소입니다. PagingData를 입력받아 내부적으로 언제 데이터를 추가해야 할지 관찰하게 됩니다. PagingDataAdapter는 백그라운드 스레드에서 DiffUtil을 사용하여 데이터를 정제한 뒤에 데이터를 로드하기 때문에 UI 스레드에서 새로운 항목을 추가할 때 부드럽게 나타낼 수 있습니다.


예시 !!

PagingSource

class ScrapPagingSource(fragmentContext: Context) : PagingSource<String, ItemScrapMain>() {

    private val service by lazy {
        getService(fragmentContext)
    }

    var nextKey: String? = null
    override suspend fun load(params: LoadParams<String>): LoadResult<String, ItemScrapMain> {
        val position = params.key
        return try {
            val currentLoadingPageKey = params.key ?: "present"
            val response = service.getVideo(20, position)
            val data : List<ItemScrapMain> = ScrapMainRecyclerFactory().settingScrapItem(response, checkFirst)

            nextKey = if (response.body()?.nextCursor != null) {
                response.body()!!.nextCursor
            } else {
                null
            }
            val prevKey = if (currentLoadingPageKey == "present") {
                null
            } else {
                "previous"
            }
            return LoadResult.Page(
                data = data,
                prevKey = prevKey,
                nextKey = nextKey
            )
        } catch (e: Exception) {
            return LoadResult.Error(e)
        }
    }

    override fun getRefreshKey(state: PagingState<String, ItemScrapMain>): String? {
        return nextKey
    }
}

Repository

class ScrapRepository {

    fun getScrapResults(context: Context): Flow<PagingData<ItemScrapMain>> {
        return Pager(
            config = PagingConfig(
                pageSize = 20
            ),
            pagingSourceFactory = { ScrapPagingSource(context) }
        ).flow
    }
}

ViewModel

class ScrapViewModel(application: Application) : AndroidViewModel(application) {

    private var currentSearchResult: Flow<PagingData<ItemScrapMain>>? = null
    private val repository = ScrapRepository()
    fun searchRepo(context: Context): Flow<PagingData<ItemScrapMain>> {
        val newResult: Flow<PagingData<ItemScrapMain>> = repository.getScrapResults(context)
            .cachedIn(viewModelScope)
        currentSearchResult = newResult
        return newResult
    }
}

Adpater

class ScrapMainAdapter :
    PagingDataAdapter<ItemScrapMain, RecyclerView.ViewHolder>(diffCallback) {

    inner class ViewHolderClass(itemView: View) : RecyclerView.ViewHolder(itemView) {
        val scrapTitle: TextView = itemView.findViewById(R.id.scrapTitle)
        val scrapImage: ImageView = itemView.findViewById(R.id.scrapItemBackground)
        val glide = Glide.with((itemView).context)
    }

    inner class ViewHolderClassanother(itemView: View) : RecyclerView.ViewHolder(itemView)

    override fun getItemViewType(position: Int): Int {
        return getItem(position)?.viewtype!!
    }

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

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

    override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): RecyclerView.ViewHolder {
        val view: View?
        return when (viewType) {
            0 -> {
                view = LayoutInflater.from(parent.context)
                    .inflate(R.layout.item_viewpager_blank, parent, false)
                ViewHolderClassanother(view)
            }
            else -> {
                view = LayoutInflater.from(parent.context)
                    .inflate(R.layout.item_scrapvertical, parent, false)
                ViewHolderClass(view)
            }
        }
    }

    override fun onBindViewHolder(holder: RecyclerView.ViewHolder, position: Int) {
        val obj = getItem(position)!!
        if (obj.viewtype == 0) {
            holder as ViewHolderClassanother
        } else {
            (holder as ViewHolderClass).scrapTitle.text = obj.scrapTitle
            holder.glide.load(obj.scrapImages)
                .apply(
                    RequestOptions.bitmapTransform(
                        MultiTransformation(
                            CenterCrop(),
                            MaskTransformation(obj.scrapMask)
                        )
                    )
                )
                .into(holder.scrapImage)
            holder.scrapImage.setColorFilter(
                Color.parseColor(obj.videoColor),
                PorterDuff.Mode.MULTIPLY
            )
        }
    }
}

Fragment

@AndroidEntryPoint
class ScrapMainFragment : BaseFragment() {
    @Inject
    lateinit var service: ApiService

    private val viewModel: ScrapViewModel by viewModels()

    lateinit var binding: FragmentScrapMainBinding

    override fun onCreateView(
        inflater: LayoutInflater, container: ViewGroup?,
        savedInstanceState: Bundle?
    ): View {
        binding = FragmentScrapMainBinding.inflate(inflater, container, false)
        return binding.root
    }

    override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
        super.onViewCreated(view, savedInstanceState)
        initRecyclerview()
    }

    private fun initRecyclerview() {
        binding.scrapMainRecyclerview.adapter = ScrapMainAdapter()
        binding.scrapMainRecyclerview.layoutManager =
            StaggeredGridLayoutManager(2, LinearLayoutManager.VERTICAL)
        lifecycleScope.launch {
            viewModel.searchRepo(requireContext()).collectLatest {
                (binding.scrapMainRecyclerview.adapter as ScrapMainAdapter).submitData(it)
            }
        }
    }
}

실 작업에서는 많은 wrapping이 들어가있습니다. 참고 하시면서 헷갈리시는 부분이 있으면 대답해드리겠습니다!


주의!!

작업에 Epoxy와 RecyclerView를 구현해야했습니다.
작업 처음에는 Epoxy에 CarouselModel, Recyclreview 2개의리스트와 라벨, 인디케이터가 들어간 모양이였습니다. ViweModel,PagingSource,Adapter,Repository를 Epoxy와 연동하여 사용하려고 했으나 실패하여
상단에는 Epoxy CarouselModel을 포함한 여러개의 아이템들이 존재하였고 그 하단에 리사이클뷰를 붙이려고했습니다.

그러기 위해서는 NestedScrollView를 사용해야했습니다.
하지만 NestedScrollView와 Paging을 같이 사용하니 문제가 발생하였습니다.
Nested는 계속해서 하단을 늘려주고 한번에 아이템을 전부 불러옵니다. Paging은 마지막을 감지하여 데이터를 불러옵니다. 서로 다른 두개가 엮이게 되면 계속해서 아이템들을 불러오게 됩니다. → 서버에 계속 요청을 하고 어플은 렉이 걸리기 시작하고 서버는 부하가 걸리게 됩니다.

[참고] 꾸준하게님의 블로그 https://leveloper.tistory.com/202

profile
러닝커브를 따라서 등반중입니다.

0개의 댓글