Android Jetpack架構組件-Paging介紹及實踐

Android 列表分頁組件Paging的設計與實現(xiàn)

先通過官方Paging示例開始,通過Paging實現(xiàn)加載Room數(shù)據(jù)庫中的聯(lián)系人列表簡單介紹jetpack中的Paging的使用

數(shù)據(jù)庫為Room,于是先定義的數(shù)據(jù)查詢Dao,如下所示:

@Dao
interface CheeseDao {
    @Query("select * from cheese order by name ")
    fun findAllCheese(): DataSource.Factory<Int, Cheese>  //返回的為    DataSource.Factory對象
}

可以看到Room數(shù)據(jù)庫直接返回的為DataSource.Factory而不是livedata<User>,后文會提出來,因為它也可構建出一個可觀察的對象LiveData數(shù)據(jù)。

接下來可查看ViewModel和Activity中的實現(xiàn):

class MainActivity : AppCompatActivity() {
  
    //負責數(shù)據(jù)的類的加載
    private val viewModel by viewModels<CheeseViewModel>()

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_main)
        // Create adapter for the RecyclerView
        val adapter = CheeseAdapter()
        cheeseList.adapter = adapter
        // Subscribe the adapter to the ViewModel, so the items in the adapter are refreshed
        // 當viewModel中的allCheeses發(fā)生變化后會調用
        viewModel.allCheeses.observe(this, Observer {
            mAdapter.submitList(it)
        })
        //...
    }
}

在來看看Paging中為RecycleView準備的CheeseAdapter

class CheeseAdapter :
    PagedListAdapter<Cheese, CheeseViewHolder>(object : DiffUtil.ItemCallback<Cheese>() {
        override fun areItemsTheSame(oldItem: Cheese, newItem: Cheese): Boolean =
            oldItem.id == newItem.id
        override fun areContentsTheSame(oldItem: Cheese, newItem: Cheese): Boolean =
            oldItem == newItem
    }) {
    
    override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): CheeseViewHolder =
        CheeseViewHolder(parent)
    
    override fun onBindViewHolder(holder: CheeseViewHolder, position: Int) {
        holder.bindData(getItem(position))
    }
}

這里使用到了PagedListAdapter 需要一個DiffUtil.ItemCallback<T>類型參數(shù),它是官方基于RecyclerView.AdapterAsyncListDiffer封裝類,其內創(chuàng)建了AsyncListDiffer的示例,以便在后臺線程中使用DiffUtil計算新舊數(shù)據(jù)集的差異,從而節(jié)省Item更新的性能。

viewModel中負責處理數(shù)據(jù),則可以去到CheeseViewModel中,查看數(shù)據(jù)是如何加載,可以看到dao.findAllCheese()是DataSource.Factory對象。通過toLiveData(),傳入Paging所需要的Config,即可完成數(shù)據(jù)的轉化和查找。

class CheeseViewModel(app: Application) : AndroidViewModel(app) {
    val dao = CheeseDb.get(app).cheeseDao()
        //LiveData類型數(shù)據(jù)
    val allCheese = dao.findAllCheese().toLiveData(
        Config(
            pageSize = 30,
            enablePlaceholders = true,
            maxSize = 200
        )
    )
}

以上:則一個Paging最簡單的列表完成,可以看到用的如下幾個核心類:DataSource.Factory 、 PagedListAdapter、 DiffUtil.ItemCallbackPagedListBuilder、DataSourceRoom數(shù)據(jù)庫的使用

接下來通過單獨介紹這幾種組件和關系,來探究Paging框架

一、分頁組件的簡介

1.核心類 PagedList

上文提到,一個普通的RecyclerView展示的是一個列表的數(shù)據(jù),比如List,但在列表分頁的需求中,列表局部更新或者差分異比對,顯然一個List不太夠用了。

為此,Google設計出了一個新的角色PagedList,顧名思義,該角色的意義就是 分頁列表數(shù)據(jù)的容器 。

既然有了List,為什么需要額外設計這樣一個PagedList的數(shù)據(jù)結構?本質原因在于加載分頁數(shù)據(jù)的操作是異步的 ,因此定義PagedList的第二個作用是 對分頁數(shù)據(jù)的異步加載 ,這個我們后文再提。

所以ViewModel可以定義成這樣,因為PagedList也作為列表數(shù)據(jù)的容器(就像List一樣):

class viewModel :viewModel(){
    //before 
    //val users :LiveData<List<User>> = dao.findAllUsers()
    
    //after
     val users:LiveData<PagedList<User>> = dao.findAllUsers()
}

