Paging's application in Recycler View, this article is enough.

Preface

AAC is a very good set of framework components. If you haven't understood it yet, I recommend you to read my previous series of articles:

Android Architecture Components Part1:Room

Android Architecture Components Part2:LiveData

Android Architecture Components Part3:Lifecycle

Android Architecture Components Part4:ViewModel

After a year's development, AAC has launched a series of new components to help developers build and develop project frameworks faster. This is a comprehensive introduction to Paging usage. I believe you will have a good grasp of Paging usage after reading this article.

Paging focuses on the list processing of a large number of data requests, so that developers do not need to care about the data paging logic, the data acquisition logic is completely isolated from the ui, reducing project coupling.

But the only limitation of Paging is that it needs to be used in conjunction with RecyclerView and also with a proprietary PagedListAdapter. This is because it encapsulates data as a PagedList object, which is held by adapter, and all data updates and changes are triggered by PagedList.

The advantage is that we can combine LiveData or RxJava to observe the creation of PagedList objects. Once the PagedList has been created, we just need to pass it to adapter, and the rest of the data manipulation updates will be done automatically by adapter. It's much simpler than normal Recycler View development.

Next, we look at Paging with two concrete examples.

  1. Use in Database
  2. Customize DataSource

Use in Database

Paging's use in Database is very simple. It combines with Room to make the operation extremely simple. I summarize it in three steps here.

  1. Use DataSource.Factory to retrieve data in Room
  2. Use LiveData to observe PagedList
  3. Use PagedListAdapter to bind and update data

DataSource.Factory

First, we need to use the DataSource.Factory abstract class to get the data in Room. There is only one create abstract method in it. We don't need to implement it here. Room will automatically create an instance of PositionalDataSource for us, which will implement the create method. So what we have to do is very simple, as follows:

@Dao
interface ArticleDao {
 
    // PositionalDataSource
    @Query("SELECT * FROM article")
    fun getAll(): DataSource.Factory<Int, ArticleModel>
}

We just need to get an instance that implements the abstraction of DataSource.Factory.

The first step is as simple as that. Next, look at the second step.

LiveData

Now we call the getAll method above in ViewMode to get all the article information and encapsulate the returned data as a LiveData, as follows:

class PagingViewModel(app: Application) : AndroidViewModel(app) {
    private val dao: ArticleDao by lazy { AppDatabase.getInstance(app).articleDao() }
 
    val articleList = dao.getAll()
            .toLiveData(Config(
                    pageSize = 5
            ))
}

Through the toLiveData extension method of DataSource.Factory, the LiveData data of PagedList is constructed. The parameters in Config represent the number of data requests per page.

Now that we've got LiveData data, let's move on to step three.

PagedListAdapter

As I said earlier, we need to implement the PagedListAdapter and pass the data we got in the second step to it.

PagedListAdapter is not much different from RecyclerView.Adapter in use, but it rewrites getItemCount and getItem because it uses DiffUtil to avoid unnecessary updates to data.

class PagingAdapter : PagedListAdapter<ArticleModel, PagingVH>(diffCallbacks) {
 
    companion object {
        private val diffCallbacks = object : DiffUtil.ItemCallback<ArticleModel>() {

            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))
}

So the adapter has been built, and once the PagedList is observed, it can be imported into the adapter using submitList.

viewModel.articleList.observe(this, Observer {
    adapter.submitList(it)
})

Has a Paging-based Database list been completed? Is it very simple? If you need complete code, you can view it Github

Customize DataSource

The above data is obtained through Room, but what we need to know is that Room is simple because it can help us implement many database-related logic codes, so we only need to focus on the logic related to our own business. Related to Paging is the concrete implementation of DataSource and DataSource.Factory.

But the vast majority of data in our actual development comes from the network, so the implementation of DataSource and DataSource.Factory still needs us to gnaw.

Fortunately, for the implementation of DataSource, Paging has helped us to provide three very comprehensive implementations, namely:

  1. PageKeyed Data Source: Getting data through the key associated with the current page, it is very common to see the size of the page where the key is used as the request.
  2. ItemKeyed Data Source: Get the next page of data by using specific item data as key. For example, in a chat session, requesting the next page of data may require the id of the previous data.
  3. Positional Data Source: Get the next page of data by using position as key in the data. This is typical of the application of the Database mentioned above.

Positional Data Source believes it's a bit impressive. The default in Room is to get data from the database through Positional Data Source.

Next, we implement network data by using the most widely used Page Keyed Data Source.

Based on the three steps of database, we divide the first step into two steps, so we only need four steps to realize Paging's processing of network data.

  1. Implementation of Network Request Based on PageKeyed Data Source
  2. Implementing DataSource.Factory
  3. Use LiveData to observe PagedList
  4. Use PagedListAdapter to bind and update data

PageKeyedDataSource

