之前的幾篇源碼分析我們分別對(duì)
Navigation
租副、Lifecycles
、ViewModel
较性、LiveData
用僧、進(jìn)行了分析,也對(duì)JetPack有了更深入的了解赞咙。但是Jetpack遠(yuǎn)不止這些組件责循,今天的主角---Paging,Jetpack中的分頁(yè)組件攀操,官方是這么形容它的:‘’逐步從您的數(shù)據(jù)源按需加載信息‘’
如果你對(duì)Jetpack組件有了解或者想對(duì)源碼有更深入的了解院仿,請(qǐng)看我之前的幾篇文章:
1. Jetpack源碼解析---看完你就知道Navigation是什么了?
2. Jetpack源碼解析---Navigation為什么切換Fragment會(huì)重繪速和?
3. Jetpack源碼解析---用Lifecycles管理生命周期
4. Jetpack源碼解析—LiveData的使用及工作原理
5. Jetpack源碼解析---ViewModel基本使用及源碼解析
1. 背景
在我的Jetpack_Note系列中歹垫,對(duì)每一篇的分析都有相對(duì)應(yīng)的代碼片段及使用,我把它做成了一個(gè)APP颠放,目前功能還不完善排惨,代碼我也上傳到了GitHub上,參考了官方的Demo以及目前網(wǎng)上的一些文章碰凶,有興趣的小伙伴可以看一下暮芭,別忘了給個(gè)Star。
https://github.com/Hankkin/JetPack_Note
今天我們的主角是Paging欲低,介紹之前我們先看一下效果:
2. 簡(jiǎn)介
2.1 基本介紹
官方定義:
分頁(yè)庫(kù)Pagin Library是Jetpack的一部分辕宏,它可以妥善的逐步加載數(shù)據(jù),幫助您一次加載和顯示一部分?jǐn)?shù)據(jù)砾莱,這樣的按需加載可以減少網(wǎng)絡(luò)貸款和系統(tǒng)資源的使用瑞筐。分頁(yè)庫(kù)支持加載有限以及無(wú)限的list,比如一個(gè)持續(xù)更新的信息源腊瑟,分頁(yè)庫(kù)可以與RecycleView無(wú)縫集合聚假,它還可以與LiveData或RxJava集成块蚌,觀察界面中的數(shù)據(jù)變化。
2.2 核心組件
1. PagedList
PageList是一個(gè)集合類(lèi)魔策,它以分塊的形式異步加載數(shù)據(jù),每一塊我們稱(chēng)之為頁(yè)河胎。它繼承自AbstractList
,支持所有List的操作闯袒,它的內(nèi)部有五個(gè)主要變量:
- mMainThreadExecutor 主線程Executor,用于將結(jié)果傳遞到主線程
- mBackgroundThreadExecutor 后臺(tái)線程游岳,執(zhí)行負(fù)載業(yè)務(wù)邏輯
- BoundaryCallback 當(dāng)界面顯示緩存中靠近結(jié)尾的數(shù)據(jù)的時(shí)候政敢,它將加載更多的數(shù)據(jù)
- Config PageList從DataSource中加載數(shù)據(jù)的配置
- PagedStorage<T> 用于存儲(chǔ)加載到的數(shù)據(jù)
Config屬性:
- pageSize:分頁(yè)加載的數(shù)量
- prefetchDistance:預(yù)加載的數(shù)量
- initialLoadSizeHint:初始化數(shù)據(jù)時(shí)加載的數(shù)量,默認(rèn)為pageSize*3
- enablePlaceholders:當(dāng)item為null是否使用placeholder顯示
PageList會(huì)通過(guò)DataSource加載數(shù)據(jù)胚迫,通過(guò)Config的配置喷户,可以設(shè)置一次加載的數(shù)量以及預(yù)加載的數(shù)量。除此之外访锻,PageList還可以想RecycleView.Adapter發(fā)送更新的信號(hào)褪尝,驅(qū)動(dòng)UI的刷新。
2. DataSource
DataSource<Key,Value> 顧名思義就是數(shù)據(jù)源期犬,它是一個(gè)抽象類(lèi)河哑,其中Key
對(duì)應(yīng)加載數(shù)據(jù)的條件信息,Value
對(duì)應(yīng)加載數(shù)據(jù)的實(shí)體類(lèi)龟虎。Paging庫(kù)中提供了三個(gè)子類(lèi)來(lái)讓我們?cè)诓煌瑘?chǎng)景的情況下使用:
- PageKeyedDataSource:如果后端API返回?cái)?shù)據(jù)是分頁(yè)之后的璃谨,可以使用它;例如:官方Demo中GitHub API中的SearchRespositories就可以返回分頁(yè)數(shù)據(jù)鲤妥,我們?cè)贕itHub API的請(qǐng)求中制定查詢(xún)關(guān)鍵字和想要的哪一頁(yè)佳吞,同時(shí)也可以指明每個(gè)頁(yè)面的項(xiàng)數(shù)。
- ItemKeyedDataSource:如果通過(guò)鍵值請(qǐng)求后端數(shù)據(jù)棉安;例如我們需要獲取在某個(gè)特定日期起Github的前100項(xiàng)代碼提交記錄底扳,該日期將成為DataSource的鍵,ItemKeyedDataSource允許自定義如何加載初始頁(yè);該場(chǎng)景多用于評(píng)論信息等類(lèi)似請(qǐng)求
- PositionalDataSource:適用于目標(biāo)數(shù)據(jù)總數(shù)固定贡耽,通過(guò)特定的位置加載數(shù)據(jù)花盐,這里Key是Integer類(lèi)型的位置信息,T即Value菇爪。 比如從數(shù)據(jù)庫(kù)中的1200條開(kāi)始加在20條數(shù)據(jù)算芯。
3. PagedListAdapter
PageListAdapter繼承自RecycleView.Adapter,和RecycleView實(shí)現(xiàn)方式一樣,當(dāng)數(shù)據(jù)加載完畢時(shí)凳宙,通知RecycleView數(shù)據(jù)加載完畢熙揍,RecycleView填充數(shù)據(jù);當(dāng)數(shù)據(jù)發(fā)生變化時(shí)氏涩,PageListAdapter會(huì)接受到通知届囚,交給委托類(lèi)AsyncPagedListDiffer來(lái)處理有梆,AsyncPagedListDiffer是對(duì)DiffUtil.ItemCallback<T>持有對(duì)象的委托類(lèi),AsyncPagedListDiffer使用后臺(tái)線程來(lái)計(jì)算PagedList的改變,item是否改變,由DiffUtil.ItemCallback<T>決定。
3.基本使用
3.1 添加依賴(lài)包
implementation "androidx.paging:paging-runtime:$paging_version" // For Kotlin use paging-runtime-ktx
implementation "androidx.paging:paging-runtime-ktx:$paging_version" // For Kotlin use paging-runtime-ktx
// alternatively - without Android dependencies for testing
testImplementation "androidx.paging:paging-common:$paging_version" // For Kotlin use paging-common-ktx
// optional - RxJava support
implementation "androidx.paging:paging-rxjava2:$paging_version" // For Kotlin use paging-rxjava2-ktx
3.2 PagingWithRoom使用
新建UserDao
/**
* created by Hankkin
* on 2019-07-19
*/
@Dao
interface UserDao {
@Query("SELECT * FROM User ORDER BY name COLLATE NOCASE ASC")
fun queryUsersByName(): DataSource.Factory<Int, User>
@Insert
fun insert(users: List<User>)
@Insert
fun insert(user: User)
@Delete
fun delete(user: User)
}
創(chuàng)建UserDB數(shù)據(jù)庫(kù)
/**
* created by Hankkin
* on 2019-07-19
*/
@Database(entities = arrayOf(User::class), version = 1)
abstract class UserDB : RoomDatabase() {
abstract fun userDao(): UserDao
companion object {
private var instance: UserDB? = null
@Synchronized
fun get(context: Context): UserDB {
if (instance == null) {
instance = Room.databaseBuilder(context.applicationContext,
UserDB::class.java, "UserDatabase")
.addCallback(object : RoomDatabase.Callback() {
override fun onCreate(db: SupportSQLiteDatabase) {
fillInDb(context.applicationContext)
}
}).build()
}
return instance!!
}
/**
* fill database with list of cheeses
*/
private fun fillInDb(context: Context) {
// inserts in Room are executed on the current thread, so we insert in the background
ioThread {
get(context).userDao().insert(
CHEESE_DATA.map { User(id = 0, name = it) })
}
}
}
}
創(chuàng)建PageListAdapter
/**
* created by Hankkin
* on 2019-07-19
*/
class PagingDemoAdapter : PagedListAdapter<User, PagingDemoAdapter.ViewHolder>(diffCallback) {
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int) =
ViewHolder(AdapterPagingItemBinding.inflate(LayoutInflater.from(parent.context), parent, false))
override fun onBindViewHolder(holder: ViewHolder, position: Int) {
val item = getItem(position)
holder.apply {
bind(createOnClickListener(item), item)
itemView.tag = item
}
}
private fun createOnClickListener(item: User?): View.OnClickListener {
return View.OnClickListener {
Toast.makeText(it.context, item?.name, Toast.LENGTH_SHORT).show()
}
}
class ViewHolder(private val binding: AdapterPagingItemBinding) : RecyclerView.ViewHolder(binding.root) {
fun bind(listener: View.OnClickListener, item: User?) {
binding.apply {
clickListener = listener
user = item
executePendingBindings()
}
}
}
companion object {
/**
* This diff callback informs the PagedListAdapter how to compute list differences when new
* PagedLists arrive.
* <p>
* When you add a Cheese with the 'Add' button, the PagedListAdapter uses diffCallback to
* detect there's only a single item difference from before, so it only needs to animate and
* rebind a single view.
*
* @see android.support.v7.util.DiffUtil
*/
private val diffCallback = object : DiffUtil.ItemCallback<User>() {
override fun areItemsTheSame(oldItem: User, newItem: User): Boolean =
oldItem.id == newItem.id
/**
* Note that in kotlin, == checking on data classes compares all contents, but in Java,
* typically you'll implement Object#equals, and use it to compare object contents.
*/
override fun areContentsTheSame(oldItem: User, newItem: User): Boolean =
oldItem == newItem
}
}
}
ViewModel承載數(shù)據(jù)
class PagingWithDaoViewModel internal constructor(private val pagingRespository: PagingRespository) : ViewModel() {
val allUsers = pagingRespository.getAllUsers()
fun insert(text: CharSequence) {
pagingRespository.insert(text)
}
fun remove(user: User) {
pagingRespository.remove(user)
}
}
Activity中觀察到數(shù)據(jù)源的變化后意系,會(huì)通知Adapter自動(dòng)更新數(shù)據(jù)
class PagingWithDaoActivity : AppCompatActivity() {
private lateinit var viewModel: PagingWithDaoViewModel
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_paging_with_dao)
setLightMode()
setupToolBar(toolbar) {
title = resources.getString(R.string.paging_with_dao)
setDisplayHomeAsUpEnabled(true)
}
viewModel = obtainViewModel(PagingWithDaoViewModel::class.java)
val adapter = PagingDemoAdapter()
rv_paging.adapter = adapter
viewModel.allUsers.observe(this, Observer(adapter::submitList))
}
override fun onOptionsItemSelected(item: MenuItem?): Boolean {
when (item?.itemId) {
android.R.id.home -> finish()
}
return super.onOptionsItemSelected(item)
}
}
3.3 PagingWithNetWork 使用
上面我們通過(guò)Room進(jìn)行了數(shù)據(jù)庫(kù)加載數(shù)據(jù)泥耀,下面看一下通過(guò)網(wǎng)絡(luò)請(qǐng)求記載列表數(shù)據(jù):
和上面不同的就是Respository數(shù)據(jù)源的加載,之前我們是通過(guò)Room加載DB數(shù)據(jù)蛔添,現(xiàn)在我們要通過(guò)網(wǎng)絡(luò)獲取數(shù)據(jù):
GankRespository 干貨數(shù)據(jù)源倉(cāng)庫(kù)
/**
* created by Hankkin
* on 2019-07-30
*/
class GankRespository {
companion object {
private const val PAGE_SIZE = 20
@Volatile
private var instance: GankRespository? = null
fun getInstance() =
instance ?: synchronized(this) {
instance
?: GankRespository().also { instance = it }
}
}
fun getGank(): Listing<Gank> {
val sourceFactory = GankSourceFactory()
val config = PagedList.Config.Builder()
.setPageSize(PAGE_SIZE)
.setInitialLoadSizeHint(PAGE_SIZE * 2)
.setEnablePlaceholders(false)
.build()
val livePageList = LivePagedListBuilder<Int, Gank>(sourceFactory, config).build()
val refreshState = Transformations.switchMap(sourceFactory.sourceLiveData) { it.initialLoad }
return Listing(
pagedList = livePageList,
networkState = Transformations.switchMap(sourceFactory.sourceLiveData) { it.netWorkState },
retry = { sourceFactory.sourceLiveData.value?.retryAllFailed() },
refresh = { sourceFactory.sourceLiveData.value?.invalidate() },
refreshState = refreshState
)
}
}
可以看到getGank()方法返回了Listing<Gank>,那么Listing
是個(gè)什么呢痰催?
/**
* Data class that is necessary for a UI to show a listing and interact w/ the rest of the system
* 封裝需要監(jiān)聽(tīng)的對(duì)象和執(zhí)行的操作,用于上拉下拉操作
* pagedList : 數(shù)據(jù)列表
* networkState : 網(wǎng)絡(luò)狀態(tài)
* refreshState : 刷新?tīng)顟B(tài)
* refresh : 刷新操作
* retry : 重試操作
*/
data class Listing<T>(
// the LiveData of paged lists for the UI to observe
val pagedList: LiveData<PagedList<T>>,
// represents the network request status to show to the user
val networkState: LiveData<NetworkState>,
// represents the refresh status to show to the user. Separate from networkState, this
// value is importantly only when refresh is requested.
val refreshState: LiveData<NetworkState>,
// refreshes the whole data and fetches it from scratch.
val refresh: () -> Unit,
// retries any failed requests.
val retry: () -> Unit)
Listing是我們封裝的一個(gè)數(shù)據(jù)類(lèi)迎瞧,將數(shù)據(jù)源夸溶、網(wǎng)絡(luò)狀態(tài)、刷新?tīng)顟B(tài)凶硅、下拉刷新操作以及重試操作都封裝進(jìn)去了缝裁。那么我們的數(shù)據(jù)源從哪里獲取呢,可以看到Listing的第一個(gè)參數(shù)pageList = livePageList
,livePageList
通過(guò)LivePagedListBuilder創(chuàng)建足绅,LivePagedListBuilder需要兩個(gè)參數(shù)(DataSource
,PagedList.Config
):
GankSourceFactory
?
/**
* created by Hankkin
* on 2019-07-30
*/
class GankSourceFactory(private val api: Api = Injection.provideApi()) : DataSource.Factory<Int, Gank>(){
val sourceLiveData = MutableLiveData<GankDataSource>()
override fun create(): DataSource<Int, Gank> {
val source = GankDataSource(api)
sourceLiveData.postValue(source)
return source
}
}
GankDataSource
/**
* created by Hankkin
* on 2019-07-30
*/
class GankDataSource(private val api: Api = Injection.provideApi()) : PageKeyedDataSource<Int, Gank>() {
private var retry: (() -> Any)? = null
val netWorkState = MutableLiveData<NetworkState>()
val initialLoad = MutableLiveData<NetworkState>()
fun retryAllFailed() {
val prevRetry = retry
retry = null
prevRetry?.also { it.invoke() }
}
override fun loadInitial(params: LoadInitialParams<Int>, callback: LoadInitialCallback<Int, Gank>) {
initialLoad.postValue(NetworkState.LOADED)
netWorkState.postValue(NetworkState.HIDDEN)
api.getGank(params.requestedLoadSize, 1)
.enqueue(object : Callback<GankResponse> {
override fun onFailure(call: Call<GankResponse>, t: Throwable) {
retry = {
loadInitial(params, callback)
}
initialLoad.postValue(NetworkState.FAILED)
}
override fun onResponse(call: Call<GankResponse>, response: Response<GankResponse>) {
if (response.isSuccessful) {
retry = null
callback.onResult(
response.body()?.results ?: emptyList(),
null,
2
)
initialLoad.postValue(NetworkState.LOADED)
} else {
retry = {
loadInitial(params, callback)
}
initialLoad.postValue(NetworkState.FAILED)
}
}
})
}
override fun loadAfter(params: LoadParams<Int>, callback: LoadCallback<Int, Gank>) {
netWorkState.postValue(NetworkState.LOADING)
api.getGank(params.requestedLoadSize, params.key)
.enqueue(object : Callback<GankResponse> {
override fun onFailure(call: Call<GankResponse>, t: Throwable) {
retry = {
loadAfter(params, callback)
}
netWorkState.postValue(NetworkState.FAILED)
}
override fun onResponse(call: Call<GankResponse>, response: Response<GankResponse>) {
if (response.isSuccessful) {
retry = null
callback.onResult(
response.body()?.results ?: emptyList(),
params.key + 1
)
netWorkState.postValue(NetworkState.LOADED)
} else {
retry = {
loadAfter(params, callback)
}
netWorkState.postValue(NetworkState.FAILED)
}
}
})
}
override fun loadBefore(params: LoadParams<Int>, callback: LoadCallback<Int, Gank>) {
}
}
網(wǎng)絡(luò)請(qǐng)求的核心代碼在GankDataSource中捷绑,因?yàn)槲覀兊恼?qǐng)求是分頁(yè)請(qǐng)求,所以這里的GankDataSource
我們繼承自PageKeyedDataSource
,它實(shí)現(xiàn)了三個(gè)方法:
loadInitial
: 初始化加載氢妈,初始加載的數(shù)據(jù) 也就是我們直接能看見(jiàn)的數(shù)據(jù)
loadAfter
: 下一頁(yè)加載胎食,每次傳遞的第二個(gè)參數(shù) 就是 你加載數(shù)據(jù)依賴(lài)的key
loadBefore
: 往上滑加載的數(shù)據(jù)
可以看到我們?cè)?code>loadInitial中設(shè)置了initialLoad
和netWorkState
的狀態(tài)值,同時(shí)通過(guò)RetrofitApi獲取網(wǎng)絡(luò)數(shù)據(jù)允懂,并在成功和失敗的回調(diào)中對(duì)數(shù)據(jù)和網(wǎng)絡(luò)狀態(tài)值以及加載初始化做了相關(guān)的設(shè)置厕怜,具體就不介紹了,可看代碼蕾总。loadAfter
同理粥航,只不過(guò)我們?cè)诩虞d數(shù)據(jù)后對(duì)key也就是我們的page進(jìn)行了+1操作。
Config參數(shù)就是我們對(duì)分頁(yè)加載的一些配置:
val config = PagedList.Config.Builder()
.setPageSize(PAGE_SIZE)
.setInitialLoadSizeHint(PAGE_SIZE * 2)
.setEnablePlaceholders(false)
.build()
下面看我們?cè)贏ctivity中怎樣使用:
PagingWithNetWorkActivity
class PagingWithNetWorkActivity : AppCompatActivity() {
private lateinit var mViewModel: PagingWithNetWorkViewModel
private lateinit var mDataBinding: ActivityPagingWithNetWorkBinding
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
mDataBinding = DataBindingUtil.setContentView(this,R.layout.activity_paging_with_net_work)
setLightMode()
setupToolBar(toolbar) {
title = resources.getString(R.string.paging_with_network)
setDisplayHomeAsUpEnabled(true)
}
mViewModel = obtainViewModel(PagingWithNetWorkViewModel::class.java)
mDataBinding.vm = mViewModel
mDataBinding.lifecycleOwner = this
val adapter = PagingWithNetWorkAdapter()
mDataBinding.rvPagingWithNetwork.adapter = adapter
mDataBinding.vm?.gankList?.observe(this, Observer { adapter.submitList(it) })
mDataBinding.vm?.refreshState?.observe(this, Observer {
mDataBinding.rvPagingWithNetwork.post {
mDataBinding.swipeRefresh.isRefreshing = it == NetworkState.LOADING
}
})
mDataBinding.vm?.netWorkState?.observe(this, Observer {
adapter.setNetworkState(it)
})
}
override fun onOptionsItemSelected(item: MenuItem?): Boolean {
when (item?.itemId) {
android.R.id.home -> finish()
}
return super.onOptionsItemSelected(item)
}
}
ViewModel
中的gankList
是一個(gè)LiveData
生百,所以我們?cè)谶@里給它設(shè)置一個(gè)觀察递雀,當(dāng)數(shù)據(jù)變動(dòng)是調(diào)用adapter.submitList(it)
,刷新數(shù)據(jù),這個(gè)方法是PagedListAdapter中的蚀浆,里面回去檢查新數(shù)據(jù)和舊數(shù)據(jù)是否相同缀程,也就是上面我們提到的AsyncPagedListDiffer
來(lái)實(shí)現(xiàn)的。到這里整個(gè)流程就已經(jīng)結(jié)束了市俊,想看源碼可以到Github上杨凑。
4. 總結(jié)
我們先看下官網(wǎng)給出的gif圖:
總結(jié)一下,Paging的基本原理為:
- 使用DataSource從網(wǎng)絡(luò)或者數(shù)據(jù)庫(kù)獲取數(shù)據(jù)
- 將數(shù)據(jù)保存到PageList中
- 將PageList中的數(shù)據(jù)提交給PageListAdapter
- PageListAdapter在后臺(tái)線程中通過(guò)Diff對(duì)比新老數(shù)據(jù)摆昧,反饋到RecycleView中
- RecycleView刷新數(shù)據(jù)
基本原理在圖上我們可以很清晰的了解到了撩满,本篇文章的Demo中結(jié)合了ViewModel以及DataBinding進(jìn)行了數(shù)據(jù)的存儲(chǔ)和綁定。
最后代碼地址: