Android

Android Paging3 개념정리 및 사용기

show2888 2022. 7. 6. 17:37
반응형

Paging 개념

데이터 목록을 일정한 덩어리로 나눠서 제공하는것

한번에 모든데이터를 불러오면 높은 리소스를 사용하기에 페이징 처리를 하게되면 성능, 메모리, 비용 측면에서 매우 효율적이다.

Android Jetpack 라이브러리에서 제공하는것으로 Paging3 라이브러리가 있다.

이를 이용하면 사용자가 로드된 데이터의 끝까지 스크롤할 때 RecyclerView 어댑터가 자동으로 데이터를 요청한다.

Paging3에 새로 도입된것

Paging3에서 가장 크게 달라진 점은, 기존 Paging2에서 여러가지 방법으로 DataSource 처리를 하던 부분이 PagingSource 하나로 통합 되었다는 점이다.

 

그 외 내용으론 아래와 같다.

  - Paging3 라이브러리는 Android 앱 아키텍처를 따르고 Kotlin을 우선으로 지원하며 다른 Jetpack 구성 요소와 통합하도록 설계
  - Coroutines 및 Flow뿐만 아니라 LiveData 및 RxJava도 최고 수준으로 지원
  - 오류 처리, 새로 고침 및 재시도 기능을 지원
  - 상태 머리글, 바닥 글 및 목록 구분 기호를로드하는 기능이 내장
  - 데이터의 메모리 캐싱에서 시스템 리소스의 효율적인 사용을 보장
  - API 요청 중복을 방지

Paging3 동작원리

출처 : https://yoon-dailylife.tistory.com/78

Paging3 어플리케이션 아키텍쳐 

출처 : https://yoon-dailylife.tistory.com/78

Repository Layer

RepositoryLayer는 각 페이지를 어떻게 가져올지 정의하는 레이어이다.

 

PagingSource)

페이징의 기본 구성요소로서 데이터를 검색하는 방법을 정의한다.

PagingSource는 데이터 소스를 정의하기 위한 클래스로 Key타입과, 반환할 데이터 형을 Generic으로 받는다.

class ArticlePagingSource : PagingSource<Int, Article>() {

    // 이 메소드는 Paging 라이브러리가 UI 관련 항목을 새로고침해야 할 때 호출됩니다.
    override fun getRefreshKey(state: PagingState<Int, Article>): Int? {

        val anchorPosition = state.anchorPosition ?: return null
        val article = state.closestItemToPosition(anchorPosition) ?: return null

        return ensureValidKey(key = article.id - (state.config.pageSize / 2))
    }

    // 사용자가 스크롤할 때 표시할 더 많은 데이터를 비동기식으로 가져오기 위해 Paging 라이브러리에서 load() 함수를 호출합니다.
    override suspend fun load(params: LoadParams<Int>): LoadResult<Int, Article> {
        val startKey = params.key ?: STARTING_KEY

        val range = startKey.until(startKey + params.loadSize)
//        val range = startKey.until(50)

        if(startKey != STARTING_KEY) delay(2000)
        return LoadResult.Page(
            data = range.map{ number ->
                Article(
                    id = number,
                    title = "Article $number",
                    description = "This describes article $number"
                )
            },
            prevKey = when(startKey) {
                STARTING_KEY -> null
                else -> when(val prevKey = ensureValidKey(key = range.first - params.loadSize)){
                    STARTING_KEY -> null
                    else -> prevKey
                }
            },
            nextKey = range.last + 1
        )

    }

    /**
     * Makes sure the paging key is never less than [STARTING_KEY]
     */
    private fun ensureValidKey(key: Int) = max(STARTING_KEY, key)
}

 

RemoteMediator)

Network에서 데이터 로드와, 로드된 데이터를 내부 DB로 저장하는 역할을 수행한다.

즉, Paging3에서 지원하는 내부DB캐싱에 관련된 역할을 수행하는 클래스이다.

RemoteMediator를 사용하게 되면 PagingSource는 캐시된 데이터만을 사용하여 UI처리용 데이터를 처리한다.

@OptIn(ExperimentalPagingApi::class)
class TestRemoteMediator: RemoteMediator<Int, TestPagingData>(){

    override suspend fun initialize(): InitializeAction {
        return InitializeAction.SKIP_INITIAL_REFRESH
    }

    override suspend fun load(
        loadType: LoadType,
        state: PagingState<Int, TestPagingData>
    ): MediatorResult {
        MediatorResult.Success(endOfPaginationReached = true)
    }
}

initialize 함수는 캐시된 데이터를 갱신시킬 것인지에 대한 로직구현을 위한 함수이고, load함수는 데이터를 가져오기 위한 로직구현을 위한 함수이다.

RemoteMediator 사용 시 특이점은 Key를 내부에서 제공해주거나 사용하지 않는 점이다.

load 함수에서 제공되는 LoadType을 활용하여 데이터 추가로드 여부에 대한 로직을 구현한 뒤, 결과를 load함수의 반환형인 MediatorResult에 endOfPaginationReached 파라미터에 넘겨주어 데이터 로드를 끝마칠지 판단한다.

(상황에 따라 Key가 필요하다면 내부 DB를 사용하여 직접 생성 및 관리 하도록 한다.)

 

 

ViewModel Layer

Pager)

PagingSource 나 RemoteMediator와 PageConfig의 정보를 토대로 PagingData를 생성한 뒤 스트림화 해주는 클래스입니다.

스트림화 시에는 Flow, LiveData, RxJava와 같은 Flowable 유형과 Observable유형 모두를 지원합니다.

 

- Flow를 사용한 Pager

private const val ITEMS_PER_PAGE = 50

