RecyclerView에서 Paging의 활용

전언
AAC는 아주 좋은 프레임워크 구성 요소입니다. 만약 당신이 아직 이해하지 못했다면, 저의 이전 시리즈를 읽는 것을 추천합니다.
Android Architecture Components Part1:Room
Android Architecture Components Part2:LiveData
Android Architecture Components Part3:Lifecycle
Android Architecture Components Part4:ViewModel
1년의 발전을 거쳐 AAC는 일련의 새로운 구성 요소를 내놓아 개발자가 프로젝트 구조의 구축과 개발을 더욱 빨리 진행하도록 돕는다.이번에 다루는 것은 Paging의 운용에 대한 전면적인 소개입니다. 이 글을 읽은 후에 Paging의 운용에 대해 손금 보듯 잘 알고 있을 거라고 믿습니다.
Paging은 대량의 데이터 요청이 있는 목록 처리에 전념하여 개발자가 데이터의 페이지 논리에 관심을 두지 않고 데이터의 획득 논리를 ui와 완전히 분리하여 프로젝트의 결합을 낮추도록 한다.
그러나 Paging의 유일한 한계는 RecyclerView와 결합하여 사용해야 하며, 또한 독점적인 PagedListAdapter를 사용해야 한다는 것이다.이는 데이터를 하나의 Paged List 대상으로 통일적으로 봉인하고, adapter는 이 대상을 가지고 있으며, 모든 데이터의 업데이트와 변동은 Paged List를 통해 촉발되기 때문이다.
이러한 장점은 LiveData나 RxJava와 결합하여 PagedList 대상의 창설을 관찰할 수 있다는 것이다. PagedList가 창설되면 이를 adapter에 전송하면 되고 나머지 데이터 업데이트 작업은 adapter가 자동으로 완성한다.일반적인 RecyclerView 개발에 비해 훨씬 간단합니다.
다음은 Paging에 대한 두 가지 구체적인 사례를 살펴보겠습니다.
  • Database에서 사용
  • 맞춤형 DataSource
  • Database에서의 사용
    Paging은 Database에서 사용하기 매우 간단합니다. 이것은 Room과 결합하여 조작을 극도로 간단합니다. 저는 이를 세 단계로 요약합니다.
  • DataSource를 사용합니다.Factory - Room의 데이터 가져오기
  • LiveData를 사용한 PagedList 관찰
  • PagedListAdapter를 사용하여 데이터를 바인딩 및 업데이트
  • DataSource.Factory
    먼저 DataSource를 사용해야 합니다.Factory 추상 클래스는 Room의 데이터를 가져옵니다. 내부에 하나의create 추상 방법만 있으면 됩니다. 여기서 우리는 실현할 필요가 없습니다. Room은 자동으로 Positional DataSource 실례를 만들고,create 방법을 실현할 것입니다.그래서 우리가 해야 할 일은 매우 간단하다. 다음과 같다.
    @Dao
    interface ArticleDao {
     
        // PositionalDataSource
        @Query("SELECT * FROM article")
        fun getAll(): DataSource.Factory
    }

    우리는 단지 DataSource를 실현할 수 있을 뿐이다.Factory의 추상적인 실례만 있으면 된다.
    첫 번째 단계는 이렇게 간단합니다. 다음 두 번째 단계를 보겠습니다.
    LiveData
    현재 우리는 ViewMode에서 위의 getall 방법을 호출하여 모든 글 정보를 얻고, 되돌아오는 데이터를 다음과 같이 LiveData로 봉인합니다.
    class PagingViewModel(app: Application) : AndroidViewModel(app) {
        private val dao: ArticleDao by lazy { AppDatabase.getInstance(app).articleDao() }
     
        val articleList = dao.getAll()
                .toLiveData(Config(
                        pageSize = 5
                ))
    }

    DataSource를 통해Factory의 toLiveData 확장 방법으로 PagedList의 LiveData 데이터를 구축합니다.여기서 Config의 매개 변수는 페이지당 요청된 데이터 개수를 나타냅니다.
    LiveData 데이터를 확보했으므로 3단계로 넘어가겠습니다.
    PagedListAdapter
    앞에서 말했듯이, 우리는 Paged List Adapter를 실현하고, 두 번째 단계에서 얻은 데이터를 그것에 전송해야 한다.
    PagedListAdapter 및 RecyclerView.Adapter의 사용은 크게 다르지 않습니다. GetItem Count과 GetItem을 다시 썼을 뿐입니다. DiffUtil을 사용해서 데이터의 쓸모없는 업데이트를 피합니다.
    class PagingAdapter : PagedListAdapter(diffCallbacks) {
     
        companion object {
            private val diffCallbacks = object : DiffUtil.ItemCallback() {
    
                override fun areItemsTheSame(oldItem: ArticleModel, newItem: ArticleModel): Boolean = oldItem.id == newItem.id
     
                override fun areContentsTheSame(oldItem: ArticleModel, newItem: ArticleModel): Boolean = oldItem == newItem
    
            }
        }
    
        override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): PagingVH = PagingVH(R.layout.item_paging_article_layout, parent)
     
        override fun onBindViewHolder(holder: PagingVH, position: Int) = holder.bind(getItem(position))
    }

    이렇게 하면 adapter도 구축이 완료되었고 마지막으로PagedList가 관찰되면submitList를 사용하여 adapter에 전송하면 된다.
    viewModel.articleList.observe(this, Observer {
        adapter.submitList(it)
    })

    Paging 기반 Database 목록이 완성되었으니 매우 간단하지 않습니까?전체 코드가 필요한 경우 Github
    DataSource 사용자 정의
    위에서 Room을 통해 데이터를 얻지만 우리가 알아야 할 것은 Room이 간단한 이유는 우리가 많은 데이터베이스와 관련된 논리 코드를 실현할 수 있기 때문에 우리는 자신의 업무와 관련된 논리에만 관심을 가지면 된다는 것이다.이 중에서 Paging과 관련된 것은 DataSource와 DataSource이다.팩토리의 구체적 구현
    그러나 우리가 실제 개발한 데이터의 대부분은 네트워크에서 나온 것이기 때문에 DataSource와 DataSource.팩토리의 실현은 역시 우리 스스로 해결해야 한다.
    다행히 DataSource의 구현에 있어서 Paging은 다음과 같은 세 가지 포괄적인 구현을 지원했습니다.
  • PageKeyedDataSource: 현재 페이지와 관련된 키를 통해 데이터를 얻습니다. 흔히 키가 요청한 페이지의 크기입니다.
  • ItemKeyedDataSource: 구체적인 item 데이터를 키로 삼아 다음 페이지의 데이터를 가져옵니다.예를 들어 채팅 세션에서 다음 페이지의 데이터를 요청하려면 이전 데이터의 id가 필요할 수 있습니다.
  • Positional DataSource: 데이터에 있는position을 키로 하여 다음 페이지의 데이터를 가져옵니다.이 전형적인 것은 위에서 말한 Database에서의 운용이다.

  • Positional DataSource는 이미 약간의 인상이 있다고 믿습니다. 룸에서 기본적으로 저를 도와준 것은 Positional DataSource를 통해 데이터베이스에 있는 데이터를 얻는 것입니다.
    다음은 가장 광범위한 PageKeyedDataSource를 사용하여 네트워크 데이터를 구현하는 것입니다.
    Databases의 3단계를 바탕으로 1단계를 2단계로 나누었기 때문에 4단계만 있으면 Paging이 네트워크 데이터를 처리할 수 있습니다.
  • PageKeyedDataSource 기반 네트워크 요청
  • DataSource.Factory
  • LiveData를 사용한 PagedList 관찰
  • PagedListAdapter를 사용하여 데이터를 바인딩 및 변경
  • PageKeyedDataSource
    저희가 사용자 정의한 DataSource는 PageKeyed DataSource를 실현해야 합니다. 실현된 후에 다음과 같은 세 가지 방법이 있습니다.
    class NewsDataSource(private val newsApi: NewsApi,
                         private val domains: String,
                         private val retryExecutor: Executor) : PageKeyedDataSource() {
     
        override fun loadInitial(params: LoadInitialParams, callback: LoadInitialCallback) {
            //         
        }
        
        override fun loadAfter(params: LoadParams, callback: LoadCallback) {
            //        
        }
    
        override fun loadBefore(params: LoadParams, callback: LoadCallback) {
            //        
        }
    }

    그중에서loadBefore는 잠시 사용할 수 없습니다. 왜냐하면 이 실례는 뉴스 목록을 가져오는 것이기 때문에loadInitial과loadAfter만 있으면 됩니다.
    이 두 가지 방법의 구체적인 실현은 사실 더 이상 말할 것이 없다. 당신의 업무 요구에 따라 하면 된다. 여기서 말하고자 하는 것은 데이터 획득이 끝난 후에 방법 두 번째 매개 변수인 콜백의 onResult 방법을 되돌려야 한다는 것이다.예를 들어 loadInitial:
        override fun loadInitial(params: LoadInitialParams, callback: LoadInitialCallback) {
            initStatus.postValue(Loading(""))
            CompositeDisposable().add(getEverything(domains, 1, ArticleListModel::class.java)
                    .subscribeWith(object : DisposableObserver() {
                        override fun onComplete() {
                        }
     
                        override fun onError(e: Throwable) {
                            retry = {
                                loadInitial(params, callback)
                            }
                            initStatus.postValue(Error(e.localizedMessage))
                        }
    
                        override fun onNext(t: ArticleListModel) {
                            initStatus.postValue(Success(200))
                            callback.onResult(t.articles, 1, 2)
                        }
                    }))
        }

    onNext 방법에서 우리는 얻은 데이터를 onResult 방법에 채우고 이전의 페이지 번호previousPageKey(첫 페이지로 초기화)와 그 다음 페이지의nextPageKey로 전송한다. nextPageKey는loadAfter 방법에 자연히 작용한다.이렇게 하면 loadAfter의 params 매개변수에서 다음을 얻을 수 있습니다.
        override fun loadAfter(params: LoadParams, callback: LoadCallback) {
            loadStatus.postValue(Loading(""))
            CompositeDisposable().add(getEverything(domains, params.key, ArticleListModel::class.java)
                    .subscribeWith(object : DisposableObserver() {
                        override fun onComplete() {
                        }
     
                        override fun onError(e: Throwable) {
                            retry = {
                                loadAfter(params, callback)
                            }
                            loadStatus.postValue(Error(e.localizedMessage))
                        }
     
                        override fun onNext(t: ArticleListModel) {
                            loadStatus.postValue(Success(200))
                            callback.onResult(t.articles, params.key + 1)
                        }
                    }))
        }

    이렇게 하면 DataSource는 기본적으로 완성되었다. 다음에 해야 할 것은 DataSource를 실현하는 것이다.사용자 정의 DataSource를 생성하는 Factory
    DataSource.Factory
    앞서 언급했듯이, DataSource.Factory에는 abstract 메서드가 하나뿐입니다. 이 방법을 사용하여 사용자 정의 DataSource를 만들면 됩니다.
    class NewsDataSourceFactory(private val newsApi: NewsApi,
                                private val domains: String,
                                private val executor: Executor) : DataSource.Factory() {
     
        val dataSourceLiveData = MutableLiveData()
     
        override fun create(): DataSource {
            val dataSource = NewsDataSource(newsApi, domains, executor)
            dataSourceLiveData.postValue(dataSource)
            return dataSource
        }
    }

    응, 코드는 이렇게 간단해. 이 단계도 완성됐어. 다음은 페이지dList를 LiveData로 봉인하는 거야.
    Repository & ViewModel
    Database와 달리 ViewModel에서 DataSource를 직접 통과하지 않았습니다.Factory는 페이지dList를 가져오는 대신 Repository를 사용하여 봉인을 하고 sendRequest 추상적인 방법을 통해 뉴스Listing 모델의 봉인 결과 실례를 통일적으로 가져옵니다.
    data class NewsListingModel(val pagedList: LiveData>,
                                val loadStatus: LiveData,
                                val refreshStatus: LiveData,
                                val retry: () -> Unit,
                                val refresh: () -> Unit)
     
    sealed class LoadStatus : BaseModel()
    data class Success(val status: Int) : LoadStatus()
    data class NoMore(val content: String) : LoadStatus()
    data class Loading(val content: String) : LoadStatus()
    data class Error(val message: String) : LoadStatus()

    따라서 Repository의sendRequest가 되돌아오는 것은 뉴스Listing 모델입니다. 데이터 목록, 불러오는 상태, 새로 고침 상태, 재시도, 새로 고침 요청이 포함되어 있습니다.
    class NewsRepository(private val newsApi: NewsApi,
                         private val domains: String,
                         private val executor: Executor) : BaseRepository {
     
        override fun sendRequest(pageSize: Int): NewsListingModel {
            val newsDataSourceFactory = NewsDataSourceFactory(newsApi, domains, executor)
            val newsPagingList = newsDataSourceFactory.toLiveData(
                    pageSize = pageSize,
                    fetchExecutor = executor
            )
            val loadStatus = Transformations.switchMap(newsDataSourceFactory.dataSourceLiveData) {
                it.loadStatus
            }
            val initStatus = Transformations.switchMap(newsDataSourceFactory.dataSourceLiveData) {
                it.initStatus
            }
            return NewsListingModel(
                    pagedList = newsPagingList,
                    loadStatus = loadStatus,
                    refreshStatus = initStatus,
                    retry = {
                        newsDataSourceFactory.dataSourceLiveData.value?.retryAll()
                    },
                    refresh = {
                        newsDataSourceFactory.dataSourceLiveData.value?.invalidate()
                    }
            )
        }
    
    }

    그 다음에 View 모델에서는 상대적으로 훨씬 간단해졌다. 뉴스Listing 모델에서의 데이터를 하나의 LiveData 대상으로 분리하면 된다. 그 자체가 LiveDate 대상이기 때문에 분리도 매우 간단하다.분리는 Activity에서 observe 관찰을 할 수 있도록 하기 위해서입니다.
    class NewsVM(app: Application, private val newsRepository: BaseRepository) : AndroidViewModel(app) {
    
        private val newsListing = MutableLiveData()
     
        val adapter = NewsAdapter {
            retry()
        }
     
        val newsLoadStatus = Transformations.switchMap(newsListing) {
            it.loadStatus
        }
     
        val refreshLoadStatus = Transformations.switchMap(newsListing) {
            it.refreshStatus
        }
     
        val articleList = Transformations.switchMap(newsListing) {
            it.pagedList
        }
     
        fun getData() {
            newsListing.value = newsRepository.sendRequest(20)
        }
     
        private fun retry() {
            newsListing.value?.retry?.invoke()
        }
     
        fun refresh() {
            newsListing.value?.refresh?.invoke()
        }
    }

    PagedListAdapter & Activity
    Adapter 부분은 Database와 기본적으로 유사하며 주로 DiffUtil을 실현해야 한다.Item Callback, 나머지는 정상적인 어댑터 구현입니다. 저는 더 이상 말하지 않겠습니다. 필요하면 원본 코드를 읽어주세요.
    마지막 observe 코드
        private fun addObserve() {
            newsVM.articleList.observe(this, Observer {
                newsVM.adapter.submitList(it)
            })
            newsVM.newsLoadStatus.observe(this, Observer {
                newsVM.adapter.updateLoadStatus(it)
            })
            newsVM.refreshLoadStatus.observe(this, Observer {
                refresh_layout.isRefreshing = it is Loading
            })
            refresh_layout.setOnRefreshListener {
                newsVM.refresh()
            }
            newsVM.getData()
        }

    Paging은 포장이 잘 되어 있는데 특히 프로젝트에서RecyclerView에 의존하는 것이 효과가 좋다.물론 장점도 한계라는 점도 어쩔 수 없다.
    이 글을 통해 Paging을 익히고 활용할 수 있기를 바랍니다. 만약에 이 글이 당신에게 도움이 된다면 당신은 이 글에 관심을 가질 수 있습니다. 이것은 저에게 가장 큰 격려입니다!
    프로젝트 주소
    안드로이드 에센스
    이 라이브러리의 목적은 상세한 데모를 결합하여 안드로이드와 관련된 지식을 전면적으로 해석하여 독자들이 논술한 요점을 더욱 빨리 파악하고 이해할 수 있도록 돕는 것이다
    안드로이드 에센스
    blog

    좋은 웹페이지 즐겨찾기