目錄
Android 列表分頁組件Paging的設計與實現
先通過官方Paging示例開始,通過Paging實現加載Room數據庫中的聯繫人列表簡單介紹jetpack中的Paging的使用
數據庫爲Room,於是先定義的數據查詢Dao,如下所示:
@Dao
interface CheeseDao {
@Query("select * from cheese order by name ")
fun findAllCheese(): DataSource.Factory<Int, Cheese> //返回的爲 DataSource.Factory對象
}
可以看到Room數據庫直接返回的爲DataSource.Factory而不是livedata,後文會提出來,因爲它也可構建出一個可觀察的對象LiveData數據。
接下來可查看ViewModel和Activity中的實現:
class MainActivity : AppCompatActivity() {
//負責數據的類的加載
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發生變化後會調用
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類型參數,它是官方基於RecyclerView.Adapter
的AsyncListDiffer
封裝類,其內創建了AsyncListDiffer
的示例,以便在後臺線程中使用DiffUtil
計算新舊數據集的差異,從而節省Item
更新的性能。
viewModel
中負責處理數據,則可以去到CheeseViewModel
中,查看數據是如何加載,可以看到dao.findAllCheese()是DataSource.Factory
對象。通過toLiveData(),傳入Paging
所需要的Config
,即可完成數據的轉化和查找。
class CheeseViewModel(app: Application) : AndroidViewModel(app) {
val dao = CheeseDb.get(app).cheeseDao()
//LiveData類型數據
val allCheese = dao.findAllCheese().toLiveData(
Config(
pageSize = 30,
enablePlaceholders = true,
maxSize = 200
)
)
}
以上:則一個Paging最簡單的列表完成,可以看到用的如下幾個核心類:DataSource.Factory 、 PagedListAdapter、 DiffUtil.ItemCallback、PagedListBuilder、DataSource和Room數據庫的使用
接下來通過單獨介紹這幾種組件和關係,來探究Paging框架
一、分頁組件的簡介
1.核心類 PagedList
上文提到,一個普通的RecyclerView
展示的是一個列表的數據,比如List
,但在列表分頁的需求中,列表局部更新或者差分異比對,顯然一個List
不太夠用了。
爲此,Google
設計出了一個新的角色PagedList
,顧名思義,該角色的意義就是 分頁列表數據的容器 。
既然有了List
,爲什麼需要額外設計這樣一個PagedList
的數據結構?本質原因在於加載分頁數據的操作是異步的 ,因此定義PagedList
的第二個作用是 對分頁數據的異步加載 ,這個我們後文再提。
所以ViewModel可以定義成這樣,因爲PagedList
也作爲列表數據的容器(就像List
一樣):
class viewModel :viewModel(){
//before
//val users :LiveData<List<User>> = dao.findAllUsers()
//after
val users:LiveData<PagedList<User>> = dao.findAllUsers()
}
在ViewModel
中,開發者可以輕易通過對users
進行訂閱以響應分頁數據的更新,這個LiveData
的可觀察者是通過Room
組件創建的,我們來看一下我們的dao
:
@Dao
interface UserDao {
// 注意,這裏 LiveData<List<User>> 改成了 LiveData<PagedList<User>>
@Query("SELECT * FROM user")
fun queryUsers(): LiveData<PagedList<User>>
}
乍得一看似乎理所當然,但實際需求中有一個問題,這裏的定義是模糊不清的——對於分頁數據而言,不同的業務場景,所需要的相關配置是不同的。那麼什麼是分頁相關配置呢?
最直接的一點是每頁數據的加載數量PageSize
,不同的項目都會自行規定每頁數據量的大小,一頁請求15個數據還是20個數據?所以接下來DataSource
和PagedListBuilder
對象,通過簡單的配置將數據源和分頁Page的相關屬性。
2.數據源:DataSource及其工廠
回答這個問題之前,我們還需要定義一個角色,用來爲PagedList
容器提供分頁數據,那就是數據源DataSource
。
什麼是DataSource呢?可以理解爲 數據庫數據 或者是 服務端數據 的一個快照,而不應該是數據庫數據或者是服務端數據
每當Paging
被告知需要更多的數據的時候,數據源DataSource
就會將當前的快照對應的索引的數據交給PagedList
處理
但是需要構建一個新的PagedList
的時候,比如數據已經失效,DataSource
中舊的數據就有意義了,因爲DataSource
需要被重置
在代碼中,這意味着新的DataSource對象被創建,因此,我們需要提供的不是DataSource
,而是提供DataSource的工廠(DataSouce.Factory
) 這就是爲什麼查找數據庫的時候,返回的事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>
}
返回的是一個數據源的提供者DataSource.Factory
,頁面初始化時,會通過工廠方法創建一個新的DataSource
,這之後對應會創建一個新的PagedList
,每當PagedList
想要獲取下一頁的數據,數據源都會根據請求索引進行數據的提供。
當數據失效時,DataSource.Factory
會再次創建一個新的DataSource
,其內部包含了最新的數據快照(本案例中代表着數據庫中的最新數據),隨後創建一個新的PagedList
,並從DataSource
中取最新的數據進行展示——當然,這之後的分頁流程都是相同的,無需再次複述。
引用一幅圖用於描述三者之間的關係,讀者可參考上述文字和圖片加以理解
3.串聯兩者:PagedListBuilder
分頁中的相關業務配置,如每次加載多少條數據等等
現在在Dao中接口的返回值已經是DataSource.Factory,而ViewModel中的成員被觀察者則是LiveData<PagedList>類型,那麼如何將數據源的工廠DataSource.Factory,和LiveData進行串聯?
因此需要定義一個新的角色PagedListBuilder
,開發者將 數據源工廠
和 相關配置
統一交給PagedListBuilder
,即可生成對應的LiveData<PagedList>:
class MyViewModel(val dao: UserDao) : ViewModel() {
val users: LiveData<PagedList<User>>
init {
// 1.創建DataSource.Factory
val factory: DataSource.Factory = dao.queryUsers()
// 2.通過LivePagedListBuilder配置工廠和pageSize, 對users進行實例化
// users = LivePagedListBuilder(factory, config).build()
// 也可以是具體的config對象,定製更多的配置參數
users = LivePagedListBuilder(factory, 30).build()
}
}
如代碼所示:在viewmodel中先通過dao獲取到DataSource.Factory,工廠創建數據源DataSource,後者爲PagedList提供列表所需要的數據;此外,另外一個Int類型的參數則制定每頁數據加載的數量,這裏指定數量爲30
所以在viewmodel中創建了一個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.每當數據更新,計算新舊數據集的差異,對列表進行更新
adapter.submitList(it)
}
}
}
4.更多可選的配置:PagedList.Config
目前介紹中,分頁的功能大致已經介紹完成,但是這些在現實開發中往往不夠,因此,設計者額外定義了更復雜的數據結構PagedList.Config,以描述更細節化的配置參數
// after
val config = PagedList.Config.Builder()
.setPageSize(15) // 分頁加載的數量
.setInitialLoadSizeHint(30) // 初次加載的數量
.setPrefetchDistance(10) // 預取數據的距離
.setEnablePlaceholders(false) // 是否啓用佔位符
.build()
// API發生了改變
val users: LiveData<PagedList<User>> = LivePagedListBuilder(factory, config).build()
4.1.分頁數量:PageSize
最易理解的配置,分頁請求數據時,開發者總是需要定義每頁加載數據的數量。
4.2.初始加載數量:InitialLoadSizeHint
定義首次加載時要加載的Item
數量。
此值通常大於PageSize
,因此在初始化列表時,該配置可以使得加載的數據保證屏幕可以小範圍的滾動。
如果未設置,則默認爲PageSize
的三倍。
4.3.預取距離:PrefetchDistance
顧名思義,該參數配置定義了列表當距離加載邊緣多遠時進行分頁的請求,默認大小爲PageSize
——即距離底部還有一頁數據時,開啓下一頁的數據加載。
若該參數配置爲0,則表示除非明確要求,否則不會加載任何數據,通常不建議這樣做,因爲這將導致用戶在滾動屏幕時看到佔位符或列表的末尾。
4.4.是否啓用佔位符:PlaceholderEnabled
該配置項需要傳入一個boolean
值以決定列表是否開啓placeholder
(佔位符),在知道DataSource知道總數的情況下,設置爲true,則可實現骨架屏的效果
4.5 更多觀察者類型的配置
在本文的示例中,我們建立了一個LiveData>
的可觀察者對象供用戶響應數據的更新,實際上組件的設計應該面向提供對更多優秀異步庫的支持,比如RxJava
。
因此,和LivePagedListBuilder
一樣,設計者還提供了RxPagedListBuilder
,通過DataSource
數據源和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數據源簡介
ItemKeyedDataSource<Key, Value>, PageKeyedDataSource<Key, Value>, PositionalDataSource
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.
用於將快照數據頁加載到PagedList的基類。
查詢數據源以將內容頁加載到PagedList中。頁面列表可以隨着加載更多數據而增長,但無法更新加載的數據。如果修改了基礎數據集,則必須創建一個新的pagelist/DataSource對來表示新數據。
Paging
分頁組件的設計中,DataSource
是一個非常重要的模塊。顧名思義,DataSource
中的Key
對應數據加載的條件,Value
對應數據集的實際類型, 針對不同場景,Paging
的設計者提供了三種不同類型的DataSource
抽象類:
PositionalDataSource
ItemKeyedDataSource
PageKeyedDataSource
接下來我們分別對其進行簡單的介紹。
1.PositionalDataSource
PositionalDataSource
是最簡單的DataSource
類型,顧名思義,其通過數據所處當前數據集快照的位置(position
)提供數據。
PositionalDataSource
適用於 目標數據總數固定,通過特定的位置加載數據,這裏Key
是Integer
類型的位置信息,並且被內置固定在了PositionalDataSource
類中,T
即數據的類型。
最容易理解的例子就是本文的聯繫人列表,其所有的數據都來自本地的數據庫,這意味着,數據的總數是固定的,我們總是可以根據當前條目的position
映射到DataSource
中對應的一個數據。
PositionalDataSource
也正是Room
幕後實現的功能,使用Room
爲什麼可以避免DataSource
的配置,通過dao
中的接口就能返回一個DataSource.Factory
?
來看Room
組件配置的dao
對應編譯期生成的源碼:
// 1.Room自動生成了 DataSource.Factory
@Override
public DataSource.Factory<Integer, Student> getAllStudent() {
// 2.工廠函數提供了PositionalDataSource
return new DataSource.Factory<Integer, Student>() {
@Override
public PositionalDataSource<Student> create() {
return new PositionalDataSource<Student>(__db, _statement, false , "Student") {
// ...
};
}
};
}
2.ItemKeyedDataSource
ItemKeyedDataSource
適用於目標數據的加載依賴特定條目的信息,比如需要根據第N項的信息加載第N+1項的數據,傳參中需要傳入第N項的某些信息時。
使用場景:如QQ或者wechat中的聊天記錄
3.PageKeyedDataSource
這也是最常用的DataSource,更多的用於網絡請求API
中,服務器返回的數據中都會包含一個String
類型類似nextPage
的字段,以表示當前頁數據的下一頁數據的接口(比如Github
的API
),這種分頁數據加載的方式正是PageKeyedDataSource
的拿手好戲。
這是日常開發中用到最多的DataSource
類型,和ItemKeyedDataSource
不同的是,前者的數據檢索關係是單個數據與單個數據之間的,後者則是每一頁數據和每一頁數據之間的。
同樣拿聯繫人列表舉例,這種分頁加載方式是按照頁碼進行數據加載的,比如一次請求15條數據,服務器返回數據列表的同時會返回下一頁數據的url
(或者頁碼),藉助該參數請求下一頁數據成功後,服務器又回返回下下一頁的url
,以此類推。
總的來說,DataSource
針對不同種數據分頁的加載策略提供了不同種的抽象類以方便開發者調用,很多情況下,同樣的業務使用不同的DataSource
都能夠實現,開發者按需取用即可。
三、通過Paging加載網絡數據列表
通過以上,相信讀者能明白Paging中的核心類的認識和作用,接下來,通過Paging加載一個簡單的網絡列表,具體的實現自定義DataSource和Repository等,更加深刻的理解Paging框架。
請求地址爲:https://www.wanandroid.com/article/list/0/json (感謝玩Android)
效果圖如下圖所示:
- Activity中的代碼如下
class NetPagingActivity : AppCompatActivity() {
lateinit var mAdapter: ArticleAdapter
//加載數據使用的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()
}
//請求數據,並更新列表
private fun getData() {
viewModel.data.observe(this, Observer {
mAdapter.submitList(it)
})
}
}
- ArticleViewModel
ViewModel很簡單,通過NetRepository().getData()獲取DataSource中的可觀察數據
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爲具體加載服務器數據的快照
val source = NetDataSource()
sourceLiveData.postValue(source)
return source
}
}
- NetDataSource
通過繼承PageKeyedDataSource,因爲請求的列表是根據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框架已介紹完成,待後續更新Jetpack更多的組件!
文章中的所有示例代碼已上傳至github:https://github.com/OnexZgj/Jetpack_Component
##詳細介紹文章