class ArticleViewModel(
    repository: ArticleRepository
) : ViewModel(){

    val items: Flow<PagingData<Article>> = Pager(
        config = PagingConfig(pageSize = ITEMS_PER_PAGE, enablePlaceholders = false),
        pagingSourceFactory = { repository.articlePagingSource() }
    ).flow.cachedIn(viewModelScope)
    // 구성 또는 탐색 변경사항에도 페이징 상태를 유지하려면 viewModelScope를 전달하는 cachedIn() 메서드를 사용합니다.
}
// RemoteMediator를 사용하는 형태의 Pager생성
val pagingDataFlow: Flow<PagingData<TestPagingData>> = Pager(
        config = PagingConfig(pageSize = 30),
        remoteMediator = TestRemoteMediator()
    ) {
        TestPagingSource()
    }.flow

 

- LiveData를 사용한 Pager

 

ViewModel class

var pagerItemList = MutableLiveData<MutableList<PagerItem>>()

val pagerItem = pagerItemList.switchMap {
        repository.getItem(PagingSource.Type).cachedIn(viewModelScope)
    }

Repository class

class PagingRepository {
    fun getItem(pageType: Int) =
        Pager(
            config = PagingConfig(
                pageSize = 30,
                maxSize = 120,
                enablePlaceholders = false
            ),
            // 사용할 메소드 선언
            pagingSourceFactory = { ItemPagingSource(DataRepository(), pageType) }
        ).liveData
}

UI Layer

PagingDataAdapter)

RecylcerView.Adapter를 상속받은 PagingDataAdapter를 이용하여 어댑터를 만들어서 UI를 구성한다.

이때 DiffUtil.ItemCallBack<>()을 이용해 아이템을 가져올때의 기준을 정할수있다. 

이를 이용해 refresh할때 캐시된 데이터를 사용 할 수 있다.

class ArticleAdapter : PagingDataAdapter<Article, ArticleViewHolder>(ARTICLE_DIFF_CALLBACK) {

    override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ArticleViewHolder =
        ArticleViewHolder(
            ArticleViewholderBinding.inflate(
                LayoutInflater.from(parent.context),
                parent,
                false,
            )
        )

    override fun onBindViewHolder(holder: ArticleViewHolder, position: Int) {
        val tile = getItem(position)
        if (tile != null) {
            holder.bind(tile)
        }
        getItem(0)
    }

    companion object {
        private val ARTICLE_DIFF_CALLBACK = object : DiffUtil.ItemCallback<Article>() {
            override fun areItemsTheSame(oldItem: Article, newItem: Article): Boolean =
                oldItem.id == newItem.id

            override fun areContentsTheSame(oldItem: Article, newItem: Article): Boolean =
                oldItem == newItem
        }
    }
}

리스트를 갱신할때 notifyDataSetChanged()보다 refresh()를 추천한다.

Note: the notifyDataSetChanged() is a costly function and can lead to performance impacts. You can optimize the update for example by making use of DiffUtil.ItemCallback.

 

Activity or Fragment)

   
   val items = viewModel.items
   val articleAdapter = ArticleAdapter()
		
        // Collect from the PagingData Flow in the ViewModel,
        // and submit it to the PagingDataAdapter.
        lifecycleScope.launch {
            // We repeat on the STARTED lifecycle because an Activity may be PAUSED
            // but still visible on the screen, for example in a multi window app
            repeatOnLifecycle(Lifecycle.State.STARTED) {
                items.collectLatest {
                    articleAdapter.submitData(it)
                }
            }
        }
// Use the CombinedLoadStates provided by the loadStateFlow on the ArticleAdapter to
// show progress bars when more data is being fetched
lifecycleScope.launch {
    repeatOnLifecycle(Lifecycle.State.STARTED) {
        articleAdapter.loadStateFlow.collect {
            binding.prependProgress.isVisible = it.source.prepend is LoadState.Loading
            binding.appendProgress.isVisible = it.source.append is LoadState.Loading
        }
    }
}


Paging 단점

페이징된 리스트에서 각자의 아이템 컨트롤이 어렵다. 나 역시 이문제 때문에 애를 많이먹었다.

참고 : https://valuenetwork.tistory.com/111

 

페이지된 아이템 조작하기

 

페이징에서 snapshot()메소드로 현재까지 불러온 페이지아이템목록을 가져온뒤 필터를 걸어서 아이템 하나를 선택한다.

val position = pagingAdapter.snapshot().items.indexOfFirst {
    it.scheduleSq == schedule.scheduleSq
}

참고 : https://velog.io/@silmxmail/Paging3-Library

 

 

 

 

More)

- 왜 페이징을 쓰는가

https://medium.com/@jungil.han/paging-library-%EA%B7%B8%EA%B2%83%EC%9D%B4-%EC%93%B0%EA%B3%A0%EC%8B%B6%EB%8B%A4-bc2ab4d27b87

 

- Generic Paging

http://labs.brandi.co.kr/2021/07/07/parkks2.html

 

- 로컬 데이터 캐싱

https://www.charlezz.com/?p=44568 

 

 

출처)

https://velog.io/@heetaeheo/Paging3-With-MVVM

https://yoon-dailylife.tistory.com/78

https://valuenetwork.tistory.com/111

https://leveloper.tistory.com/202

http://labs.brandi.co.kr/2021/07/07/parkks2.html

반응형

'Android' 카테고리의 다른 글

Camera2 API 개념부터 구현까지  (0) 2023.04.06
안드로이드 Server’scertificate is not trusted 해결  (0) 2022.06.20
Android Room  (0) 2022.06.15
안드로이드 애니메이션  (0) 2022.06.08