ViewModel中,開發(fā)者可以輕易通過對users進行訂閱以響應分頁數(shù)據(jù)的更新,這個LiveData的可觀察者是通過Room組件創(chuàng)建的,我們來看一下我們的dao:

@Dao
interface UserDao {
  // 注意,這里 LiveData<List<User>> 改成了 LiveData<PagedList<User>>  
  @Query("SELECT * FROM user")
  fun queryUsers(): LiveData<PagedList<User>>  
}

乍得一看似乎理所當然,但實際需求中有一個問題,這里的定義是模糊不清的——對于分頁數(shù)據(jù)而言,不同的業(yè)務場景,所需要的相關配置是不同的。那么什么是分頁相關配置呢?

最直接的一點是每頁數(shù)據(jù)的加載數(shù)量PageSize,不同的項目都會自行規(guī)定每頁數(shù)據(jù)量的大小,一頁請求15個數(shù)據(jù)還是20個數(shù)據(jù)?所以接下來DataSourcePagedListBuilder對象,通過簡單的配置將數(shù)據(jù)源和分頁Page的相關屬性。

2.數(shù)據(jù)源:DataSource及其工廠

回答這個問題之前,我們還需要定義一個角色,用來為PagedList容器提供分頁數(shù)據(jù),那就是數(shù)據(jù)源DataSource。

什么是DataSource呢?可以理解為 數(shù)據(jù)庫數(shù)據(jù) 或者是 服務端數(shù)據(jù) 的一個快照,而不應該是數(shù)據(jù)庫數(shù)據(jù)或者是服務端數(shù)據(jù)

每當Paging被告知需要更多的數(shù)據(jù)的時候,數(shù)據(jù)源DataSource就會將當前的快照對應的索引的數(shù)據(jù)交給PagedList處理

但是需要構建一個新的PagedList的時候,比如數(shù)據(jù)已經失效,DataSource中舊的數(shù)據(jù)就有意義了,因為DataSource需要被重置

在代碼中,這意味著新的DataSource對象被創(chuàng)建,因此,我們需要提供的不是DataSource,而是提供DataSource的工廠(DataSouce.Factory) 這就是為什么查找數(shù)據(jù)庫的時候,返回的事DataSouce.Factory而不是DataSouce<PageList<User>>或者是LiveData<PageList<User>>的原因

為什么要提供DataSource.Factory而不是一個DataSource? 復用這個DataSource不可以嗎,當然可以,但是將DataSource設置為immutable(不可變)會避免更多的未知因素。

接下來如何修改方法中放回的類型,如下所示:

@Dao
interface UserDao{
        @Query("select * from user")
        fun findAllUser():DataSource.Factory<Int,User>
}

返回的是一個數(shù)據(jù)源的提供者DataSource.Factory,頁面初始化時,會通過工廠方法創(chuàng)建一個新的DataSource,這之后對應會創(chuàng)建一個新的PagedList,每當PagedList想要獲取下一頁的數(shù)據(jù),數(shù)據(jù)源都會根據(jù)請求索引進行數(shù)據(jù)的提供。

當數(shù)據(jù)失效時,DataSource.Factory會再次創(chuàng)建一個新的DataSource,其內部包含了最新的數(shù)據(jù)快照(本案例中代表著數(shù)據(jù)庫中的最新數(shù)據(jù)),隨后創(chuàng)建一個新的PagedList,并從DataSource中取最新的數(shù)據(jù)進行展示——當然,這之后的分頁流程都是相同的,無需再次復述。

引用一幅圖用于描述三者之間的關系,讀者可參考上述文字和圖片加以理解

截屏2020-03-2215.59.30.png

3.串聯(lián)兩者:PagedListBuilder

分頁中的相關業(yè)務配置,如每次加載多少條數(shù)據(jù)等等

現(xiàn)在在Dao中接口的返回值已經是DataSource.Factory,而ViewModel中的成員被觀察者則是LiveData<PagedList<User>>類型,那么如何將數(shù)據(jù)源的工廠DataSource.Factory,和LiveData<PagedList>進行串聯(lián)?

因此需要定義一個新的角色PagedListBuilder,開發(fā)者將 數(shù)據(jù)源工廠相關配置統(tǒng)一交給PagedListBuilder,即可生成對應的LiveData<PagedList<User>>:

class MyViewModel(val dao: UserDao) : ViewModel() {
  val users: LiveData<PagedList<User>>