Our custom DataSource needs to implement PageKeyedDataSource, which will be followed by three methods that we need to implement

class NewsDataSource(private val newsApi: NewsApi,
                     private val domains: String,
                     private val retryExecutor: Executor) : PageKeyedDataSource<Int, ArticleModel>() {
 
    override fun loadInitial(params: LoadInitialParams<Int>, callback: LoadInitialCallback<Int, ArticleModel>) {
        // Initialize the first page of data
    }
    
    override fun loadAfter(params: LoadParams<Int>, callback: LoadCallback<Int, ArticleModel>) {
        // Load the next page of data
    }

    override fun loadBefore(params: LoadParams<Int>, callback: LoadCallback<Int, ArticleModel>) {
        // Load the previous page of data
    }
}

Load Before is not needed for the time being, because this example is to get the news list, so it only needs load Initial and load After.

As for the specific implementation of these two methods, there is little to say, just according to your business requirements. What I want to say here is that the onResult method of callback, the second parameter of the method, needs to be adjusted after data acquisition. For example, load Initial:

    override fun loadInitial(params: LoadInitialParams<Int>, callback: LoadInitialCallback<Int, ArticleModel>) {
        initStatus.postValue(Loading(""))
        CompositeDisposable().add(getEverything(domains, 1, ArticleListModel::class.java)
                .subscribeWith(object : DisposableObserver<ArticleListModel>() {
                    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)
                    }
                }))
    }

In the onNext method, we fill in the data we get into the onResult method, and pass in the previous page number previous PageKey (initialized as the first page) and the subsequent page nextPageKey. NextPageKey naturally acts on the loadAfter method. In this way, we can get the params parameters in loadAfter:

    override fun loadAfter(params: LoadParams<Int>, callback: LoadCallback<Int, ArticleModel>) {
        loadStatus.postValue(Loading(""))
        CompositeDisposable().add(getEverything(domains, params.key, ArticleListModel::class.java)
                .subscribeWith(object : DisposableObserver<ArticleListModel>() {
                    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)
                    }
                }))
    }

So the DataSource is basically complete. Next, we need to implement DataSource.Factory to generate our custom DataSource.

DataSource.Factory

As we mentioned earlier, DataSource.Factory has only one abstract method. We just need to implement its create method to create a custom DataSource.

class NewsDataSourceFactory(private val newsApi: NewsApi,
                            private val domains: String,
                            private val executor: Executor) : DataSource.Factory<Int, ArticleModel>() {
 
    val dataSourceLiveData = MutableLiveData<NewsDataSource>()
 
    override fun create(): DataSource<Int, ArticleModel> {
        val dataSource = NewsDataSource(newsApi, domains, executor)
        dataSourceLiveData.postValue(dataSource)
        return dataSource
    }
}

Well, the code is so simple that this step is complete. The next thing to do is to wrap pagedList in LiveData.

Repository & ViewModel

Unlike Database, it does not use DataSource.Factory to get pagedList directly in ViewModel, but uses Repository to encapsulate it. It uses sendRequest abstract method to get the encapsulated result instance of NewsListing Model.

data class NewsListingModel(val pagedList: LiveData<PagedList<ArticleModel>>,
                            val loadStatus: LiveData<LoadStatus>,
                            val refreshStatus: LiveData<LoadStatus>,
                            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()

So the sendRequest in Repository will return the NewsListing Model, which contains data lists, load status, refresh status, retry and refresh requests.

class NewsRepository(private val newsApi: NewsApi,
                     private val domains: String,
                     private val executor: Executor) : BaseRepository<NewsListingModel> {
 
    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()
                }
        )
    }

}

Next, ViewModel is much simpler. What it needs to focus on is that data in NewsListing Model can be separated into a single LiveData object. Since its members are LiveDate objects, separation is also very simple. Separation is for observing in Activity.

class NewsVM(app: Application, private val newsRepository: BaseRepository<NewsListingModel>) : AndroidViewModel(app) {

    private val newsListing = MutableLiveData<NewsListingModel>()
 
    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

The Adapter part is basically similar to the Database part. It also needs to implement DiffUtil.ItemCallback. The rest is the normal Adapter implementation. I won't talk about it here. If you need to read the source code, please.

The last observation code

    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 encapsulation is still very good, especially the RecyclerView dependence on the project, or the effect is good. Of course, its advantages are also its limitations, which is also impossible.

I hope you will be familiar with using Paging through this article. If this article is helpful to you, you can pay close attention to a wave. This is my greatest encouragement!

Project address

Essence of Android

The purpose of this library is to comprehensively analyze Android-related knowledge points in combination with detailed Demo, so as to help readers grasp and understand the key points elaborated faster.

Essence of Android

blog

Keywords: Android Database network REST

Added by schlag on Wed, 31 Jul 2019 17:48:21 +0300