preface

AAC is a great set of framework components, and if you haven’t already, I recommend you read my previous series:

Android Architecture Components Part1:Room

Android Architecture Components Part2:LiveData

Android Architecture Components Part3:Lifecycle

Android Architecture Components Part4:ViewModel

After a year of development, AAC has released a series of new components to help developers build and develop project frameworks faster. This time mainly involves a comprehensive introduction to the application of Paging. I believe that you will be familiar with the application of Paging after reading this article.

Paging focuses on list processing with a large number of data requests, which eliminates the need for developers to care about the Paging logic of data and completely isolates the data acquisition logic from the UI to reduce project coupling.

The only limitation of Paging, however, is that it needs to be used in conjunction with RecyclerView, as well as using a proprietary PagedListAdapter. This is because it encapsulates the data into a PagedList object, which the Adapter holds, and all data updates and changes are triggered by the PagedList.

The advantage of this is that we can use LiveData or RxJava to observe the creation of the PagedList object. Once the PagedList has been created, we simply pass it to the Adapter and the rest of the data update is done automatically by the Adapter. Compared with the normal RecyclerView development, much simpler.

In the following, we use two concrete examples to understand Paging

  1. Use in Database
  2. Custom DataSource

Use in Database

The use of Paging in Database is very simple, and its combination with Room makes the operation extremely simple. I summarize it here in three steps.

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

DataSource.Factory

The first step is to use the DataSource.Factory abstract class to retrieve the data in Room. It only needs a create abstract method inside. So here’s what we’re going to do:

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

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

That’s the first step. Let’s move on to step two

LiveData

Now we call the above getAll method in ViewMode to getAll the article information and encapsulate the returned data into 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))}Copy the code

The LiveData of the PagedList is built by the toLiveData extension method of DataSource.Factory. The parameter Config represents the number of data requested per page.

Now that we have the LiveData, let’s move on to step 3

PagedListAdapter

As mentioned earlier, we are going to implement the PagedListAdapter and pass it the data from the second step.

The use of PagedListAdapter and recyclerView. Adapter is not very different, but the getItemCount and getItem are overwritten, because it uses DiffUtil, to avoid useless 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))
}
Copy the code

Then the Adapter is built, and finally once the PagedList is observed, pass it to the Adapter using the submitList.

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

A Database list based on Paging has been completed, isn’t it very simple? See Github for the full code

Custom DataSource

This is using Room to get the data, but remember that Room is simple because it implements a lot of database-specific logic code for ourselves, leaving us to focus only on the logic that is relevant to our business. Among them, Paging is related to the concrete implementation of DataSource and DataSource.Factory.

However, most of the data in our actual development comes from the network, so the implementation of DataSource and DataSource.Factory is still to be gnawed by ourselves.

Fortunately, for the implementation of DataSource, Paging has provided us with three very comprehensive implementations, which are:

  1. PageKeyedDataSource: Data is retrieved by the key associated with the current page, most commonly the size of the page requested by the key.
  2. ItemKeyedDataSource: Uses the item data as the key to retrieve the next page data. For example, in a chat session, requesting the next page of data might require the ID of the previous data.
  3. PositionalDataSource: Obtains the next page of data by using position as the key in the data. This is typical of the application of Database mentioned above.

PositionalDataSource = PositionalDataSource = PositionalDataSource = PositionalDataSource

Next we implement network data by using the broadest PageKeyedDataSource.

Based on the three steps of Databases, we split its first step into two steps here, so we only need four steps to realize Paging’s processing of network data.

  1. Implement network request based on PageKeyedDataSource
  2. Implement a DataSource. The Factory
  3. Use LiveData to observe the PagedList
  4. Use PagedListAdapter to bind and update data

PageKeyedDataSource

Our custom DataSource needs to implement PageKeyedDataSource. After that, we need to implement the following three methods

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}}Copy the code

LoadBefore is not used for the moment, because my example is to get the news list, so only loadInitial and loadAfter are needed.

The onResult method (callback) is called as the second parameter to the callback method. For example loadInitial:

    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(a){}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)}}}))Copy the code

In the onNext method, we populate the onResult method with the obtained data, passing in the previous page number previousPageKey(initialized as the first page) and the next page nextPageKey, which naturally acts on the loadAfter method. This can be obtained in the params parameter of 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(a){}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)}}}))Copy the code

Now that the DataSource is almost complete, we need to implement DataSource.Factory to generate our own DataSource

DataSource.Factory

We already mentioned that the DataSource.Factory has only one abstract method, so we 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(a): DataSource<Int, ArticleModel> {
        val dataSource = NewsDataSource(newsApi, domains, executor)
        dataSourceLiveData.postValue(dataSource)
        return dataSource
    }
}
Copy the code

Well, that’s it, that’s it, that’s it, that’s it, what we need to do is wrap pagedList with LiveData.

Repository & ViewModel

Unlike Database, pagedList is not directly retrieved in ViewModel via datasource. Factory. It is further encapsulated in Repository. The result instance of NewsListingModel is obtained by sendRequest abstract method.

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(a)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()
Copy the code

So sendRequest in Repository will return the NewsListingModel, which contains the data list, load status, refresh status, retries, 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
        }
        returnNewsListingModel( pagedList = newsPagingList, loadStatus = loadStatus, refreshStatus = initStatus, retry = { newsDataSourceFactory.dataSourceLiveData.value? .retryAll() }, refresh = { newsDataSourceFactory.dataSourceLiveData.value? .invalidate() } ) } }Copy the code

Then the ViewModel is relatively simple, it needs to focus on the NewsListingModel data to separate into a single LiveData object, because its members are LiveDate objects, so the separation is also very simple. The separation is in order to observe the 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(a) {
        newsListing.value = newsRepository.sendRequest(20)}private fun retry(a){ newsListing.value? .retry? .invoke() }fun refresh(a){ newsListing.value? .refresh? .invoke() } }Copy the code

PagedListAdapter & Activity

The Adapter part is basically similar to the Database, but it also needs to implement diffUtil.itemCallback. The rest is the normal Adapter implementation, I will not say more here, if necessary, please read the source code

The last observe 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()
    }
Copy the code

Paging encapsulation is still very good, especially the RecyclerView is very dependent on the project, or good effect. Of course, its advantages are also its limitations, which can not be helped.

I hope that you can be familiar with the application of Paging through this article. If this article is helpful to you, you can follow a wave, which is the biggest encouragement to me!

The project address

Android excerption

The purpose of this library is to fully analyze the knowledge points related to Android with detailed Demo, to help readers to grasp and understand the main points explained faster

Android excerption

blog