  init {
    // 1.創(chuàng)建DataSource.Factory
    val factory: DataSource.Factory = dao.queryUsers()

    // 2.通過LivePagedListBuilder配置工廠和pageSize, 對users進行實例化
    //  users = LivePagedListBuilder(factory, config).build()
    // 也可以是具體的config對象,定制更多的配置參數(shù)
    users = LivePagedListBuilder(factory, 30).build()
  }
}

如代碼所示:在viewmodel中先通過dao獲取到DataSource.Factory,工廠創(chuàng)建數(shù)據(jù)源DataSource,后者為PagedList提供列表所需要的數(shù)據(jù);此外,另外一個Int類型的參數(shù)則制定每頁數(shù)據(jù)加載的數(shù)量,這里指定數(shù)量為30

所以在viewmodel中創(chuàng)建了一個LiveData<PagedList<User>> 的可觀察對象,則在Actiivty中的代碼如下所示:

class MyActivity : Activity {
  val myViewModel: MyViewModel
  // 1.這里我們使用PagedListAdapter
  val adapter: PagedListAdapter

  fun onCreate(bundle: Bundle?) {
    // 2.在Activity中對LiveData進行訂閱
    myViewModel.users.observe(this) {
      // 3.每當數(shù)據(jù)更新,計算新舊數(shù)據(jù)集的差異,對列表進行更新
      adapter.submitList(it)
    }
  }    
}

4.更多可選的配置:PagedList.Config

目前介紹中,分頁的功能大致已經介紹完成,但是這些在現(xiàn)實開發(fā)中往往不夠,因此,設計者額外定義了更復雜的數(shù)據(jù)結構PagedList.Config,以描述更細節(jié)化的配置參數(shù)

// after
val config = PagedList.Config.Builder()
      .setPageSize(15)              // 分頁加載的數(shù)量
      .setInitialLoadSizeHint(30)   // 初次加載的數(shù)量
      .setPrefetchDistance(10)      // 預取數(shù)據(jù)的距離
      .setEnablePlaceholders(false) // 是否啟用占位符
      .build()

// API發(fā)生了改變
val users: LiveData<PagedList<User>> = LivePagedListBuilder(factory, config).build()

4.1.分頁數(shù)量:PageSize

最易理解的配置,分頁請求數(shù)據(jù)時,開發(fā)者總是需要定義每頁加載數(shù)據(jù)的數(shù)量。

4.2.初始加載數(shù)量:InitialLoadSizeHint

定義首次加載時要加載的Item數(shù)量。

此值通常大于PageSize,因此在初始化列表時,該配置可以使得加載的數(shù)據(jù)保證屏幕可以小范圍的滾動。

如果未設置,則默認為PageSize的三倍。

4.3.預取距離:PrefetchDistance

顧名思義,該參數(shù)配置定義了列表當距離加載邊緣多遠時進行分頁的請求,默認大小為PageSize——即距離底部還有一頁數(shù)據(jù)時,開啟下一頁的數(shù)據(jù)加載。

若該參數(shù)配置為0,則表示除非明確要求,否則不會加載任何數(shù)據(jù),通常不建議這樣做,因為這將導致用戶在滾動屏幕時看到占位符或列表的末尾。

4.4.是否啟用占位符:PlaceholderEnabled

該配置項需要傳入一個boolean值以決定列表是否開啟placeholder(占位符),在知道DataSource知道總數(shù)的情況下,設置為true,則可實現(xiàn)骨架屏的效果

4.5 更多觀察者類型的配置

在本文的示例中,我們建立了一個LiveData>的可觀察者對象供用戶響應數(shù)據(jù)的更新,實際上組件的設計應該面向提供對更多優(yōu)秀異步庫的支持,比如RxJava。

因此,和LivePagedListBuilder一樣,設計者還提供了RxPagedListBuilder,通過DataSource數(shù)據(jù)源和PagedList.Config以構建一個對應的Observable:

// LiveData support
val users: LiveData<PagedList<User>> = LivePagedListBuilder(factory, config).build()

// RxJava support
val users: Observable<PagedList<User>> = RxPagedListBuilder(factory, config).buildObservable()

二、DataSource數(shù)據(jù)源簡介

ItemKeyedDataSource<Key, Value>, PageKeyedDataSource<Key, Value>, PositionalDataSource<T>

Base class for loading pages of snapshot data into a PagedList.

DataSource is queried to load pages of content into a PagedList. A PagedList can grow as it loads more data, but the data loaded cannot be updated. If the underlying data set is modified, a new PagedList / DataSource pair must be created to represent the new data.

用于將快照數(shù)據(jù)頁加載到PagedList的基類。

查詢數(shù)據(jù)源以將內容頁加載到PagedList中。頁面列表可以隨著加載更多數(shù)據(jù)而增長,但無法更新加載的數(shù)據(jù)。如果修改了基礎數(shù)據(jù)集,則必須創(chuàng)建一個新的pagelist/DataSource對來表示新數(shù)據(jù)。

Paging分頁組件的設計中,DataSource是一個非常重要的模塊。顧名思義,DataSource中的Key對應數(shù)據(jù)加載的條件,Value對應數(shù)據(jù)集的實際類型, 針對不同場景,Paging的設計者提供了三種不同類型的DataSource抽象類:

  • PositionalDataSource
  • ItemKeyedDataSource
  • PageKeyedDataSource

接下來我們分別對其進行簡單的介紹。

1.PositionalDataSource

PositionalDataSource是最簡單的DataSource類型,顧名思義,其通過數(shù)據(jù)所處當前數(shù)據(jù)集快照的位置(position)提供數(shù)據(jù)。

PositionalDataSource適用于 目標數(shù)據(jù)總數(shù)固定,通過特定的位置加載數(shù)據(jù),這里KeyInteger類型的位置信息,并且被內置固定在了PositionalDataSource類中,T即數(shù)據(jù)的類型。

最容易理解的例子就是本文的聯(lián)系人列表,其所有的數(shù)據(jù)都來自本地的數(shù)據(jù)庫,這意味著,數(shù)據(jù)的總數(shù)是固定的,我們總是可以根據(jù)當前條目的position映射到DataSource中對應的一個數(shù)據(jù)。

PositionalDataSource也正是Room幕后實現(xiàn)的功能,使用Room為什么可以避免DataSource的配置,通過dao中的接口就能返回一個DataSource.Factory

來看Room組件配置的dao對應編譯期生成的源碼:

// 1.Room自動生成了 DataSource.Factory
@Override
 public DataSource.Factory<Integer, Student> getAllStudent() {
   // 2.工廠函數(shù)提供了PositionalDataSource
   return new DataSource.Factory<Integer, Student>() {
     @Override
     public PositionalDataSource<Student> create() {
       return new PositionalDataSource<Student>(__db, _statement, false , "Student") {
         // ...
       };
     }
   };
 }

2.ItemKeyedDataSource

ItemKeyedDataSource適用于目標數(shù)據(jù)的加載依賴特定條目的信息,比如需要根據(jù)第N項的信息加載第N+1項的數(shù)據(jù),傳參中需要傳入第N項的某些信息時。

使用場景:如QQ或者wechat中的聊天記錄

3.PageKeyedDataSource

這也是最常用的DataSource,更多的用于網絡請求API中,服務器返回的數(shù)據(jù)中都會包含一個String類型類似nextPage的字段,以表示當前頁數(shù)據(jù)的下一頁數(shù)據(jù)的接口(比如GithubAPI),這種分頁數(shù)據(jù)加載的方式正是PageKeyedDataSource的拿手好戲。

這是日常開發(fā)中用到最多的DataSource類型,和ItemKeyedDataSource不同的是,前者的數(shù)據(jù)檢索關系是單個數(shù)據(jù)與單個數(shù)據(jù)之間的,后者則是每一頁數(shù)據(jù)和每一頁數(shù)據(jù)之間的。

同樣拿聯(lián)系人列表舉例,這種分頁加載方式是按照頁碼進行數(shù)據(jù)加載的,比如一次請求15條數(shù)據(jù),服務器返回數(shù)據(jù)列表的同時會返回下一頁數(shù)據(jù)的url(或者頁碼),借助該參數(shù)請求下一頁數(shù)據(jù)成功后,服務器又回返回下下一頁的url,以此類推。

總的來說,DataSource針對不同種數(shù)據(jù)分頁的加載策略提供了不同種的抽象類以方便開發(fā)者調用,很多情況下,同樣的業(yè)務使用不同的DataSource都能夠實現(xiàn),開發(fā)者按需取用即可。

三、通過Paging加載網絡數(shù)據(jù)列表

通過以上,相信讀者能明白Paging中的核心類的認識和作用,接下來,通過Paging加載一個簡單的網絡列表,具體的實現(xiàn)自定義DataSource和Repository等,更加深刻的理解Paging框架。

請求地址為:https://www.wanandroid.com/article/list/0/json (感謝玩Android)

效果圖如下圖所示:

截屏2020-03-2217.04.00.png
  • Activity中的代碼如下
class NetPagingActivity : AppCompatActivity() {

    lateinit var mAdapter: ArticleAdapter
    //加載數(shù)據(jù)使用的ViewModel對象
    val viewModel: ArticleViewModel by viewModels<ArticleViewModel>()

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_second)
        //為RecycleView設置的adapter
        mAdapter = ArticleAdapter()

        rv_as_article.layoutManager = LinearLayoutManager(
            this, LinearLayoutManager.VERTICAL, false
        )
        rv_as_article.adapter = mAdapter
        getData()
    }
    //請求數(shù)據(jù),并更新列表
    private fun getData() {
        viewModel.data.observe(this, Observer {
            mAdapter.submitList(it)
        })
    }
}
  • ArticleViewModel
    ViewModel很簡單,通過NetRepository().getData()獲取DataSource中的可觀察數(shù)據(jù)
class ArticleViewModel : ViewModel() {
    val data = NetRepository().getData()
}
  • NetRepository倉庫
class NetRepository {

    var pageSize = 20
    lateinit var article: LiveData<PagedList<Article>>
    
    fun getData(): LiveData<PagedList<Article>> {
        val dataSourceFactory = NetDataSourceFactory()
        article = dataSourceFactory.toLiveData(
            config = Config(
                pageSize = pageSize,
                enablePlaceholders = false,
                initialLoadSizeHint = pageSize * 2
            )
        )
        return article
    }
}
  • NetDataSourceFactory
    通過繼承DataSource.Factory.重寫onCreate()方法,即構建出一個 DataSource對象
class NetDataSourceFactory() : DataSource.Factory<Int, Article>() {
    val sourceLiveData = MutableLiveData<NetDataSource>()
    override fun create(): DataSource<Int, Article> {
        //NetDataSource為具體加載服務器數(shù)據(jù)的快照
        val source = NetDataSource()
        sourceLiveData.postValue(source)
        return source
    }
}
  • NetDataSource
    通過繼承PageKeyedDataSource,因為請求的列表是根據(jù)nextPage來定位查找,所以選中PageKeyedDataSource。
class NetDataSource : PageKeyedDataSource<Int, Article>() {

    var pageNo = 0

    @SuppressLint("CheckResult")
    override fun loadInitial(
        params: LoadInitialParams<Int>,
        callback: LoadInitialCallback<Int, Article>
    ) {
        RedditApi.create().getArticles(pageNo)
            .subscribeOn(Schedulers.io())
            .subscribe {
                it.data?.datas?.let { it1 ->
                    callback.onResult(it1, pageNo, it.data?.curPage)
                }
                pageNo = it.data?.curPage!!
            }
    }

    @SuppressLint("CheckResult")
    override fun loadAfter(params: LoadParams<Int>, callback: LoadCallback<Int, Article>) {
        RedditApi.create().getArticles(pageNo)
            .subscribeOn(Schedulers.io())
            .subscribe {
                callback.onResult(it.data?.datas!!, it.data?.curPage)
                pageNo = it.data?.curPage!!
            }
    }

    override fun loadBefore(params: LoadParams<Int>, callback: LoadCallback<Int, Article>) {

    }
}
  • ArticleAdapter
    通過繼承子基于RecycleView的PagedListAdapter
class ArticleAdapter : PagedListAdapter<Article, ArticleViewHolder>(diffCallback) {

    companion object {
        val diffCallback = object : DiffUtil.ItemCallback<Article>() {
            override fun areContentsTheSame(oldItem: Article, newItem: Article): Boolean =
                oldItem == newItem

            override fun areItemsTheSame(oldItem: Article, newItem: Article): Boolean =
                oldItem.id == newItem.id
        }
    }

    override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ArticleViewHolder {
        return ArticleViewHolder(parent)
    }

    override fun onBindViewHolder(holder: ArticleViewHolder, position: Int) {
        holder.bindData(getItem(position))
    }
}

至此,Paging框架已介紹完成,待后續(xù)更新Jetpack更多的組件!

文章中的所有示例代碼已上傳至github:https://github.com/OnexZgj/Jetpack_Component

?著作權歸作者所有,轉載或內容合作請聯(lián)系作者
【社區(qū)內容提示】社區(qū)部分內容疑似由AI輔助生成,瀏覽時請結合常識與多方信息審慎甄別。
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發(fā)布,文章內容僅代表作者本人觀點,簡書系信息發(fā)布平臺,僅提供信息存儲服務。

相關閱讀更多精彩內容

友情鏈接更多精彩內容